Zend Framework 2 comes packed with an assortment of new features and functionality and greatly streamlines how common scenarios, such as interacting with datasources and application configuration and caching are implemented.
Synopsis
Zend Framework 2 comes packed with an assortment of new features and functionality and greatly streamlines how common scenarios, such as interacting with datasources and application configuration and caching are implemented.
Whether it’s the new HTML5 form elements and view helpers, the new implementation of Zend\Http, the Service Manager, the Event Manager or that Modules are now first-class citizens - you can’t help but see that it’s bursting at the seams.
Update: Thanks to Jeff for pointing out the missing method in TableEntityMapper, which caused a fatal error. This has now been added.
But one set of features has really been helping me of late ones that really have me smiling; these are: Hydrators, Models and Table Gateways. If you’re new to ZF2 or database interaction with frameworks, then you’re in a perfect position as today’s post will be giving you a good introduction to the basics of using both together.
We’ll be working through sample code which will show you how to create models, which are decoupled from data source logic, yet via a simple ServiceManager configuration and a not too complex hydrator, will be able to extract information from a database query and auto-fill the model with it - ready to be used.
Why This Approach?
In the past, in Zend Framework 1, when you wanted to have a datasource agnostic model, it wasn’t always so simple to implement. Though there is still a lot of talk of PHP & MySQL; we all know that there is a veritable cornucopia of choice, including MongoDB, CouchDB, PostgreSQL, Cassandra, Redis and much, much, more.
Applications we write can start out with simple, modest needs. At first, maybe a basic RDBMS will suffice. But, as your needs change and grow, it’s nice to know that, without too much code refactoring, you can change around to match.
Today’s approach does this - allowing for a nearly transparent data source, which the model class knows nothing about. It ensures that irrespective of where the information comes from, it will be transformed so that the model class will be able to use it.
How Does It Work?
Simple analogy of Datasources, TableGateways, Hydrators, and Models in Zend Framework 2
In a nutshell here’s how it works. Firstly a TableGateway class performs the specific interaction with the datasource, such as fetching all records, deleting, adding and updating records. In this case it is a MySQL 5 database. The, the hydrator class maps (and transforms where applicable) the information retrieved from the TableGateway to the Model class.
In the getServiceConfig method in our Module class, the two are bound together and the Hydrator is configured. Finally, in a controller action, we can then access the datasource, querying records, auto-populating our Model and then iterate over the records. Sound good? Great!
Let’s get going!
The Table Gateway Class
Here we have the TableGateway class. I've called it UserTable as it is responsible for managing information in a user table. I've kept it simple and just focused on the retrieving of records skipping over the remainder of the CRUD operations.
In the constructor, we passing in a TableGateway object, which will be made available in the ServiceManager configuration. This provides us with simple access to the database. In the fetchAll function, we retrieve a result set by calling the select method on the TableGateway object.
The calls to buffer and next aren't strictly necessary - but have been included as, in the list action, a Paginator object is returned. Errors will be thrown if these two methods aren't called. Not much to it - right?
namespace MaltBlueCore\Model;
use Zend\Db\TableGateway\TableGateway;
class UserTable
{
protected $tableGateway;
public function __construct(TableGateway $tableGateway)
{
$this->tableGateway = $tableGateway;
}
public function fetchAll()
{
$resultSet = $this->tableGateway->select();
$resultSet->buffer();
$resultSet->next();
return $resultSet;
}
}
The Model Class
Now we have the User class. As you can see, this class has no code that relates to any kind of datasource or database. Here, we're only concerned about the data that we want to work with - in this case a:
-
user id
-
name
-
active flag
-
record creation date
The exchangeArray method is a common mention in ZF2 for passing in a dataset, such as an array in this case and auto-populating the object. You can see that I've set all the properties from the respective array key. Nice clean and simple.
namespace MaltBlueCore\Model;
class User
{
public $userId;
public $userName;
public $userActive;
public $createdDate;
public function exchangeArray($data)
{
if (isset($data['userName'])) {
$this->userName = $data['userName'];
} else {
$this->userName = null;
}
if (isset($data['userActive'])) {
$this->userActive = $data['userActive'];
} else {
$this->userActive = null;
}
if (isset($data['userId'])) {
$this->userId = $data['userId'];
} else {
$this->userId = null;
}
if (isset($data['createdDate'])) {
$this->createdDate = $data['createdDate'];
} else {
$this->createdDate = null;
}
}
public function getArrayCopy()
{
return get_object_vars($this);
}
}
The Hydrator
The Hydrator is the key aspect of the setup. It's what maps the database field names on one side, to the entity properties on the other. However, it doesn't store this information internally - as you'll see. I went for a more generic approach, one class which could be applied to any table.
What this does is allow for an array of Column Names -> Entity Properties to be passed in as an array and, when its hydrate method is called, will transfer the respective information from the datasource to the model object.
namespace MaltBlueCore\Hydrator;
use ReflectionMethod;
use Traversable;
use Zend\Stdlib\Exception;
use Zend\Stdlib\Hydrator\AbstractHydrator;
use Zend\Stdlib\Hydrator\HydratorOptionsInterface;
class TableEntityMapper
extends AbstractHydrator
implements HydratorOptionsInterface
{
protected $_dataMap = true;
public function __construct($map)
{
parent::__construct();
$this->_dataMap = $map;
}
public function setOptions($options)
{
return $this;
}
public function extract($object) {}
In the hydrate method, we pass in the datasource and the model. If the model is not an object, we throw a BadMethodCallException. If it is, we proceed and iterate over the data available.
If there is a mapping available, we use that to determine which field in the model matches with which field in the datasource. If it doesn't (if no mapping is required), we set the property directly. Any unknown or missing properties are silently skipped over.
public function hydrate(array $data, $object)
{
if (!is_object($object)) {
throw new Exception\BadMethodCallException(sprintf(
'%s expects the provided $object to be a PHP object)',
__METHOD__
));
}
foreach ($data as $property => $value) {
if (!property_exists($object, $property)) {
if (in_array($property, array_keys($this->_dataMap))) {
$_prop = $this->_dataMap[$property];
$object->$_prop = $value;
} else {
// unknown properties are skipped
}
} else {
$object->$property = $value;
}
}
return $object;
}
}
The Module Service Configuration
In the Module configuration, is where we tie everything together. I've been writing about the ServiceManager previously and I've listed some excellent links in the further reading section, which I encourage you to check out. Without this component, the rest really would not be possible.
Firstly we register an object in the factories list, which initialises the UserTable model we covered earlier, passing it the TableGateway object. Following that, we have the configuration for the TableGateway object. I have much to thank Evan Coury for in the approach that I've taken here.
What this does is to retrieve the application database adapter and initialise an instance of the hydrator that we previously covered, where we provide it with the mapping array; the table column names on the left and the model properties on the right.
We then provide the User model as the prototype to use with the HydratingResultSet object. If you're not familiar with it, the HydratingResultSet is a truly amazing part of Zend Framework 2.
I'll not try an invent the wheel and quote directly from the manual:
Zend\Db\ResultSet\HydratingResultSet is a more flexible ResultSet object that allows the developer to choose an appropriate “hydration strategy” for getting row data into a target object.
<p>
While iterating over results, HydratingResultSet will take a prototype of a target object and clone it once for each row. The HydratingResultSet will then hydrate that clone with the row data.
</p>
In essence, we provide the model, data and hydrator and it takes care of the rest. Following that, we return a new TableGateway object, specifying the underlying table, tbluser, that will be the source of the data, the database adapter and the resultset object we've just initialised.
Now, we have the two sides of the equation beautifully joined. Should the datasource change, the table name, the properties or column names, we only need make a slight adjustment here. No other classes or code need be touched.
public function getServiceConfig()
{
return array(
'factories' => array(
'MaltBlueAdmin\Model\UserTable' => function($sm) {
$tableGateway = $sm->get('UserTableGateway');
$table = new UserTable($tableGateway);
return $table;
},
'UserTableGateway' => function ($sm) {
$dbAdapter = $sm->get('Zend\Db\Adapter\Adapter');
$hydrator = new \MaltBlueCore\Hydrator\TableEntityMapper(
array(
'UserID' => 'userId',
'UserName' => 'userName',
'StatusID' => 'userActive',
'CreatedOn' => 'createdDate'
));
$rowObjectPrototype = new \MaltBlueCore\Model\User;
$resultSet = new \Zend\Db\ResultSet\HydratingResultSet(
$hydrator, $rowObjectPrototype
);
return new TableGateway(
'tbluser', $dbAdapter, null, $resultSet
);
}
)
);
}
The Controller
Ok, now we show how to use it. In our controller, we have a list action, which, as the name implies, will retrieve a list of records, which we will iterate over.
namespace MaltBlueManagementAdmin\Controller;
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use MaltBlueCore\Model\User;
use MaltBlueCore\Form\AddUserForm;
use MaltBlueCore\Form\EditUserForm;
class userController extends AbstractActionController
{
protected $userTable;
protected $_createUserForm;
public function listAction()
{
Firstly, we retrieve a copy of the userTable object through the Service Locator. We then call the fetchAll method, retrieving all rows from the table. I've used a filter iterator and a paginator to make for a more juicy example.
The filter iterator will only return records where the userActive field is set to "active". All others will be skipped. This iterator is then passed to the Zend\Paginator object, a few properties are set on it and is returned in the ViewModel, ready to be iterated over in the view script.
$sm = $this->getServiceLocator();
$userTable = new \MaltBlueCore\Model\UserTable(
$sm->get('userTableGateway')
);
$filterIterator = new StatusFilterIterator(
$userTable->fetchAll(), "active"
);
$paginator = new Paginator(new Iterator($filterIterator));
$paginator->setCurrentPageNumber(
$this->params()->fromRoute('page')
);
$paginator->setItemCountPerPage(
$this->params()->fromRoute('perPage', 10)
);
return new ViewModel(array(
'paginator' => $paginator,
'status' => $this->params()->fromRoute('status')
));
}
}
Conclusion
So, there we have it. With, only a little bit of code, we've created a model that is able to interact with a variety of data sources, yet avoid coupling to tightly at the same time. If we move from MySQL to PostgreSQL or Redis, then the requisite parts can be changed to suit.
Now, I'm not as experienced with hydrators, data mappers and the table gateway pattern as others. So I'd love to get your feedback. Tell me where this approach can be improved. Are there other aspects of ZF2 that can take it further - such as Strategies by Jurian Sluiman.
Join the discussion
comments powered by Disqus