Start Coding with Doctrine 2 ORM!

Start Coding with Doctrine 2 ORM!

Finally decided to use ORM instead building all data mapping myself in my applications.

doctrine.png
Parent post: Online Book Catalog Application Tutorial

I was trying to convince myself start using Doctrine 2 ORM in my Zend Framework applications for a long while. Apparently today is the day!

I started this series by using TableGateway pattern. Meanwhile I was studying Doctrine 2 ORM to build more efficient applications by also coding faster. In fact I loved ORM so much and decided to migrate this tutorial series by using Doctrine 2 ORM as well.

Basically, I will apply the following changes and refactor the Online Book Catalog Application by using Doctrine 2 ORM:

  • Install Doctrine 2 ORM by following the instructions at Doctrine 2 ORM Module for Zend Framework 2.
  • Configure module.config.php and global.php to work with Doctrine entity manager.
  • Change BlogControllerFactory to inject Doctrine entity manager instead previously used mapper.
  • Adjust BlogController accordingly.
  • Create an Entity folder to create and store annotated entity classes.
  • Some command line actions to build database from entities by using CLI commands.
    Note: I will won't create database tables manually from now on!
  • Delete Model folder for good!

Installing Doctrine 2 ORM for Zend Framework can be easily done with the composer.


[smozgur@local OnlineBookCatalog]$ composer require doctrine/doctrine-orm-module

To register the book entity with the ORM, I need to add the following driver configuration into the module configuration file.

# /module/Application/config/module.config.php

<?php
namespace Application;

// ...

use Doctrine\ORM\Mapping\Driver\AnnotationDriver;

return [
    // ...
    
    'doctrine' => [
        'driver' => [
            __NAMESPACE__ . '_driver' => [
                'class' => AnnotationDriver::class,
                'cache' => 'array',
                'paths' => [
                    __DIR__ . '/../src/Entity',
                ]
            ],
            'orm_default' => [
                'drivers' => [
                    __NAMESPACE__ . '\Entity' => __NAMESPACE__ . '_driver',
                ]
            ]
        ],
    ]
    
    // ...
];
This must be done for all modules that require entity access in the application. Therefore, I prefixed the driver configuration entry with the current namespace.

I need to introduce my database to the Doctrine module. I provided similar information to create a database adapter in global.php before, and I will still use the same global config file for Doctrine database configuration:

# /config/autoload/global.php

<?php
use Doctrine\DBAL\Driver\PDOMySql\Driver as PDOMySqlDriver;

return [
    'doctrine' => [
        'connection' => [
            'orm_default' => [
                'driverClass' => PDOMySqlDriver::class,
                'params' => [
                    'host'     => 'localhost',                    
                    'user'     => 'dbuser',
                    'password' => 'dbpwd',
                    'dbname'   => 'dbname',
                    'charset'  => 'utf8',
                ]
            ],            
        ],        
    ],
];

And it is time to create the Book entity. In fact, there is an existing book entity created for TableGateway version previously. The new entity class will have the same variables and setters/getters.

Additionally, Doctrine requires mapping the entity with the database by using metadata which I will specify by using Docblock Annotations.

# /module/Application/src/Entity/Book.php

<?php
namespace Application\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * Book
 *
 * @ORM\Table(name="book")
 * @ORM\Entity
 *
 */
class Book
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer", nullable=false)
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $id;
    
    /**
     * @var string
     *
     * @ORM\Column(name="alias", type="string", length=150, nullable=false, unique=true)
     *
     */
    private $alias;
    
    /**
     * @var string
     *
     * @ORM\Column(name="title", type="string", length=150)
     *
     */
    protected $title;
    
    /**
     * @var string
     *
     * @ORM\Column(name="author", type="string", length=50)
     *
     */
    protected $author;
    
    /**
     * @var string
     *
     * @ORM\Column(name="description", type="string", length=500, nullable=true)
     *
     */
    protected $description;
    
    /**
     * @var \DateTime
     *
     * @ORM\Column(name="publish_date", type="date", nullable=true)
     *
     */
    protected $publishDate;
    
    /**
     * @var string
     *
     * @ORM\Column(name="isbn", type="string", length=14, nullable=true)
     *
     */
    protected $isbn;
    
    /**
     * @var string
     *
     * @ORM\Column(name="language", type="string", length=20, nullable=true)
     *
     */
    protected $language;
    
    /**
     * Get id
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }
    
    /**
     * Get alias
     *
     * @return string
     */
    public function getAlias()
    {
        return $this->alias;
    }
    
    /**
     * Set alias
     *
     * @param string $alias
     *
     * @return Book
     */
    public function setAlias($alias)
    {
        $this->alias = $alias;
        return $this;
    }
    
    /**
     * Get title
     *
     * @return string
     */
    public function getTitle()
    {
        return $this->title;
    }
    
    /**
     * Set title
     *
     * @param string $title
     *
     * @return Book
     */
    public function setTitle($title)
    {
        $this->title = $title;
        return $this;
    }
    
    /**
     * Get author
     *
     * @return string
     */
    public function getAuthor()
    {
        return $this->author;
    }
    
    /**
     * Set author
     *
     * @param string $author
     *
     * @return Book
     */
    public function setAuthor($author)
    {
        $this->author = $author;
        return $this;
    }
    
    /**
     * Get description
     *
     * @return string
     */
    public function getDescription()
    {
        return $this->description;
    }
    
    /**
     * Set description
     *
     * @param string $description
     *
     * @return Book
     */
    public function setDescription($description)
    {
        $this->description = $description;
        return $this;
    }
    
    /**
     * Get publishDate
     *
     * @return \DateTime
     */
    public function getPublishDate()
    {
        return $this->publish_date;
    }
    
    /**
     * Set publishDate
     *
     * @param \DateTime $publishDate
     *
     * @return Book
     */
    public function setPublishDate($publishDate)
    {
        $this->publishDate = $publishDate;
        return $this;
    }
    
    /**
     * Get isbn
     *
     * @return string
     */
    public function getIsbn()
    {
        return $this->isbn;
    }
    
    /**
     * Set isbn
     *
     * @param string $isbn
     *
     * @return Book
     */
    public function setIsbn($isbn)
    {
        $this->isbn = $isbn;
        return $this;
    }
    
    /**
     * Get language
     *
     * @return string
     */
    public function getLanguage()
    {
        return $this->language;
    }
    
    /**
     * Set language
     *
     * @param string $language
     *
     * @return Book
     */
    public function setLanguage($language)
    {
        $this->language = $language;
        return $this;
    }
    
}

As you will notice I used @ORM\Entity() to define the entity and associated with a table called book by using @ORM\Table(name="book") annotations. Entity variables represent corresponding table fields and specified by @ORM\Column annotation with required type and other optional parameters. Finally id variable is also specified by @ORM\Id which marks the variable as identifier - primary key in the database and @ORM\GeneratedValue(strategy="IDENTITY") which defines the identifier generation strategy.

Since I created the Book entity with all the field definitions, I can now create the table in the defined database by using Doctrine schema-tool!

I deleted the existing book table in the database to see how a table can be easily created in the database by using the Doctrine schema tool.

[smozgur@local OnlineBookCatalog]$ ./vendor/bin/doctrine-module orm:schema-tool:create

And importing the same data I used in the earlier post of this series.

-- Sample data for `book` table

INSERT INTO `book` (`id`, `alias`, `title`, `author`, `description`, `publish_date`, `isbn`, `language`) VALUES
(1, 'learn-zf2-2013', 'Learn ZF2: Learning By Example', 'Slavey Karadzhov', 'Zend Framework 2 (ZF2) has changed the way to develop PHP applications and like every revolution takes time to be digested.\r\nThe book will help you understand the major components in ZF2 and how to use them as best as possible. The chapters in this book will lead you through the different components and in the process together with the author you will build a complete application.', '2013-09-25', '978-1492372219', 'English'),
(2, 'advanced-php-programming-2004', 'Advanced PHP Programming', 'George Schlossnagle', 'Over the past three years PHP has evolved from being a niche language use to add dynamic functionality to small sites to a powerful tool making strong inroads into large-scale, business-critical Web systems. While there are many books on learning PHP and developing small applications with it, there is a serious lack of information on "scaling" PHP for large-scale, business-critical systems. Schlossnagle''s Advanced PHP Programming fills that void, demonstrating that PHP is ready for enterprise Web applications by showing the reader how to develop PHP-based applications for maximum performance, stability, and extensibility', '2004-03-01', '978-0672325618', 'English'),
(3, 'php-and-mysql-web-development-2008', 'PHP and MySQL Web Development (4th Edition)', 'Luke Welling', 'PHP and MySQL Web Development shows how to use these tools together to produce effective, interactive Web applications. It clearly describes the basics of the PHP language, explains how to set up and work with a MySQL database, and then shows how to use PHP to interact with the database and the server.', '2008-10-11', '978-0672329166', 'English'),
(4, 'zend-framework-in-action-2009', 'Zend Framework in Action', 'Rob Allen', 'Zend Framework in Action is a comprehensive tutorial that shows how to use the Zend Framework to create web-based applications and web services. This book takes you on an over-the-shoulder tour of the components of the Zend Framework as you build a high quality, real-world web application. This book is organized around the techniques you''ll use every day as a web developer "data handling, forms, authentication, and so forth. As you follow the running example, you''ll learn to build interactive Ajax-driven features into your application without sacrificing nuts-and-bolts considerations like security and performance.', '2009-01-07', '978-1933988320', 'English');

Ok, I have the entity and the associated table with sample data. I will be using the Doctrine entity manager in the controllers to use entities corresponding to the database tables from now on. Therefore, I need to modify the controller factory to inject Doctrine entity manager instead of previously used mapper.

# /module/Application/src/Controller/Factory/CatalogControllerFactory.php

<?php
namespace Application\Controller\Factory;

use Zend\ServiceManager\Factory\FactoryInterface;
use Interop\Container\ContainerInterface;
use Application\Controller\CatalogController;

/**
 * CatalogControllerFactory
 * 
 * Controller Factory for Catalog Controller
 */
class CatalogControllerFactory implements FactoryInterface
{
    /**
     * Handle invoke calls injecting dependencies
     *
     * @return CatalogController
     */
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $entityManager = $container->get('doctrine.entitymanager.orm_default');

        $controller = new CatalogController($entityManager);
        
        return $controller;
    }
}
'doctrine.entitymanager.orm_default' is one of the default registered service name for Doctrine\ORM\EntityManager instance. Controller will be able to access Doctrine entity manager instance by this injected service.

I also need to refactor the CatalogController accordingly.

# /module/Application/src/Controller/CatalogController.php

<?php
namespace Application\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;

use Doctrine\ORM\EntityManager;

use Application\Entity\Book;

// ...
class CatalogController extends AbstractActionController
{
    /**
     * Entity manager.
     * @var EntityManager
     */
    private $entityManager;
    
    /**
     *
     * @param EntityManager $entityManager
     */
    public function __construct(EntityManager $entityManager)
    {
        $this->entityManager = $entityManager;
    }
    
    /**
     * 
     * Action called for displaying the book listing
     * 
     * {@inheritDoc}
     * @see \Zend\Mvc\Controller\AbstractActionController::indexAction()
     */
    public function indexAction()
    {
        $books = $this->entityManager->getRepository(Book::class)->findAll();
        
        return new ViewModel([
            'books' => $books,
        ]);
    }    
}
Please note that I didn't apply pagination and search features as I'd like to implement those separately since it will require creating a custom repository called BookRepository for the entity.

Finally, I'll change the view to remove pagination and search that I originally implemented in the TableGateway version.

# /module/Application/view/application/catalog/index.phtml

<div class="row">
    <div class="col-sm-6">
        <h1>Book Listing</h1>
    </div>
</div>
<hr>
<div class="row">
    <?php /* @var $book \Application\Entity\Book */?>
    <?php foreach ($this->books as $book): ?>
    <div class="col-sm-6 col-md-4">
        <div class="panel panel-default">
            <div class="panel-heading">
                <h5><?= $book->getTitle() ?></h5>
                <h6>by <?= $book->getAuthor() ?></h6>
            </div>
            <div class="panel-body">
                <table class="table table-condensed">
                    <colgroup>
                        <col class="col-xs-5">
                        <col class="col-xs-9">
                    </colgroup>
                    <tr>
                        <th>Author:</th><td><?= $book->getAuthor() ?></td>
                    </tr>
                    <tr>
                        <th>Published on:</th><td><?= $book->getPublishDate()->format('Y-m-d') ?></td>
                    </tr>
                    <tr>
                        <th>Language:</th><td><?= $book->getLanguage() ?></td>
                    </tr>
                    <tr>
                        <th>ISBN:</th><td><?= $book->getIsbn() ?></td>
                    </tr>
                </table>
            </div>
        </div>
    </div>
    <?php endforeach; ?>
</div>

Please note how I used $book->getPublishDate()->format('Y-m-d') to format \DateTime variable to display in view.

Fig1 shows the migrated application directory structure with Entity folder.

Migrated Application directory structure with Entity folder
Migrated Application module directory

And finally, the catalog index that has nothing to do with the data source other than displaying it. Therefore it is expected to be in exactly the same look comparing the TableGateway version.

New catalog index page look
Current look of the catalog index.

Published on May 29, 2017