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.
<?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 aContainerInterface
argument - named as$container
in the code above. Container interface basically retrieves service manager key values by using theget()
method. It also has a second method namedhas()
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:
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.
<?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:
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:
<?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.
<?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 withResultSet
prototype thatBookEntity
object injected during the instantiation in the mapper factory.
Finally the result:
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.
<?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:
<?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: