Chapter 9. Implementing The Domain Model: Entries and Authors

9.1. Introduction

Back in Chapter 3: The Model I discussed some of the concepts surrounding the Model, a representation of the state and business rules of an entity, such as those governing entries in a blog. We're lucky in this chapter because our entities are extremely simple and with that simplicity we can ignore a lot of abstract ideas concerning the Model that are more relevant to web applications dealing with far more complex systems. However, even in our current state of simplicity we meet a number of design challenges to get us started.

Let me start by acknowledging that this chapter does not teach Zend_Db, Zend_Db_Table or Zend_Db_Table_Row in any great depth. Uses of these classes will be explained throughout the book as they are encountered, however the focus of this chapter is using Zend_Db_Table as the basis of designing and implementing our Model for this blog. Specifically I look at one area of the Model - entries.

9.2. The Domain Model and Database Access Patterns

When we discuss a Model there are some standard terms we can apply. Any Model must first of all belong to a domain, basically the overall system in which the Model operates. In our current application this is simply "blogging". Within this domain, the Model is composed of one or more domain objects. A domain object is a representation of an entity, its properties, and the business rules (also known as the domain logic) applied to it. So, for our blogging domain, we may have domain objects representing Entry entities. For the sake of completeness, an entity can be defined as a uniquely identifiable member of the domain with a set of behaviours. All entries, for example, should have a unique title and content but the main unique property will be its id. All entries also have a set of procedural behaviours - they are written, validated, and published.

If you check the explanation, you'll notice that nowhere is it stated that a Model is a singular object. In a planetary climate model we would have thousands of interacting entities, factors, behaviours, constraints, etc. So when we speak of a Model, we're actually referring to all of the entities contained by that Model, within its domain, and how they behave and interact with each other. You may find a lot of Zend Framework terminology that refers to entities as Models, e.g. an Entry Model, an Author Model, etc. In nearly all cases these are domain objects within a single domain model.

We mentioned domain logic which is a more general term referring to business rules and behaviour - not all applications are business related. Often our domain logic will describe constraints on the properties of domain objects. i.e. rules for validation and filtering. I mention this to emphasise that validation is a Model concern, and not a concern for either the Controller or View. You'll see where this comes into play in a future chapter when we talk about forms.

The main problem developers may face when designing the Model layer of their application is exactly how close the Model's storage layer is to the surface.

In very simple applications, we may directly run SQL queries using Zend_Db or create domain objects which, for the sake of expediency, extend Zend_Db_Table (which implements Martin Fowler's Table Data Gateway pattern from his book, "Patterns Of Enterprise Application Architecture" (or POEAA)). These bring the storage mechanism, a relational database, to the surface as something our application can access directly. The same can be said of Zend_Db_Table_Row, which implements Fowler's Row Data Gateway pattern, or Ruby On Rails' ActiveRecord, which implements (with some improvements which move it a bit closer to a Data Mapper) Fowler's Active Record pattern. All three of these patterns have something in common, they each encapsulate database access directly, often by inheriting from a base class bound to a single database table or row.

However, in many non-simple applications the domain model can grow in complexity. We may find that the domain model, which is expressed in terms of objects and object properties, cannot be easily mapped to database tables. A simple example of this is a blog entry. While the entry seems like a simple entity which can be assigned an entries table in a database schema, it also contains a reference (e.g. a foreign key) for an author. From the perspective of our domain objects, this would mean the entry object contains an author object. Why? Because the Model is designed from an object oriented perspective and this is the most obvious natural design.

From a database schema perspective, authors would be stored on a different table. What this means, is that our entry object does not map exactly to one single table, but should be mapped to two tables. We can resolve this by perhaps using an SQL table join when retrieving data. Using a join, we would need to filter the author data into an author object or, alternatively, fall back on two separate SQL queries to match the two objects. Either way, we have a problem as our domain object inherits from a class bound to one database table but actually needs to query two.

This serves to demonstrate an inescapable fact of life in application development - objects only map to databases one for one in very simple domain models. Eventually any complex Model needs more complex logic to perform a mapping between the two, and this leads into one of the primary patterns used outside of simple scenarios, the Data Mapper. Guess who defined it by the way? Martin Fowler - and yes, you really should read his book if you can get a copy...

So where am I going with this? Well, if we leave database access at the surface (for example, extending from Zend_Db_Table which represents only one table) we will quickly find ourselves stuck in an inflexible design which is incapable of easily dealing with any mismatch between the object domain and the database table domain. For example, if our entry domain object extends Zend_Db_Table it is directly tied to an entries table. So where does the author come from? Authors cannot be retrieved by querying an entries table, so somehow we need to drag in additional code, or run direct SQL queries (back to joins or two separate queries), etc. All of which will make it clear that inheriting from a class bound to only one table isn't working.

The alternative is leaving our domain objects completely free of any database related methods, and instead hide all database access behind a Data Mapper which performs any object to table mapping, and which can be made aware of any number of tables (just throw more Mappers at it). Now our domain objects are decoupled from the database schema.

Let's take a look at Fowler's definition of a Data Mapper.

A layer of Mappers that moves data between objects and a database while keeping them independent of each other and the mapper itself.

The definition confirms our suspicions then, domain objects will not even be aware that a database exists when a Data Mapper is implemented.

If there is anything to take from this section it's the following. Extending database access classes like Zend_Db_Table is fine, for very simple domain models. It's easy to do, easy to understand and requires little code to accomplish. However, in more complex domain models where domain objects can contain other domain objects, the inheritance route will not work well. At this point the need for a better solution like a Data Mapper will become obvious and justify the need for a slightly more complex class design.

I've deliberately stuck to Fowler's patterns for a reason. The more that simple inheritance manifests itself as a problem, the more likely it is that developers will begin changing their designs in an attempt to remove the inflexibility. In the Zend Framework community this has often become a debate over whether Models (or rather domain objects) should have an is-a (via inheritance) or has-a (via composition) relationship to the Zend_Db classes. At other times there are departures into the realm of Data Containers and Gateways. All of these are talking or coding around an obvious concept - they are intermediary or near complete steps towards the Data Mapper solution. It's so obvious as a solution that many developers will create a complete Data Mapper without ever realising it has a formal name and a massive body of implementation knowledge in the literature!

9.3. Exploring The Domain Objects

With all these ideas to hand, let's actually get around to examining the requirements of an actual domain object to represent each of our blog entries. We know from the last section that, at a minimum, we need an Entry domain object. At this point we are still not concerned over database schemas and we have only a basic idea of what properties our entries need anyway, so we'll focus on the core needs only.

At this point it is also important to keep in mind the definition of a domain object. It represents a uniquely identifiable entity with a set of behaviours. The key word is "uniquely" since every domain object represents one single entity. If we wish to represent many similar entities we will need a class responsible for containing a collection of entries.

At a minimum, our domain object will expose the following properties:

  • id

  • title

  • content

  • published_date

  • author

These are not all the properties we'll need, but they are enough to get started with. In applying incremental development we'll worry about additional features only when they become a requirement.

We can describe these properties by setting out the constraints that should apply to these in order for our domain object to represent a validated entry. Although we won't immediately deal with validation in this chapter, we'll see these integrated a little later.

  • The id is a positive integer greater than zero uniquely identifying the current entry.

  • The title is a non-empty string containing plain text or XHTML representing a unique entry title.

  • Content is a XHTML 1.0 Strict or plain text formatted non-empty string.

  • Both title and content may only contain a subset of XHTML tags and attributes specified by a whitelist.

  • The author is a valid author domain object representing the author of the entry.

  • The published_date property is a date which can be interpreted using the ISO 8601 standard.

These constraints are how a domain object can tell whether or not its assigned properties make it a valid entry. As we note, our entry will reference an author. Since all of these entities are represented by domain objects, we create a similar profile of an author object. This object's properties may include:

  • id

  • username

  • fullname

  • email

  • url

9.4. Exploring The Entry Data Mapper

Where the Entry domain object deals with a single entry, its properties and its validity as an entry, the Data Mapper is concerned more with the persistence of these objects between requests. Its function is to create, read, update and delete (collectively referred to as CRUD operations) domain object data on the database through a database access layer, and of course mapping the properties on those domain objects to their correct tables and column names. The Mapper must also do this without exposing the database schema, the access method or even the mapping logic to the domain objects.

As the Mapper is responsible for retrieving an entities data from the database in order to return a domain object created from that data, it stands to reason it also hosts any utility methods concerning CRUD operations and their selection criteria, i.e. which may relate to the SQL WHERE part of queries or the column name part where only certain details are required for an entity. With all of this going on it becomes more clear that domain objects probably will not be making calls to the Data Mapper. Instead we'll use the Data Mapper within our application and pass it any domain objects to work on. This isn't necessarily the easiest design to use - for obvious reasons it means we need more code in our application's Controller layer since have double the number of objects by introducing Data Mappers.

One common method of alleviating this object count problem is to allow domain objects be aware of their Data Mappers, but ensuring that this awareness does not extend beyond the Data Mapper API. Remember that in applying object oriented programming we should always code to the interface, never the implementation. We won't take any shortcuts here though - our domain model for blogging is simple enough that getting any more complex about our solution really would be pressing past the boundary of how much time we should be spending developing this Data Mapper.

9.5. Assessing Implementation Tools

With our Model fleshed out in more detail, we can start to identify some of the Model functionality we can pass out to existing Zend Framework components.

9.5.1. Domain Objects

Implementing domain objects in simple forms requires only one thing. PHP5. All domain objects are just plain old PHP objects with nothing particularly special to note. It's disappointing, I know! The Model is supposed to be complex, impossible to understand, and require a PhD. Instead we've boiled it into a system of objects that are not particularly complicated in isolation.

9.5.2. Validation And Filtering Rules

Before our Model can be stored anywhere, it needs to ensure the data it's holding follows any constraints and rules we define, i.e. that it is valid and any values have been filtered as required. While we could start throwing in validators using Zend_Validate and filters using Zend_Filter, we already know that any of our generated forms for the Model will then duplicate these rules. Duplication is bad, so the logical step is to use Zend_Form instances as the basis for implementing this.

Using Zend_Form does not come without a question mark. Since it represents a form, it's obviously something we will use a lot in our View. Isn't this mixing Model and View together inappropriately? This is one way of looking at it. The other is that any instance of Zend_Form may perform a dual role as a presentational element and as a container for Model derived validation/filtering rules. This may not always be the case, and indeed in complex Models this may only work for a small part of the Model where forms correlate fully to the data held by a Model. Our blog is pretty simple though so there's little need to worry. Technically, an ideal theoretical solution would be a Form solution which maintains two independent parts: a data container with validators and filters (much closer to a domain object) and a set of renderers capable of transforming these containers into a View included form. Wishful thinking aside (even the theoretically perfect solutions in this area tend be as complex if not more so than Zend_Form), we have to go with the hand we're dealt and adapt it to our needs.

We will be looking at Zend_Form and how this is implemented in a subsequent chapter.

9.5.3. Database Access

Since this is a Zend Framework book, we will of course be using Zend_Db. To be exact, Zend_Db_Table_Abstract which implements the Table Data Gateway pattern from Martin Fowler's POEAA. This Gateway pattern can be defined as a class which provides access to a table on the database, allowing us to perform inserts, selects, updates, and deletes for any row or group of rows on that table. Fowler defines this pattern as follows:

An object that acts as a Gateway to a database table. One instance handles all the rows in the table.

Zend_Db also offers an implementation of the Row Data Gateway pattern via Zend_Db_Table_Row which is similar to the Table Data Gateway except it deals with an individual row on a database table. In either case, Zend_Db offers abstracted access via its public API which allows you to construct SQL queries by chaining together object methods and is used for either pattern implementation.

For the purposes of our Model, we will be creating Data Mappers which access the database via Zend_Db_Table (i.e. the Table Data Gateway option). This fits in with the purpose of a Data Mapper which can be used by many domain objects but remains independent of them all, i.e. it's not concerned with just one specific domain object but may offer specific concrete subclasses for each to handle any domain object specific mapping logic.

It is important to note that at no point will our domain objects or Mappers extend from Zend_Db_Table as suggested by the Reference Guide. This forces our Model into one implementation based on database access. It also forces Models to be aware of their storage backend, and encourages developers to freely mix database and non-database code everywhere. Overall it's poor object oriented design unless your objective really is to only use database abstraction. The result is that we emphasising the use of composition over inheritance, a fundamental best practice in object oriented programming. All of our domain objects will have a "has-a" or "has-many" relationship with other classes, except perhaps any abstract parents or interfaces which help ensure all domain objects at least share a similar approach in their API.

9.6. Implementation

As I warned you at the start of the book, outside of short articles on my blog I always develop code using Test Driven Design. Therefore all code below is presented with unit tests at each step. Look on the bright side, at least you have something to put in the /tests directory! To setup the initial testing framework, please take a look at Appendix C: Unit Testing and Test Driven Design (to be added shortly).

9.6.1. Adding Unit Tests For Execution

The tests for our Model will be stored at /tests/ZFExt/Model, for example a test for our entry domain object will exist at /tests/ZFExt/Model/EntryTest.php. This will require adding an AllTests.php file in the same directory containing:

  • <?php
  • if (!defined('PHPUnit_MAIN_METHOD')) {
  • define('PHPUnit_MAIN_METHOD', 'ZFExt_Model_AllTests::main');
  • }
  • require_once 'TestHelper.php';
  • require_once 'ZFExt/Model/EntryTest.php';
  • class ZFExt_Model_AllTests
  • {
  • public static function main()
  • {
  • PHPUnit_TextUI_TestRunner::run(self::suite());
  • }
  • public static function suite()
  • {
  • $suite = new PHPUnit_Framework_TestSuite('ZFSTDE Blog Suite: Models');
  • $suite->addTestSuite('ZFExt_Model_EntryTest');
  • return $suite;
  • }
  • }
  • if (PHPUnit_MAIN_METHOD == 'ZFExt_Model_AllTests::main') {
  • ZFExt_Model_AllTests::main();
  • }

As we implement our Model, you will need to add additional tests to this file in order for them to be executed. They can be added by following the pattern used for the ZFExt_Model_EntryTest suite. Since this is not the top level AllTests.php file, you should add this one to the root /tests/AllTests.php using:

  • <?php
  • if (!defined('PHPUnit_MAIN_METHOD')) {
  • define('PHPUnit_MAIN_METHOD', 'AllTests::main');
  • }
  • require_once 'TestHelper.php';
  • require_once 'ZFExt/Model/AllTests.php';
  • class AllTests
  • {
  • public static function main()
  • {
  • PHPUnit_TextUI_TestRunner::run(self::suite());
  • }
  • public static function suite()
  • {
  • $suite = new PHPUnit_Framework_TestSuite('ZFSTDE Blog Suite');
  • $suite->addTest(ZFExt_Model_AllTests::suite());
  • return $suite;
  • }
  • }
  • if (PHPUnit_MAIN_METHOD == 'AllTests::main') {
  • AllTests::main();
  • }

As described in the Appendix, tests are run by navigating to /tests/ZFExt/Model (or /tests to run every single test across the application) in a console and running:

phpunit AllTests.php

9.6.2. The Domain Objects

Since our Entry domain object is just an ordinary object, we can start if off as a simple data container. We should always namespace our classes (the old pre-5.3 type of namespacing) so we'll be using a ZFExt_Model namespace for any Model related classes (which also applies to the test files). For now, everything will be stored within the /library directory. Let's start with an initial test checking we can set properties on the domain objects, and instantiate an entry domain object with an array of data. This is the initial content of /tests/ZFExt/Model/EntryTest.php:

  • <?php
  • require_once 'ZFExt/Model/Entry.php';
  • class ZFExt_Model_EntryTest extends PHPUnit_Framework_TestCase
  • {
  • public function testSetsAllowedDomainObjectProperty()
  • {
  • $entry = new ZFExt_Model_Entry;
  • $entry->title = 'My Title';
  • $this->assertEquals('My Title', $entry->title);
  • }
  • public function testConstructorInjectionOfProperties()
  • {
  • $data = array(
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z',
  • 'author' => new ZFExt_Model_Author
  • );
  • $entry = new ZFExt_Model_Entry($data);
  • $expected = $data;
  • $expected['id'] = null;
  • $this->assertEquals($expected, $entry->toArray());
  • }
  • }

We can now implement this (the tests fail anyway without the class written!) at /library/ZFExt/Model/Entry.php:

  • <?php
  • class ZFExt_Model_Entry
  • {
  • protected $_data = array(
  • 'id' => null,
  • 'title' => '',
  • 'content' => '',
  • 'published_date' => '',
  • 'author' => null
  • );
  • public function __construct(array $data = null)
  • {
  • if (!is_null($data)) {
  • foreach ($data as $name => $value) {
  • $this->{$name} = $value;
  • }
  • }
  • }
  • public function toArray()
  • {
  • return $this->_data;
  • }
  • public function __set($name, $value)
  • {
  • $this->_data[$name] = $value;
  • }
  • public function __get($name)
  • {
  • if (array_key_exists($name, $this->_data)) {
  • return $this->_data[$name];
  • }
  • }
  • }

You may ask why we don't make all properties public. The benefit of using a protected array, and using PHP's magic methods (like __set()) to provide access, is that it creates a gateway through which access passes. Now we can use any magic methods to run checks on properties and throw Exceptions on any errors.

Our new object is pretty basic. Let's add the rest of the standard magic methods so we can check if properties in the protected array are set and unset them if need be. Only the new tests are shown below.

  • <?php
  • require_once 'ZFExt/Model/Entry.php';
  • class ZFExt_Model_EntryTest extends PHPUnit_Framework_TestCase
  • {
  • // ...
  • public function testReturnsIssetStatusOfProperties()
  • {
  • $entry = new ZFExt_Model_Entry;
  • $entry->title = 'My Title';
  • $this->assertTrue(isset($entry->title));
  • }
  • public function testCanUnsetAnyProperties()
  • {
  • $entry = new ZFExt_Model_Entry;
  • $entry->title = 'My Title';
  • unset($entry->title);
  • $this->assertFalse(isset($entry->title));
  • }
  • }
  • <?php
  • class ZFExt_Model_Entry
  • {
  • protected $_data = array(
  • 'id' => null,
  • 'title' => '',
  • 'content' => '',
  • 'published_date' => '',
  • 'author' => null
  • );
  • public function __construct(array $data = null)
  • {
  • if (!is_null($data)) {
  • foreach ($data as $name => $value) {
  • $this->{$name} = $value;
  • }
  • }
  • }
  • public function toArray()
  • {
  • return $this->_data;
  • }
  • public function __set($name, $value)
  • {
  • $this->_data[$name] = $value;
  • }
  • public function __get($name)
  • {
  • if (array_key_exists($name, $this->_data)) {
  • return $this->_data[$name];
  • }
  • }
  • public function __isset($name)
  • {
  • return isset($this->_data[$name]);
  • }
  • public function __unset($name)
  • {
  • if (isset($this->_data[$name])) {
  • unset($this->_data[$name]);
  • }
  • }
  • }

Our domain object is now better defined. At the moment it offers unrestricted access when setting properties but our domain objects only needs those defined as keys on the initial data array. We can remove the ability to set out of bounds properties, and throw an Exception when it occurs, by adding an additional check to the __set() method as follows.

  • <?php
  • require_once 'ZFExt/Model/Entry.php';
  • class ZFExt_Model_EntryTest extends PHPUnit_Framework_TestCase
  • {
  • // ...
  • public function testCannotSetNewPropertiesUnlessDefinedForDomainObject()
  • {
  • $entry = new ZFExt_Model_Entry;
  • try {
  • $entry->notdefined = 1;
  • $this->fail('Setting new property not defined in class should'
  • . ' have raised an Exception');
  • } catch (ZFExt_Model_Exception $e) {
  • }
  • }
  • }
  • <?php
  • class ZFExt_Model_Entry
  • {
  • protected $_data = array(
  • 'id' => null,
  • 'title' => '',
  • 'content' => '',
  • 'published_date' => '',
  • 'author' => null
  • );
  • public function __construct(array $data = null)
  • {
  • if (!is_null($data)) {
  • foreach ($data as $name => $value) {
  • $this->{$name} = $value;
  • }
  • }
  • }
  • public function toArray()
  • {
  • return $this->_data;
  • }
  • public function __set($name, $value)
  • {
  • if (!array_key_exists($name, $this->_data)) {
  • throw new ZFExt_Model_Exception('You cannot set new properties'
  • . 'on this object');
  • }
  • $this->_data[$name] = $value;
  • }
  • public function __get($name)
  • {
  • if (array_key_exists($name, $this->_data)) {
  • return $this->_data[$name];
  • }
  • }
  • public function __isset($name)
  • {
  • return isset($this->_data[$name]);
  • }
  • public function __unset($name)
  • {
  • if (isset($this->_data[$name])) {
  • unset($this->_data[$name]);
  • }
  • }
  • }

Next up, our entry domain object will contain an author object. Since any of these domain objects will likely duplicate code we just wrote into ZFExt_Model_Entry, we should refactor our class to inherit from a parent containing any potentially reusable methods. Here, we add a new parent class called ZFExt_Model_Entity to fill this role.

  • <?php
  • class ZFExt_Model_Entity
  • {
  • public function __construct(array $data = null)
  • {
  • if (!is_null($data)) {
  • foreach ($data as $name => $value) {
  • $this->{$name} = $value;
  • }
  • }
  • }
  • public function toArray()
  • {
  • return $this->_data;
  • }
  • public function __set($name, $value)
  • {
  • if (!array_key_exists($name, $this->_data)) {
  • throw new ZFExt_Model_Exception('You cannot set new properties'
  • . ' on this object');
  • }
  • $this->_data[$name] = $value;
  • }
  • public function __get($name)
  • {
  • if (array_key_exists($name, $this->_data)) {
  • return $this->_data[$name];
  • }
  • }
  • public function __isset($name)
  • {
  • return isset($this->_data[$name]);
  • }
  • public function __unset($name)
  • {
  • if (isset($this->_data[$name])) {
  • unset($this->_data[$name]);
  • }
  • }
  • }
  • <?php
  • class ZFExt_Model_Entry extends ZFExt_Model_Entity
  • {
  • protected $_data = array(
  • 'id' => null,
  • 'title' => '',
  • 'content' => '',
  • 'published_date' => '',
  • 'author' => null
  • );
  • }

Running our tests once more will confirm that this was a successful refactoring.

Now, let's add a similar class for authors. First, to accommodate the new test, edit your /tests/ZFExt/Model/AllTests.php file to include a new test located at /tests/ZFExt/Model/AuthorTest.php. The tests and the class which contains them will be very similar to that for the entry domain object. Here are the initial tests which reflect those for the entry object but with the properties for an author object.

  • <?php
  • class ZFExt_Model_AuthorTest extends PHPUnit_Framework_TestCase
  • {
  • public function testSetsAllowedDomainObjectProperty()
  • {
  • $author = new ZFExt_Model_Author;
  • $author->fullname = 'Joe';
  • $this->assertEquals('Joe', $author->fullname);
  • }
  • public function testConstructorInjectionOfProperties()
  • {
  • $data = array(
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • );
  • $author = new ZFExt_Model_Author($data);
  • $expected = $data;
  • $expected['id'] = null;
  • $this->assertEquals($expected, $author->toArray());
  • }
  • public function testReturnsIssetStatusOfProperties()
  • {
  • $author = new ZFExt_Model_Author;
  • $author->fullname = 'Joe Bloggs';
  • $this->assertTrue(isset($author->fullname));
  • }
  • public function testCanUnsetAnyProperties()
  • {
  • $author = new ZFExt_Model_Author;
  • $author->fullname = 'Joe Bloggs';
  • unset($author->fullname);
  • $this->assertFalse(isset($author->fullname));
  • }
  • public function testCannotSetNewPropertiesUnlessDefinedInClass()
  • {
  • $author = new ZFExt_Model_Author;
  • try {
  • $author->notdefinedinclass = 1;
  • $this->fail('Setting new property not defined in class should'
  • . ' have raised an Exception');
  • } catch (ZFExt_Model_Exception $e) {
  • }
  • }
  • }

Here's the implementation which passes all of these new tests.

  • <?php
  • class ZFExt_Model_Author extends ZFExt_Model_Entity
  • {
  • protected $_data = array(
  • 'id' => null,
  • 'username' => '',
  • 'fullname' => '',
  • 'email' => '',
  • 'url' => ''
  • );
  • }

As a final gesture to ensure our interface is bound to these domain objects, let's ensure that ZFExt_Model_Entry only accepts a ZFExt_Model_Author object when setting an author property. As usual, the test first, and then the code which makes that test pass.

  • <?php
  • class ZFExt_Model_EntryTest extends PHPUnit_Framework_TestCase
  • {
  • // ...
  • public function testThrowsExceptionIfAuthorNotAnAuthorEntityObject()
  • {
  • $entry = new ZFExt_Model_Entry;
  • try {
  • $entry->author = 1;
  • $this->fail('Setting author should have raised an Exception'
  • . ' since value was not an instance of ZFExt_Model_Author');
  • } catch (ZFExt_Model_Exception $e) {
  • }
  • }
  • }
  • <?php
  • class ZFExt_Model_Entry extends ZFExt_Model_Entity
  • {
  • protected $_data = array(
  • 'id' => null,
  • 'title' => '',
  • 'content' => '',
  • 'published_date' => '',
  • 'author' => null
  • );
  • public function __set($name, $value)
  • {
  • if ($name == 'author' && !$value instanceof ZFExt_Model_Author) {
  • throw new ZFExt_Model_Exception('Author can only be set using'
  • . ' an instance of ZFExt_Model_Author');
  • }
  • parent::__set($name, $value);
  • }
  • }

All we've done here, is to override the parent class' __set() method to add a new check making sure our object only accepts an author value that is an object of type ZFExt_Model_Author. Otherwise, we pass control back to the parent to set the property.

We're done for now! Let's turn our attention to our Data Mapper implementation so can save these objects to a database or retrieve them.

9.6.3. The Data Mappers

Our Data Mapper will utilise Zend_Db_Table in the background, so its function in this design is to carry out typical CRUD operations. Later, we will also see that it can carry methods with more specific uses such as conditional fetching operations. For the moment, let's concentrate on setting it up. In the first test we'll add, our Data Mapper will be instantiated, and in turn create a configured instance of Zend_Db_Table_Abstract with which to work. You will note that I am not using an actual database. Although it takes a chunk of code at the start, I am using a mock object (a type of test double) in place of a real Zend_Db_Table_Abstract object. This will allow me to control everything this object does, including return values and setting expectations on what methods should be called, with what arguments, etc. The main reason I do this is because actually on a real database offers nothing new - our tests do not need one. If we did use a database it would work fine also, but then we are testing Zend_Db_Table_Abstract in addition to everything else since it's actually called upon. The Zend Framework already has tests for this component.

As before, to add this test to your suite, add the file and class to /tests/ZFExt/Model/AllTests.php, the tests themselves are written to /tests/ZFExt/Model/EntryMapperTest.php.

  • <?php
  • class ZFExt_Model_EntryMapperTest extends PHPUnit_Framework_TestCase
  • {
  • protected $_tableGateway = null;
  • protected $_adapter = null;
  • protected $_rowset = null;
  • protected $_mapper = null;
  • public function setup()
  • {
  • $this->_tableGateway = $this->_getCleanMock(
  • 'Zend_Db_Table_Abstract'
  • );
  • $this->_adapter = $this->_getCleanMock(
  • 'Zend_Db_Adapter_Abstract'
  • );
  • $this->_rowset = $this->_getCleanMock(
  • 'Zend_Db_Table_Rowset_Abstract'
  • );
  • $this->_tableGateway->expects($this->any())->method('getAdapter')
  • ->will($this->returnValue($this->_adapter));
  • $this->_mapper = new ZFExt_Model_EntryMapper($this->_tableGateway);
  • }
  • protected function _getCleanMock($className) {
  • $class = new ReflectionClass($className);
  • $methods = $class->getMethods();
  • $stubMethods = array();
  • foreach ($methods as $method) {
  • if ($method->isPublic() || ($method->isProtected()
  • && $method->isAbstract())) {
  • $stubMethods[] = $method->getName();
  • }
  • }
  • $mocked = $this->getMock(
  • $className,
  • $stubMethods,
  • array(),
  • $className . '_EntryMapperTestMock_' . uniqid(),
  • false
  • );
  • return $mocked;
  • }
  • }

All Data Mapper tests will rely on mocking out Zend_Db_Table_Abstract - after all it's already tested by the Zend Framework team so it's pointless using an actual object connected to a database. Typically we won't pass in a real instance through the constructor when we use this in the application, we can instead rely on the constructor creating a suitable instance. This test skeleton is setup to create a fully mocked version of Zend_Db_Table_Abstract.

While its hard to pinpoint from the PHPUnit manual without delving into the code, the protected _getCleanMock() method I'm using creates a completely clean mock object with all methods resolved and mocked. It creates a unique name for the mock on every call ensuring the mocked class names do not conflict. The only step necessary at the moment is ensuring all Zend_Db_Table_Abstract mock objects return a mocked Adapter also. Typically the only reason for mocking the Adapter is because it has one commonly used method, quoteInto() for escaping values in an SQL expression or condition.

Here's our initial (as yet untested) implementation showing why testing the real instantiation is not worth the trouble - it's dead simple, and again, testing it is really only testing Zend_Db_Table_Abstract.

  • <?php
  • class ZFExt_Model_EntryMapper
  • {
  • protected $_tableGateway = null;
  • protected $_tableName = 'entries';
  • public function __construct(Zend_Db_Table_Abstract $tableGateway)
  • {
  • if (is_null($tableGateway)) {
  • $this->_tableGateway = new Zend_Db_Table($this->_tableName);
  • } else {
  • $this->_tableGateway = $tableGateway;
  • }
  • }
  • protected function _getGateway()
  • {
  • return $this->_tableGateway;
  • }
  • }

In the code above we setup an instance of Zend_Db_Table with which to access the database. Although this class is called Abstract, it actually contains no abstract methods. The only configuration needed for now is to tell this instance which database table to use. We don't need to pass in any database connection settings because we can set a default database adapter from our bootstrap later.

Now lets add some useful methods. We'll start with a method to save a new domain object. Since we have mocked Zend_Db_Table_Abstract we are not making any direct assertions for the moment. Our assertions are actually setup by setting expectations on any mock objects, checking that our Mapper calls the expected Zend_Db_Table_Abstract method insert() with the correct array of data.

  • <?php
  • class ZFExt_Model_EntryMapperTest extends PHPUnit_Framework_TestCase
  • {
  • // ...
  • public function testSavesNewEntryAndSetsEntryIdOnSave() {
  • $author = new ZFExt_Model_Author(array(
  • 'id' => 2,
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • $entry = new ZFExt_Model_Entry(array(
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z',
  • 'author' => $author
  • ));
  • // set mock expectation on calling Zend_Db_Table::insert()
  • $insertionData = array(
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z',
  • 'author_id' => 2
  • );
  • $this->_tableGateway->expects($this->once())
  • ->method('insert')
  • ->with($this->equalTo($insertionData))
  • ->will($this->returnValue(123));
  • $this->_mapper->save($entry);
  • $this->assertEquals(123, $entry->id);
  • }
  • // ...
  • }

Here's the implementation which passes this test.

  • <?php
  • class ZFExt_Model_EntryMapper
  • {
  • protected $_tableGateway = null;
  • protected $_tableName = 'entries';
  • public function __construct(Zend_Db_Table_Abstract $tableGateway)
  • {
  • if (is_null($tableGateway)) {
  • $this->_tableGateway = new Zend_Db_Table($this->_tableName);
  • } else {
  • $this->_tableGateway = $tableGateway;
  • }
  • }
  • protected function _getGateway()
  • {
  • return $this->_tableGateway;
  • }
  • public function save(ZFExt_Model_Entry $entry)
  • {
  • $data = array(
  • 'title' => $entry->title,
  • 'content' => $entry->content,
  • 'published_date' => $entry->published_date,
  • 'author_id' => $entry->author->id
  • );
  • $entry->id = $this->_getGateway()->insert($data);
  • }
  • }

Saving a new record to the database involves calling Zend_Db_Table_Abstract::insert() with an array of the column names and values to insert in the database table, "entries". The table name is set within the constructor of the Data Mapper. The id is omitted since it will be set from the return value of Zend_Db_Table_Abstract::insert().

As you can see, our Mapper most definitely knows about the database schema - it maps the author object's id property to a table field called author_id. The domain object is not aware that this database column exists. The rest of the author data is ignored since it is stored on a different table, and it's not new. Actually this is a not so subtle point, you cannot save an author in this manner since author objects can only be saved through a future author Data Mapper.

We may also want to update entries, and these should be easy to spot since they will already have an existing id value. Let's add a test for the updating behaviour and its implementation. Once again, we'll use Mock Object expectations instead of assertions for this one. You should note that mock expectations are checked, of course, so if any of the constraints on method call count and similar are broken by how our implementation works, the test will fail.

  • <?php
  • class ZFExt_Model_EntryMapperTest extends PHPUnit_Framework_TestCase
  • {
  • // ...
  • public function testUpdatesExistingEntry() {
  • $author = new ZFExt_Model_Author(array(
  • 'id' => 2,
  • 'name' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • $entry = new ZFExt_Model_Entry(array(
  • 'id' => 1,
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z',
  • 'author' => $author
  • ));
  • // set mock expectation on calling Zend_Db_Table::update()
  • $updateData = array(
  • 'id' => 1,
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z',
  • 'author_id' => 2
  • );
  • // quoteInto() is called to escape parameters from the adapter
  • $this->_adapter->expects($this->once())
  • ->method('quoteInto')
  • ->will($this->returnValue('id = 1'));
  • $this->_tableGateway->expects($this->once())
  • ->method('update')
  • ->with($this->equalTo($updateData), $this->equalTo('id = 1'));
  • $this->_mapper->save($entry);
  • }
  • // ...
  • }
  • <?php
  • class ZFExt_Model_EntryMapper
  • {
  • protected $_tableGateway = null;
  • protected $_tableName = 'entries';
  • public function __construct(Zend_Db_Table_Abstract $tableGateway)
  • {
  • if (is_null($tableGateway)) {
  • $this->_tableGateway = new Zend_Db_Table($this->_tableName);
  • } else {
  • $this->_tableGateway = $tableGateway;
  • }
  • }
  • protected function _getGateway()
  • {
  • return $this->_tableGateway;
  • }
  • public function save(ZFExt_Model_Entry $entry)
  • {
  • if (!$entry->id) {
  • $data = array(
  • 'title' => $entry->title,
  • 'content' => $entry->content,
  • 'published_date' => $entry->published_date,
  • 'author_id' => $entry->author->id
  • );
  • $entry->id = $this->getGateway()->insert($data);
  • } else {
  • $data = array(
  • 'id' => $entry->id,
  • 'title' => $entry->title,
  • 'content' => $entry->content,
  • 'published_date' => $entry->published_date,
  • 'author_id' => $entry->author->id
  • );
  • $where = $this->getGateway()->getAdapter()
  • ->quoteInto('entry_id = ?', $entry->id);
  • $this->getGateway()->update($data, $where);
  • }
  • }
  • }

Let's add one more method before we break off - we'll add others over the course of our blogging application. Besides saving and updating, at a bare minimum we will also need to delete and retrieve entries.

This poses at least one problem in that entries may contain authors. To allow the EntryMapper to retrieve an author object, we must first add a Data Mapper for authors. Here's the full set of tests and an implementation for the class ZFExt_Model_AuthorMapper (most of which is very similar to the tests we've written so far - and includes a few we'll explore for the Entry Data Mapper soon).

  • <?php
  • class ZFExt_Model_AuthorMapperTest extends PHPUnit_Framework_TestCase
  • {
  • protected $_tableGateway = null;
  • protected $_adapter = null;
  • protected $_rowset = null;
  • protected $_mapper = null;
  • public function setup()
  • {
  • $this->_tableGateway = $this->_getCleanMock(
  • 'Zend_Db_Table_Abstract'
  • );
  • $this->_adapter = $this->_getCleanMock(
  • 'Zend_Db_Adapter_Abstract'
  • );
  • $this->_rowset = $this->_getCleanMock(
  • 'Zend_Db_Table_Rowset_Abstract'
  • );
  • $this->_tableGateway->expects($this->any())->method('getAdapter')
  • ->will($this->returnValue($this->_adapter));
  • $this->_mapper = new ZFExt_Model_AuthorMapper($this->_tableGateway);
  • }
  • public function testCreatesSuitableTableDataGatewayObjectWhenInstantiated()
  • {
  • $mapper = new ZFExt_Model_EntryMapper($this->_tableGateway);
  • $this->assertTrue($mapper->getGateway()
  • instanceof Zend_Db_Table_Abstract);
  • }
  • public function testSavesNewAuthorAndSetsAuthorIdOnSave() {
  • $author = new ZFExt_Model_Author(array(
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • // set mock expectation on calling Zend_Db_Table::insert()
  • $insertionData = array(
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • );
  • $this->_tableGateway->expects($this->once())
  • ->method('insert')
  • ->with($this->equalTo($insertionData))
  • ->will($this->returnValue(123));
  • $this->_mapper->save($author);
  • $this->assertEquals(123, $author->id);
  • }
  • public function testUpdatesExistingAuthor() {
  • $author = new ZFExt_Model_Author(array(
  • 'id' => 2,
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • // set mock expectation on calling Zend_Db_Table::update()
  • $updateData = array(
  • 'id' => 2,
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • );
  • $this->_adapter->expects($this->once())
  • ->method('quoteInto')
  • ->will($this->returnValue('id = 2'));
  • $this->_tableGateway->expects($this->once())
  • ->method('update')
  • ->with($this->equalTo($updateData), $this->equalTo('id = 2'));
  • $this->_mapper->save($author);
  • }
  • public function testFindsRecordByIdAndReturnsDomainObject()
  • {
  • $author = new ZFExt_Model_Author(array(
  • 'id' => 1,
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • // expected rowset result for found entry
  • $dbData = new stdClass;
  • $dbData->id = 1;
  • $dbData->fullname = 'Joe Bloggs';
  • $dbData->username = 'joe_bloggs';
  • $dbData->email = 'joe@example.com';
  • $dbData->url = 'http://www.example.com';
  • // set mock expectation on calling Zend_Db_Table::find()
  • $this->_rowset->expects($this->once())
  • ->method('current')
  • ->will($this->returnValue($dbData));
  • $this->_tableGateway->expects($this->once())
  • ->method('find')
  • ->with($this->equalTo(1))
  • ->will($this->returnValue($this->_rowset));
  • $entryResult = $this->_mapper->find(1);
  • $this->assertEquals($author, $entryResult);
  • }
  • public function testDeletesAuthorUsingEntryId()
  • {
  • $this->_adapter->expects($this->once())
  • ->method('quoteInto')
  • ->with($this->equalTo('id = ?'), $this->equalTo(1))
  • ->will($this->returnValue('author_id = 1'));
  • $this->_tableGateway->expects($this->once())
  • ->method('delete')
  • ->with($this->equalTo('id = 1'));
  • $this->_mapper->delete(1);
  • }
  • public function testDeletesAuthorUsingEntryObject()
  • {
  • $author = new ZFExt_Model_Author(array(
  • 'id' => 1,
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • $this->_adapter->expects($this->once())
  • ->method('quoteInto')
  • ->with($this->equalTo('id = ?'), $this->equalTo(1))
  • ->will($this->returnValue('author_id = 1'));
  • $this->_tableGateway->expects($this->once())
  • ->method('delete')
  • ->with($this->equalTo('id = 1'));
  • $this->_mapper->delete($author);
  • }
  • protected function _getCleanMock($className) {
  • $class = new ReflectionClass($className);
  • $methods = $class->getMethods();
  • $stubMethods = array();
  • foreach ($methods as $method) {
  • if ($method->isPublic() || ($method->isProtected()
  • && $method->isAbstract())) {
  • $stubMethods[] = $method->getName();
  • }
  • }
  • $mocked = $this->getMock(
  • $className,
  • $stubMethods,
  • array(),
  • $className . '_AuthorMapperTestMock_' . uniqid(),
  • false
  • );
  • return $mocked;
  • }
  • }

The implementation of this class reflects that for the Entry Mapper, and adds an additional find() and delete() method.

  • <?php
  • class ZFExt_Model_AuthorMapper
  • {
  • protected $_tableGateway = null;
  • protected $_tableName = 'authors';
  • protected $_entityClass = 'ZFExt_Model_Author';
  • public function __construct(Zend_Db_Table_Abstract $tableGateway)
  • {
  • if (is_null($tableGateway)) {
  • $this->_tableGateway = new Zend_Db_Table($this->_tableName);
  • } else {
  • $this->_tableGateway = $tableGateway;
  • }
  • }
  • protected function _getGateway()
  • {
  • return $this->_tableGateway;
  • }
  • public function save(ZFExt_Model_Author $author)
  • {
  • if (!$author->id) {
  • $data = array(
  • 'fullname' => $author->fullname,
  • 'username' => $author->username,
  • 'email' => $author->email,
  • 'url' => $author->url
  • );
  • $author->id = $this->_getGateway()->insert($data);
  • } else {
  • $data = array(
  • 'id' => $author->id,
  • 'fullname' => $author->fullname,
  • 'username' => $author->username,
  • 'email' => $author->email,
  • 'url' => $author->url
  • );
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $author->id);
  • $this->_getGateway()->update($data, $where);
  • }
  • }
  • public function find($id)
  • {
  • $result = $this->_getGateway()->find($id)->current();
  • $author = new $this->_entityClass(array(
  • 'id' => $result->id,
  • 'fullname' => $result->fullname,
  • 'username' => $result->username,
  • 'email' => $result->email,
  • 'url' => $result->url
  • ));
  • return $author;
  • }
  • public function delete($author)
  • {
  • if ($author instanceof ZFExt_Model_Author) {
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $author->id);
  • } else {
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $author);
  • }
  • $this->_getGateway()->delete($where);
  • }
  • }

With this new Author Data Mapper in tow, we can use it withing our Entry Data Mapper to retrieve any author object to be included in the resulting entry object returned from a new find() method. We'll also add a similar delete() method.

Here are the tests for finding an entry using its id property, and deleting an entry by its id.

  • <?php
  • class ZFExt_Model_EntryMapperTest extends PHPUnit_Framework_TestCase
  • {
  • // ...
  • public function testFindsRecordByIdAndReturnsDomainObject()
  • {
  • $author = new ZFExt_Model_Author(array(
  • 'id' => 1,
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • $entry = new ZFExt_Model_Entry(array(
  • 'id' => 1,
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z',
  • 'author' => $author
  • ));
  • // expected rowset result for found entry
  • $dbData = new stdClass;
  • $dbData->id = 1;
  • $dbData->title = 'My Title';
  • $dbData->content = 'My Content';
  • $dbData->published_date = '2009-08-17T17:30:00Z';
  • $dbData->author_id = 1;
  • // set mock expectation on calling Zend_Db_Table::find()
  • $this->_rowset->expects($this->once())
  • ->method('current')
  • ->will($this->returnValue($dbData));
  • $this->_tableGateway->expects($this->once())
  • ->method('find')
  • ->with($this->equalTo(1))
  • ->will($this->returnValue($this->_rowset));
  • // mock the AuthorMapper - it has separate tests
  • $authorMapper = $this->_getCleanMock('ZFExt_Model_AuthorMapper');
  • $authorMapper->expects($this->once())
  • ->method('find')->with($this->equalTo(1))
  • ->will($this->returnValue($author));
  • $this->_mapper->setAuthorMapper($authorMapper);
  • $entryResult = $this->_mapper->find(1);
  • $this->assertEquals($entry, $entryResult);
  • }
  • public function testDeletesEntryUsingEntryId()
  • {
  • $this->_adapter->expects($this->once())
  • ->method('quoteInto')
  • ->with($this->equalTo('id = ?'), $this->equalTo(1))
  • ->will($this->returnValue('entry_id = 1'));
  • $this->_tableGateway->expects($this->once())
  • ->method('delete')
  • ->with($this->equalTo('id = 1'));
  • $this->_mapper->delete(1);
  • }
  • public function testDeletesEntryUsingEntryObject()
  • {
  • $author = new ZFExt_Model_Author(array(
  • 'id' => 2,
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • $entry = new ZFExt_Model_Entry(array(
  • 'id' => 1,
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z',
  • 'author' => $author
  • ));
  • $this->_adapter->expects($this->once())
  • ->method('quoteInto')
  • ->with($this->equalTo('id = ?'), $this->equalTo(1))
  • ->will($this->returnValue('entry_id = 1'));
  • $this->_tableGateway->expects($this->once())
  • ->method('delete')
  • ->with($this->equalTo('id = 1'));
  • $this->_mapper->delete($entry);
  • }
  • // ...
  • }

Here's our implementation with these two new methods. As the tests suggest, we can delete entries by either passing an integer id value or the domain object itself.

  • <?php
  • class ZFExt_Model_EntryMapper
  • {
  • protected $_tableGateway = null;
  • protected $_tableName = 'entries';
  • protected $_entityClass = 'ZFExt_Model_Entry';
  • protected $_authorMapperClass = 'ZFExt_Model_AuthorMapper';
  • protected $_authorMapper = null;
  • public function __construct(Zend_Db_Table_Abstract $tableGateway)
  • {
  • if (is_null($tableGateway)) {
  • $this->_tableGateway = new Zend_Db_Table($this->_tableName);
  • } else {
  • $this->_tableGateway = $tableGateway;
  • }
  • }
  • protected function _getGateway()
  • {
  • return $this->_tableGateway;
  • }
  • public function save(ZFExt_Model_Entry $entry)
  • {
  • if (!$entry->id) {
  • $data = array(
  • 'title' => $entry->title,
  • 'content' => $entry->content,
  • 'published_date' => $entry->published_date,
  • 'author_id' => $entry->author->id
  • );
  • $entry->id = $this->_getGateway()->insert($data);
  • } else {
  • $data = array(
  • 'id' => $entry->id,
  • 'title' => $entry->title,
  • 'content' => $entry->content,
  • 'published_date' => $entry->published_date,
  • 'author_id' => $entry->author->id
  • );
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $entry->id);
  • $this->_getGateway()->update($data, $where);
  • }
  • }
  • public function find($id)
  • {
  • $result = $this->_getGateway()->find($id)->current();
  • if (!$this->_authorMapper) {
  • $this->_authorMapper = new $this->_authorMapperClass;
  • }
  • $author = $this->_authorMapper->find($result->author_id);
  • $entry = new $this->_entityClass(array(
  • 'id' => $result->id,
  • 'title' => $result->title,
  • 'content' => $result->content,
  • 'published_date' => $result->published_date,
  • 'author' => $author
  • ));
  • return $entry;
  • }
  • public function delete($entry)
  • {
  • if ($entry instanceof ZFExt_Model_Entry) {
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $entry->id);
  • } else {
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $entry);
  • }
  • $this->_getGateway()->delete($where);
  • }
  • public function setAuthorMapper(ZFExt_Model_AuthorMapper $mapper)
  • {
  • $this->_authorMapper = $mapper;
  • }
  • }

And finally...we have something of a working Data Mapper implementation! Here's what the final test tally should look like from PHPUnit.

PHPUnit 3.3.17 by Sebastian Bergmann.

.......................

Time: 0 seconds

OK (23 tests, 51 assertions)

9.6.4. Lazy Loading Domain Objects

Our Data Mapper implementation in ZFExt_Model_EntryMapper takes the route of needing two SQL queries to create a full domain object, one for the entry itself, and another for the referenced author. There may be times when we really just don't need the author details and at these times the extra query is pointless. It would make more sense if we altered the Data Mapper to lazy load the author data on demand, potentially saving us trips to the database.

We've already seen how we can override the __set() method to validate a property being set and we can use the __get() method to achieve similar functionality by intercepting any attempt to access the author object in our entry domain object, and only then firing an request through ZFExt_Model_AuthorMapper to retrieve that object.

Since this obviously alters existing tested behaviour, we need to amend at least one test for the Entry Data Mapper. We also need some way of storing the author's id value in the entry domain object so we have something to lazy load, and finally we need to make sure the lazy loading actually works. Here are the new/revised tests for both the Entry Mapper and Entry domain object:

  • <?php
  • class ZFExt_Model_EntryMapperTest extends PHPUnit_Framework_TestCase
  • {
  • // ...
  • public function testFindsRecordByIdAndReturnsDomainObject()
  • {
  • $entry = new ZFExt_Model_Entry(array(
  • 'id' => 1,
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z'
  • ));
  • // expected rowset result for found entry
  • $dbData = new stdClass;
  • $dbData->id = 1;
  • $dbData->title = 'My Title';
  • $dbData->content = 'My Content';
  • $dbData->published_date = '2009-08-17T17:30:00Z';
  • $dbData->author_id = 1;
  • // set mock expectation on calling Zend_Db_Table::find()
  • $this->_rowset->expects($this->once())
  • ->method('current')
  • ->will($this->returnValue($dbData));
  • $this->_tableGateway->expects($this->once())
  • ->method('find')
  • ->with($this->equalTo(1))
  • ->will($this->returnValue($this->_rowset));
  • $entryResult = $this->_mapper->find(1);
  • $this->assertEquals('My Title', $entryResult->title);
  • }
  • public function testFoundRecordCausesAuthorReferenceIdToBeSetOnEntryObject()
  • {
  • $entry = new ZFExt_Model_Entry(array(
  • 'id' => 1,
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z'
  • ));
  • // expected rowset result for found entry
  • $dbData = new stdClass;
  • $dbData->id = 1;
  • $dbData->title = 'My Title';
  • $dbData->content = 'My Content';
  • $dbData->published_date = '2009-08-17T17:30:00Z';
  • $dbData->author_id = 5;
  • // set mock expectation on calling Zend_Db_Table::find()
  • $this->_rowset->expects($this->once())
  • ->method('current')
  • ->will($this->returnValue($dbData));
  • $this->_tableGateway->expects($this->once())
  • ->method('find')
  • ->with($this->equalTo(1))
  • ->will($this->returnValue($this->_rowset));
  • $entryResult = $this->_mapper->find(1);
  • $this->assertEquals(5, $entryResult->getReferenceId('author'));
  • }
  • // ...
  • }
  • <?php
  • class ZFExt_Model_EntryTest extends PHPUnit_Framework_TestCase
  • {
  • // ...
  • public function testAllowsAuthorIdToBeStoredAsAReference()
  • {
  • $entry = new ZFExt_Model_Entry;
  • $entry->setReferenceId('author', 5);
  • $this->assertEquals(5, $entry->getReferenceId('author'));
  • }
  • public function testLazyLoadingAuthorsRetrievesAuthorDomainObject()
  • {
  • $author = new ZFExt_Model_Author(array(
  • 'id' => 5,
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • $entry = new ZFExt_Model_Entry;
  • $entry->setReferenceId('author', 5);
  • $authorMapper = $this->_getCleanMock('ZFExt_Model_AuthorMapper');
  • $authorMapper->expects($this->once())
  • ->method('find')
  • ->with($this->equalTo(5))
  • ->will($this->returnValue($author));
  • $entry->setAuthorMapper($authorMapper);
  • $this->assertEquals('Joe Bloggs', $entry->author->fullname);
  • }
  • protected function _getCleanMock($className) {
  • $class = new ReflectionClass($className);
  • $methods = $class->getMethods();
  • $stubMethods = array();
  • foreach ($methods as $method) {
  • if ($method->isPublic() || ($method->isProtected()
  • && $method->isAbstract())) {
  • $stubMethods[] = $method->getName();
  • }
  • }
  • $mocked = $this->getMock(
  • $className,
  • $stubMethods,
  • array(),
  • $className . '_EntryTestMock_' . uniqid(),
  • false
  • );
  • return $mocked;
  • }
  • // ...
  • }

Our starting point for implementation, is amending the ZFExt_Model_Entry class to accept the reference ID for an author for later use. Since the lazy loading occurs within this object, we also need the transfer the original awareness of ZFExt_Model_AuthorMapper within ZFExt_Model_EntryMapper over to the domain object itself. Technically, references can occur in any domain object requiring them, so we can add this feature to the parent ZFExt_Model_Entity class. The ZFExt_Model_Entry can use these methods from the parent class to set and retrieve reference information.

  • <?php
  • class ZFExt_Model_Entity
  • {
  • protected $_references = array();
  • // ...
  • public function setReferenceId($name, $id)
  • {
  • $this->_references[$name] = $id;
  • }
  • public function getReferenceId($name)
  • {
  • if (isset($this->_references[$name])) {
  • return $this->_references[$name];
  • }
  • }
  • }
  • <?php
  • class ZFExt_Model_Entry extends ZFExt_Model_Entity
  • {
  • protected $_data = array(
  • 'id' => null,
  • 'title' => '',
  • 'content' => '',
  • 'published_date' => '',
  • 'author' => null
  • );
  • protected $_authorMapperClass = 'ZFExt_Model_AuthorMapper';
  • protected $_authorMapper = null;
  • public function __set($name, $value)
  • {
  • if ($name == 'author' && !$value instanceof ZFExt_Model_Author ) {
  • throw new ZFExt_Model_Exception('Author can only be set using'
  • . ' an instance of ZFExt_Model_Author');
  • }
  • parent::__set($name, $value);
  • }
  • public function __get($name)
  • {
  • if ($name == 'author' && $this->getReferenceId('author')
  • && !$this->_data['author'] instanceof ZFExt_Model_Author) {
  • if (!$this->_authorMapper) {
  • $this->_authorMapper = new $this->_authorMapperClass;
  • }
  • $this->_data['author'] = $this->_authorMapper
  • ->find($this->getReferenceId('author'));
  • }
  • return parent::__get($name);
  • }
  • public function setAuthorMapper(ZFExt_Model_AuthorMapper $mapper)
  • {
  • $this->_authorMapper = $mapper;
  • }
  • }

Notice the new __get() method. This intercepts any attempt to access the author property of the domain object. Unless the object already includes an author object, it will attempt to load one from the database, but only if a reference id (i.e. the author's id) has been set, for example when the entry was originally loaded. Otherwise it will still return null which it should in case this is a new object without any author set.

Here's the revised ZFExt_Model_EntryMapper class. The only change is to remove the automatic loading of author objects and instead set the value of author_id as a reference on the resulting entry object.

  • <?php
  • class ZFExt_Model_EntryMapper
  • {
  • protected $_tableGateway = null;
  • protected $_tableName = 'entries';
  • protected $_entityClass = 'ZFExt_Model_Entry';
  • public function __construct(Zend_Db_Table_Abstract $tableGateway)
  • {
  • if (is_null($tableGateway)) {
  • $this->_tableGateway = new Zend_Db_Table($this->_tableName);
  • } else {
  • $this->_tableGateway = $tableGateway;
  • }
  • }
  • protected function _getGateway()
  • {
  • return $this->_tableGateway;
  • }
  • public function save(ZFExt_Model_Entry $entry)
  • {
  • if (!$entry->id) {
  • $data = array(
  • 'title' => $entry->title,
  • 'content' => $entry->content,
  • 'published_date' => $entry->published_date,
  • 'author_id' => $entry->author->id
  • );
  • $entry->id = $this->_getGateway()->insert($data);
  • } else {
  • $data = array(
  • 'id' => $entry->id,
  • 'title' => $entry->title,
  • 'content' => $entry->content,
  • 'published_date' => $entry->published_date,
  • 'author_id' => $entry->author->id
  • );
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $entry->id);
  • $this->_getGateway()->update($data, $where);
  • }
  • }
  • public function find($id)
  • {
  • $result = $this->_getGateway()->find($id)->current();
  • $entry = new $this->_entityClass(array(
  • 'id' => $result->id,
  • 'title' => $result->title,
  • 'content' => $result->content,
  • 'published_date' => $result->published_date
  • ));
  • $entry->setReferenceId('author', $result->author_id);
  • return $entry;
  • }
  • public function delete($entry)
  • {
  • if ($entry instanceof ZFExt_Model_Entry) {
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $entry->id);
  • } else {
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $entry);
  • }
  • $this->_getGateway()->delete($where);
  • }
  • }

Et voilá! We have altered our Data Mapper to support lazy loading of objects where appropriate. Now, I admit that this is a technically a form of premature optimisation - we have no idea if this helps performance in any way because we have no way of measuring any improvement just yet. But, since I've done this before, I can assume it will help with performance. Database operations are expensive, often the most expensive operation.

9.6.5. Preventing Duplicate Entities With An Identity Map

Another improvement point, not completely performance related, is the use of an Identity Map. To explain what I mean by this, imagine a scenario where you have retrieved 20 entries. Each entry is retrieved from the our Entry Data Mapper, and leaves the author unset so it can be lazy loaded as we have just implemented. How are authors loaded? By using the Author Data Mapper to retrieve them from the database. With our implementation, this means every entry we load, may also load an author. This sounds reasonable until you look at the relationship between entries and author. Any author may write many entries, so many entries will share the exact same author. This means we can conceivably load the same author from the database many times. This is obviously a problem - our domain objects should be as unique as possible.

From the outside looking in, this has no serious side effect other than lots of unnecessary database calls. So what happens if we are changing the author entity? We have lots of them! Changing one will not change the others, so entries within the same process will be using out of date author information. This lack of synchronicity must be eliminated.

An obvious solution is to make each unique entity shareable. If we load an author in one entry, and another entry needs the same author, they can somehow locate the first entry's instance of ZFExt_Model_Author for use. The most common solution is known as the Identity Map pattern. Yes, it's another Martin Fowler defined design pattern... Here's what Fowler has to say.

Ensures that each object gets loaded only once by keeping every loaded object in a map. Looks up objects using the map when referring to them.

You the man, Martin! While its not that clear from the definition, the Identity Map is also a form of cache in one respect. Once a domain object with a unique id is created or retrieved for the first time, it's registered on the Identity Map so other domain objects can use that instance, if they try to retrieve one with the same id, without making additional database calls through the Data Mapper.

Since our Data Mapper are already handling the retrieval and creation of domain objects, it seems they are the most logical place to put the implementation. Of course, since this will be a general map - there's no implementation specific to a Mapper - it's best added to a common parent class to avoid code duplication. This makes me happy for another reason! It's the perfect excuse to make all Data Mapper share a common class type and push any code duplication from the two Data Mappers to their shared parent class.

While we're at it, we can shift an code duplication across our two Data Mappers into this parent class. But, new tests first!

  • <?php
  • class ZFExt_Model_EntryMapperTest extends PHPUnit_Framework_TestCase
  • {
  • // ...
  • public function testFindsRecordByIdAndReturnsMappedObjectIfExists()
  • {
  • $entry = new ZFExt_Model_Entry(array(
  • 'id' => 1,
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z'
  • ));
  • // expected rowset result for found entry
  • $dbData = new stdClass;
  • $dbData->id = 1;
  • $dbData->title = 'My Title';
  • $dbData->content = 'My Content';
  • $dbData->published_date = '2009-08-17T17:30:00Z';
  • $dbData->author_id = 1;
  • // set mock expectation on calling Zend_Db_Table::find()
  • $this->_rowset->expects($this->once())
  • ->method('current')
  • ->will($this->returnValue($dbData));
  • $this->_tableGateway->expects($this->once())
  • ->method('find')
  • ->with($this->equalTo(1))
  • ->will($this->returnValue($this->_rowset));
  • $mapper = new ZFExt_Model_EntryMapper($this->_tableGateway);
  • $result = $mapper->find(1);
  • $result2 = $mapper->find(1);
  • $this->assertSame($result, $result2);
  • }
  • public function testSavingNewEntryAddsItToIdentityMap() {
  • $author = new ZFExt_Model_Author(array(
  • 'id' => 2,
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • $entry = new ZFExt_Model_Entry(array(
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z',
  • 'author' => $author
  • ));
  • // set mock expectation on calling Zend_Db_Table::insert()
  • $insertionData = array(
  • 'title' => 'My Title',
  • 'content' => 'My Content',
  • 'published_date' => '2009-08-17T17:30:00Z',
  • 'author_id' => 2
  • );
  • $this->_tableGateway->expects($this->once())
  • ->method('insert')
  • ->with($this->equalTo($insertionData))
  • ->will($this->returnValue(123));
  • $mapper = new ZFExt_Model_EntryMapper($this->_tableGateway);
  • $mapper->save($entry);
  • $result = $mapper->find(123);
  • $this->assertSame($result, $entry);
  • }
  • // ...
  • }

The new test is just like the one we wrote testing the operation of the Data Mapper's find() method. The difference is that this time, we make a second call (without changing the mock object expectations that Zend_Db_Table_Abstract will only be used once) and check that the resulting objects are the same. PHPUnit will go so far as to the check the object ids so this does ensure both results reference the exact same object. We also instantiate a new Mapper object for each test rather than use the one stored in the test class $_mapper property. This prevents calls from other tests setting up a member of the Identity Map and creating a false positive result. Here's the additional test, this time for ZFExt_Model_AuthorMapper.

  • <?php
  • class ZFExt_Model_AuthorMapperTest extends PHPUnit_Framework_TestCase
  • {
  • // ...
  • public function testFindsRecordByIdAndReturnsMappedObjectIfExists()
  • {
  • $author = new ZFExt_Model_Author(array(
  • 'id' => 1,
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • // expected rowset result for found entry
  • $dbData = new stdClass;
  • $dbData->id = 1;
  • $dbData->fullname = 'Joe Bloggs';
  • $dbData->username = 'joe_bloggs';
  • $dbData->email = 'joe@example.com';
  • $dbData->url = 'http://www.example.com';;
  • // set mock expectation on calling Zend_Db_Table::find()
  • $this->_rowset->expects($this->once())
  • ->method('current')
  • ->will($this->returnValue($dbData));
  • $this->_tableGateway->expects($this->once())
  • ->method('find')
  • ->with($this->equalTo(1))
  • ->will($this->returnValue($this->_rowset));
  • $mapper = new ZFExt_Model_AuthorMapper($this->_tableGateway);
  • $result = $mapper->find(1);
  • $result2 = $mapper->find(1);
  • $this->assertSame($result, $result2);
  • }
  • public function testSavingNewAuthorAddsItToIdentityMap() {
  • $author = new ZFExt_Model_Author(array(
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • ));
  • // set mock expectation on calling Zend_Db_Table::insert()
  • $insertionData = array(
  • 'username' => 'joe_bloggs',
  • 'fullname' => 'Joe Bloggs',
  • 'email' => 'joe@example.com',
  • 'url' => 'http://www.example.com'
  • );
  • $this->_tableGateway->expects($this->once())
  • ->method('insert')
  • ->with($this->equalTo($insertionData))
  • ->will($this->returnValue(123));
  • $mapper = new ZFExt_Model_AuthorMapper($this->_tableGateway);
  • $mapper->save($author);
  • $result = $mapper->find(123);
  • $this->assertSame($result, $author);
  • }
  • // ...
  • }

Our implementation starts by adding the common parent class, ZFExt_Model_Mapper. Both ZFExt_Model_EntryMapper and ZFExt_Model_AuthorMapper will extend this class.

  • <?php
  • class ZFExt_Model_Mapper
  • {
  • protected $_tableGateway = null;
  • protected $_identityMap = array();
  • public function __construct(Zend_Db_Table_Abstract $tableGateway)
  • {
  • if (is_null($tableGateway)) {
  • $this->_tableGateway = new Zend_Db_Table($this->_tableName);
  • } else {
  • $this->_tableGateway = $tableGateway;
  • }
  • }
  • protected function _getGateway()
  • {
  • return $this->_tableGateway;
  • }
  • protected function _getIdentity($id)
  • {
  • if (array_key_exists($id, $this->_identityMap)) {
  • return $this->_identityMap[$id];
  • }
  • }
  • protected function _setIdentity($id, $entity)
  • {
  • $this->_identityMap[$id] = $entity;
  • }
  • }

All that now remains are making the necessary changes to both Data Mappers to set newly retrieved objects in the Identity Map and preferentially retrieve them from the Map instead of wasting time on another visit to the database. Note that the above methods: _getGateway and __construct() should be removed from the Data Mapper classes since they will inherit them from the new parent class.

  • <?php
  • class ZFExt_Model_EntryMapper extends ZFExt_Model_Mapper
  • {
  • // ...
  • public function save(ZFExt_Model_Entry $entry)
  • {
  • if (!$entry->id) {
  • $data = array(
  • 'title' => $entry->title,
  • 'content' => $entry->content,
  • 'published_date' => $entry->published_date,
  • 'author_id' => $entry->author->id
  • );
  • $entry->id = $this->_getGateway()->insert($data);
  • $this->_setIdentity($entry->id, $entry); // add new
  • } else {
  • $data = array(
  • 'id' => $entry->id,
  • 'title' => $entry->title,
  • 'content' => $entry->content,
  • 'published_date' => $entry->published_date,
  • 'author_id' => $entry->author->id
  • );
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $entry->id);
  • $this->_getGateway()->update($data, $where);
  • }
  • }
  • public function find($id)
  • {
  • if ($this->_getIdentity($id)) {
  • return $this->_getIdentity($id);
  • }
  • $result = $this->_getGateway()->find($id)->current();
  • $entry = new $this->_entityClass(array(
  • 'id' => $result->id,
  • 'title' => $result->title,
  • 'content' => $result->content,
  • 'published_date' => $result->published_date
  • ));
  • $entry->setReferenceId('author', $result->author_id);
  • $this->_setIdentity($id, $entry); // add retrieved
  • return $entry;
  • }
  • // ...
  • }
  • <?php
  • class ZFExt_Model_AuthorMapper extends ZFExt_Model_Mapper
  • {
  • // ...
  • public function save(ZFExt_Model_Author $author)
  • {
  • if (!$author->id) {
  • $data = array(
  • 'fullname' => $author->fullname,
  • 'username' => $author->username,
  • 'email' => $author->email,
  • 'url' => $author->url
  • );
  • $author->id = $this->_getGateway()->insert($data);
  • $this->_setIdentity($author->id, $author);
  • } else {
  • $data = array(
  • 'id' => $author->id,
  • 'fullname' => $author->fullname,
  • 'username' => $author->username,
  • 'email' => $author->email,
  • 'url' => $author->url
  • );
  • $where = $this->_getGateway()->getAdapter()
  • ->quoteInto('id = ?', $author->id);
  • $this->_getGateway()->update($data, $where);
  • }
  • }
  • public function find($id)
  • {
  • if ($this->_getIdentity($id)) {
  • return $this->_getIdentity($id);
  • }
  • $result = $this->_getGateway()->find($id)->current();
  • $author = new $this->_entityClass(array(
  • 'id' => $result->id,
  • 'fullname' => $result->fullname,
  • 'username' => $result->username,
  • 'email' => $result->email,
  • 'url' => $result->url
  • ));
  • $this->_setIdentity($id, $author);
  • return $author;
  • }
  • // ...
  • }

This is as far as I'll take our domain model implementation in this chapter. There are more methods that can be added, and other problems that could be resolved. We'll add to the foundation we've created here as we go through the application.

9.7. Conclusion

This has been the first chapter in the book to start delving into source code. As you can tell, the focus is less on teaching Zend_Db and its child classes (the Reference Guide does this very well) and more on how to utilise these database access classes when designing a Model. I've also introduced one of the books other focuses - using testing to drive all development. While code examples that are prepared in advance work well, I do hope the slightly longer route of developing code within chapters using unit tests assists in understanding why and how we are making design decisions.

I'm also hoping you have spotted a potential problem. Why are we building a Data Mapper from scratch?

I said in the book's introduction that I have few qualms about crying foul when it's needed, and this is a case in point. PHP offers libraries for this sort of thing. There are some great Data Mapper libraries out there, lots of ORM libraries, and even the ZF Incubator itself has a full Data Mapper solution in progress. We should use be using them unless there are specific reasons for not doing so. Zend_Db implements the Row Data Gateway and Table Data Gateway patterns but by itself it is time consuming to implement for anything other than a very simple application. In short, if you want to remain sane, save time on development, and by extension save money on projects, replace it with an external library (or wait for ZF's own Data Mapper solution) on anything more complex than a blog. I know this sounds harsh, and probably it's not what you expected to hear, but it needs saying while you're still in the shallow end of this pond.

Does this make Zend Framework somehow less than its alternatives like Symfony or Ruby On Rails? No! Symfony uses an external ORM library itself that just happens to be distributed with the framework - nothing stops you using similar (or the same - Doctrine is very good and I use it myself) with your applications. Ruby On Rails uses an ActiveRecord implementation bound to the database layer but that hasn't stopped the Ruby community developing solutions like merb's datamapper so objects are not as closely tied to the database schema. It will be interesting to see how this influences Rails 3.0 since merb remained agnostic to any one solution preferring a pluggable system. If you remember nothing else, always remember that a framework offers you lots of features but you should never be required to use all of them if a more suitable alternative library for that feature exists. In a future chapter I'll spend more time on one of these Zend_Db alternatives.

So what was the point of this chapter? Just because ready to rock solutions exist, it doesn't mean we can't understand them and implement them ourselves. A project may be too simple (you wouldn't use an ORM library in a simple transaction script), carry too much legacy code to replace with anything other than simple abstraction, you might simply be told what to use by someone higher up the food chain or you may just prefer doing it yourself out of some other concern. A good example here is when the backend storage is not a relational database - something becoming more common as document and object based alternatives materialise. For whatever reason, the chapter's intent is to show you how to implement a better solution than vanilla Zend_Db.

Powered by jQuery Powered by Zend Framework Powered by Nginx Http Server