Model Mapper Connected to Database Table - Zend\Db

Model Mapper Connected to Database Table - Zend\Db

Connecting model mapper to database table by using Zend\Db.

model-mapper-vs-database.png
Parent post: Online Book Catalog Application Tutorial

I will work with the actual database starting from this post of the series. Therefore I am going to include Zend\Db component into my application as the first step.


[smozgur@local OnlineBookCatalog]$ composer require zendframework/zend-db

I selected to inject Zend\Db into config\modules.config.php during the module installation above. Now I have it included in my application and ready to be used.

I initially followed the Zend Framework 2 Album Tutorial Application to learn how to use TableGateway pattern to use database as the data source of my model. Basically I create the model mapper class injecting TableGateway object and interact with the database in the mapper as the business logic requires - so far displaying all book records by using fetchAll() method.

I already created BookMapper in the previous post and all I need to inject TableGateway into its constructor to retrieve data from the database instead of using hard coded $books array variable.

Previously created BookMapper is an InvokableFactory without dependencies. I will now modify it to be a factory closure to inject TableGateway into it.

Here I am creating the factory by using closure for quick demonstration purposes only, as I first learned. However then I learned that I should use designated Factory Class instead of closure due to performance and also caching purposes as well as maintain better readability. Therefore I will update Module.php and create a dedicated factory class at the end of this post.
# /module/Application/src/Module.php

<?php
namespace Application;

use Zend\ModuleManager\Feature\ConfigProviderInterface;
use Zend\ModuleManager\Feature\ServiceProviderInterface;
use Zend\Db\TableGateway\TableGateway;
use Zend\Db\ResultSet\ResultSet;
use Zend\Db\Adapter\AdapterInterface;
use Interop\Container\ContainerInterface;
use Application\Model\Book\BookEntity;
use Application\Model\Book\BookMapper;

class Module implements ConfigProviderInterface, ServiceProviderInterface
{
    public function getConfig()
    {
        return include __DIR__ . '/../config/module.config.php';
    }
    
    public function getServiceConfig()
    {
        return [
            'factories' => [
                BookMapper::class => function(ContainerInterface $container, $requestedName) {
                    $dbAdapter = $container->get(AdapterInterface::class);
                    
                    /**
                     * Preparing TableGateway resultset parameter by setting
                     * BookEntity as the object prototype of the resultset prototype  
                     * 
                     * @var \Zend\Db\ResultSet\ResultSet $resultSetPrototype
                     */
                    $resultSetPrototype = new ResultSet();
                    $resultSetPrototype->setArrayObjectPrototype(new BookEntity());
                    
                    /**
                     * Setting table name as 'book' since this TableGateway is 
                     * responsible to return data from the 'book' table
                     * 
                     * @var \Zend\Db\TableGateway\TableGateway $tableGateway
                     */
                    $tableGateway = new TableGateway('book', $dbAdapter, null, $resultSetPrototype);
                    
                    /**
                     * Finally instantiating and returning BookMapper as tableGateway is injected
                     * so mapper can interact with the database within its methods 
                     * 
                     * @var \Application\Model\Book\BookMapper $mapper
                     */
                    $mapper = new BookMapper($tableGateway);
                    return $mapper;
                },
            ]
        ];
    }
}
Each factory always receive a ContainerInterface argument - named as $container in the code above. Container interface basically retrieves service manager key values by using the get() method. It also has a second method named has() to be used to validate the existence of the requested key.

Here is the failure when I try to open the page in the browser:

Service not created exception
Service is tried to be implemented but failed.

This exception simply says that there is an interface that I used in my code but it has not been implemented correctly.

If I take a closer look at the factory closure, it is trying to get AdapterInterface which is supposed to create the database adapter. I did nothing fancy about it but simply added to the closure. However, it requires a certain configuration key called db consisting of driver type and connection properties to be included in the service manager.

I am going to do that in the global.php file which is globally available in my application so it can be accessed by every class in the application.

# /config/autoload/global.php

<?php
return [
    'db' => [
        'driver'         => 'Pdo',
        'dsn'            => 'mysql:dbname=obd;host=localhost',
        'username'       => 'dbuser',
        'password'       => 'dbpwd',
    ],
];
All the right side assignments are related with the database server, database itself and privileged database user used by the application.

It is time to face with the next exception:

Service not created exception
Service is tried to be implemented but failed again.

This time it complains about BookMapper instantiation, specifally a required but missing method. Since I am using TableGateway, I am now responsible to implement exchangeArray() method in the model entity that TableGateway requires. This method simply copies the passed data array to the BookEntity properties.

BookEntity class that exchangeArray() method implemented:

# /module/Application/src/Model/Book/BookEntity.php

<?php
namespace Application\Model\Book;

class BookEntity
{
    
    // ... previously created properties, setters and getters
        
    /**
     * @param array $data
     */
    public function exchangeArray($data)
    {
        $this->id = (isset($data['id'])) ? $data['id'] : null;
        $this->alias = (isset($data['alias'])) ? $data['alias'] : null;
        $this->title = (isset($data['title'])) ? $data['title'] : null;
        $this->author = (isset($data['author'])) ? $data['author'] : null;
        $this->description = (isset($data['description'])) ? $data['description'] : null;
        $this->publish_date = (isset($data['publish_date'])) ? $data['publish_date'] : null;
        $this->isbn = (isset($data['isbn'])) ? $data['isbn'] : null;
        $this->language = (isset($data['language'])) ? $data['language'] : null;
    }
}

Since I am sure that it won't fail this time, I can go one more step and update BookMapper class to interact with the real database instead of returning a hard-coded array variable. I should receive the injected TableGateway object into the class in the constructor and use its select() method to return all records from the associated table in fetchAll() method.

# /module/Application/src/Model/Book/BookMapper.php

<?php
namespace Application\Model\Book;
 
use Zend\Db\TableGateway\TableGateway;

class BookMapper
{
    protected $tableGateway;
    
    /**
     * Constructor
     * @param TableGateway $tableGateway
     */
    public function __construct(TableGateway $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }
    
    /**
     * Fetch All for Book Model
     * 
     * @return array \Application\Model\Book\BookEntity[]
     */
    public function fetchAll()
    {
        return $this->tableGateway->select();
    }
}
select() method returns an array consist of book entity objects since we set up associated TableGateway to be prepared with ResultSet prototype that BookEntity object injected during the instantiation in the mapper factory.

Finally the result:

Book listing
Listed books are actually coming from the database now.

This is exactly the same result of the previous post. I didn't change a single line code in the controller or view template, yet - as promised - it is exactly the same result.

Ok, I need to make a change that I promised to complete this part. It will definitely not affect the final result, however will make my application better. I will quit using the closure that I created in Module.php and use factory class instead.

First, I will create BookMapperFactory class.

# /module/Application/src/Model/Book/Factory/BookMapperFactory.php

<?php
namespace Application\Model\Book\Factory;

use Zend\Db\TableGateway\TableGateway;
use Zend\Db\Adapter\AdapterInterface;
use Zend\Db\ResultSet\ResultSet;
use Interop\Container\ContainerInterface;

use Application\Model\Book\BookEntity;
use Application\Model\Book\BookMapper;

/**
 * BookMapperFactory
 * 
 * Mapper Factory for Book Model
 * r
 */
class BookMapperFactory
{
    /**
     * Handle invoke calls injecting mappers
     *
     * @return BookMapper
     */
    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $dbAdapter = $container->get(AdapterInterface::class);

        $resultSetPrototype = new ResultSet();
        $resultSetPrototype->setArrayObjectPrototype(new BookEntity());
        
        $tableGateway = new TableGateway('book', $dbAdapter, null, $resultSetPrototype);        
        
        return new BookMapper($tableGateway);
    }
}

And finally change Module.php to instantiate the mapper from this factory class instead previously written closure:

# /module/Application/src/Module.php

<?php
namespace Application;

use Zend\ModuleManager\Feature\ConfigProviderInterface;
use Zend\ModuleManager\Feature\ServiceProviderInterface;

use Application\Model\Book\BookMapper;

class Module implements ServiceProviderInterface, ConfigProviderInterface
{
    public function getConfig()
    {
        return include __DIR__ . '/../config/module.config.php';
    }
    
    public function getServiceConfig()
    {
        return [
            'factories' => [
                BookMapper::class => Model\Book\Factory\BookMapperFactory::class,
            ],
        ];
    }
}

Beautifully done!

Model retrieves data as I desire from where I want, Controller doesn't care how the data is retrieved but only get it from the model and pass it to the view, View on the other hand still loops through the passed array to list the book items as it was initially doing at the previous post of this series.

Fig4. shows the current application directory with the new elements:

Current application directory structure
Current aapplication directory structure.

Published on Dec 28, 2016