Apollo Clark
Apollo Clark

Reputation: 806

Zend Framework Model, Active Record pattern alternative

I'm writing some code that allows users to read reports on a site, using AJAX calls to dynamically load only what is requested, instead of the entire 15+MB report.

I'm writing a Model to access all the report data from the database, and I don't want to use the Active Record pattern. I'm following the idea of "A Model HAS a table, instead of IS-A table", since this model will be accessing 5 different tables, and there are some complex MySQL JOIN's between these tables.

What is a good design pattern to follow in Zend Framework for this, examples?


UPDATED on 2012-12-05 @ 12:14PM EST

I'm currently working for a Market Research Report company. Without using actual function names, or revealing any meaningful details of the code, here are the basics:

code diagram

readreportAction() does:

readsectionAction() does:

reportpdfAction() does the exact same thing as readreportAction() and readsectionAction(), except all at one time. I'm trying to conceptualize a way to NOT copy + paste this code / programming logic. A data mapper seems to solve this.

Upvotes: 2

Views: 2435

Answers (3)

RockyFord
RockyFord

Reputation: 8519

First it looks like you need to make a little bit more of a conceptual leap. With the data mapper pattern it helps to think in terms of objects instead of database tables. I found these two articles helpful when I needed to make the leap.

http://phpmaster.com/building-a-domain-model/
http://phpmaster.com/integrating-the-data-mappers/

That being said ZF 1 has some very useful tools for building a data mapper/domain model.

The convention in ZF 1 is for each table you are working with to be accessible through the Zend_Db_Table api. The simplest way I've found is to just use the DbTable resource for each table. You could also use the Zend_Db::factory or new Zend_Db_Table('tableName') or any other method that appeals to you.

This example is based on a mp3 song track.

//in effect this is the database adapter for database table 'track', This is $tableGateway used later.
<?php
class Application_Model_DbTable_Track extends Zend_Db_Table_Abstract
{
    //name of database table, required to be set if name of class does not match name of table
    protected $_name = 'track';
    //optional, column name of primary key
    protected $_primary = 'id';

}

there are several ways to attach a table to the Db adapter and the Zend_Db_Table api, I just find this method simple to implement and it makes setting up a mapper simple as well.

The mapper class is the bridge between the data source and your object (domain entity). The mapper interacts with the api for Zend_Db_Table in this example.

A really important point to understand: when using classes that extend Zend_Db_Table_Abstract you have all the basic functionality of the Zend_Db component at your disposal. (find(),fetchall(), fetchRow(), select() ...)

<?php
class Music_Model_Mapper_Track extends Model_Mapper_Abstract
{

    //the mapper to access the songs artist object
    protected $artistMapper;
    //the mapper to access to songs album object
    protected $albumMapper;

    /**
     * accepts instance of Zend_Db_Table_Abstract
     *
     * @param Zend_Db_Table_Abstract $tableGateway
     */
    public function __construct(Zend_Db_Table_Abstract $tableGateway = null)
    {
        //at this point I tend to hardcode $tablegateway but I don't have to
        $tableGateway = new Application_Model_DbTable_Track();
        parent::__construct($tableGateway);
        //parent sets the $tablegateway variable and provides an abstract requirement
        //for createEntity(), which is the point of this class
    }
    /**
     * Creates concrete object of Music_Model_Track
     *
     * @param object $row
     * @return Music_Model_Track
     */
    public function createEntity($row)
    {
        $data = array(
            'id'           => $row->id,
            'filename'     => $row->filename,
            'format'       => $row->format,
            'genre'        => $row->genre,
            'hash'         => $row->hash,
            'path'         => $row->path,
            'playtime'     => $row->playtime,
            'title'        => $row->title,
            'track_number' => $row->track_number,
            'album'        => $row->album_id,//foriegn key
            'artist'       => $row->artist_id//foriegn key
        );
        //instantiate new entity object
        return new Music_Model_Track($data);
    }
    /**
     * findById() is proxy for find() method and returns
     * an entity object.
     *
     * @param type $id
     * @return object Model_Entity_Abstract
     */
    public function findById($id)
    {
        //instantiate the Zend_Db_Select object
        $select = $this->getGateway()->select();
        $select->where('id = ?', $id);
        //retrieve one database table row
        $row = $this->getGateway()->fetchRow($select);
        //create one entity object Music_Model_Track
        $entity = $this->createEntity($row);
        //return one entity object Music_Model_Track
        return $entity;
    }

  //truncated
}

All that has gone before is for the express purpose of building the following object:

<?php
class Music_Model_Track extends Model_Entity_Abstract
{
    /**
     * $id, __set, __get and toArray() are implemented in the parent
     */
    protected $album;
    protected $artist;
    protected $filename;
    protected $format;
    protected $genre;
    protected $hash;
    protected $path;
    protected $playtime;
    protected $title;
    protected $track_number;
    //artist and album mappers
    protected $albumMapper  = null;
    protected $artistMapper = null;


   //these are the important accessors/mutators because they convert a foreign key
   //in the database table to an entity object.
    public function getAlbum()
    {
        //if the album object is already set, use it.
        if(!is_null($this->album) && $this->album instanceof Music_Model_Album) {
            return $this->album;
        } else {
            //else we make a new album object
            if(!$this->albumMapper) {
                $this->albumMapper = new Music_Model_Mapper_Album();
            }
            //This is the album object we get from the id in our reference array.
            return $this->albumMapper->findById($this->getReferenceId('album'));
        }
    }
    //same as above only with the artist object.
    public function getArtist()
    {
        if(!is_null($this->artist) && $this->artist instanceof Music_Model_Artist) {
            return $this->artist;
        } else {
            if(!$this->artistMapper) {
                $this->artistMapper = new Music_Model_Mapper_Artist();
            }
            return $this->artistMapper->findById($this->getReferenceId('artist'));
        }
    }
    //the setters record the foriegn keys recorded in the table row to an array,
    //this allows the album and artist objects to be loaded only when needed.
    public function setAlbum($album)
    {
        $this->setReferenceId('album', $album);
        return $this;
    }

    public function setArtist($artist)
    {
        $this->setReferenceId('artist', $artist);
        return $this;
    }

  //standard setter and getters truncated...
}

so when using the track object you would get album or artist info like:

//this would be used in a controller most likely.
$mapper = new Music_Model_Mapper_Track();
$track = $mapper->findById('1');
//all of the information contained in the album or artist object is
//available to the track object.
//echo album title, year or artist. This album object also contains the artist object
//so the artist object would be available in two ways.
echo $track->album->title; //or
echo $track->artist->name;
echo $track->album->artist->name;
echo $track->getAlbum()->getArtist()->getName();

So what you really need to decide is how you want to structure your application. What I see as obvious may not be an option you wish to implement. A lot of the answers to your questions depend on exactly how these resources are to be used.

I hope this helps you at least a little bit.

Upvotes: 0

ficuscr
ficuscr

Reputation: 7054

I would recommend the Data Mapper pattern.

enter image description here

Everything you said makes sense and this pattern fits. Your model should not know or care how it is persisted. Instead the mapper does what it suggests - maps your model to your database. One of the things I like about this approach is it encourages people to think about the model in terms of an object, not a relational database table, as often happens with active record patterns and table row gateways.

Your object, unless very simple, typically will not reflect the structure of a database table. This lets you write good objects and then worry about the persistence aspects afterward. Sometimes more manual in that your mapper will need to deal with the complex joins, probably requiring writing some code or SQL, but the end result is it does just what you want and nothing more. No magic or conventions required if you don't want to leverage them.

I've always though these articles do a good job of explaining some of the design patterns that can be used well in ZF: http://survivethedeepend.com/zendframeworkbook/en/1.0/implementing.the.domain.model.entries.and.authors#zfbook.implementing.the.domain.model.entries.and.authors.exploring.the.entry.data.mapper

UPDATE:

Well you mapper might extend from an interface similar to this:

<?php
interface Mapper_Interface
{

    /**
     * Sets the name of the entity object used by the mapper.    
     */
    public function setObjectClass($class);

    /**
     * Sets the name of the list class used by the mapper.
     */
    public function setObjectListClass($listClass);

    /**
     * Get the name of the object class used by the mapper.
     * 
     */
    public function getObjectClass();

    /**
     * Get the name of the object list class used by the mapper.
     * 
     * @return string
     */
    public function getObjectListClass();

    /**
     * Fetch one row.
     *   
     * @param array $where Criteria for the selection.
     * @param array [$order = array()] Optionally the order of results
     * @return Object_Abstract
     * @throws Mapper_Exception
     */
    public function fetchRow($where, $order = array());

    /**
     * Fetch all records.  If there is no underlying change in the persisted data this    should
     * return a consistant result.
     * 
     * @param string|array|Zend_Db_Table_Select $where  OPTIONAL An SQL WHERE clause or Zend_Db_Table_Select object.
     * @param string|array                    $order  OPTIONAL An SQL ORDER clause.
     * @param int                              $count  OPTIONAL An SQL LIMIT count.
     * @param int                              $offset OPTIONAL An SQL LIMIT offset.
     * @return Object_List_Abstract 
     * @throws Mapper_Exception
     */
    public function fetchAll($where = null, $order = null, $count = null, $offset = null);

    /**
     * Deletes one or more object.
     * 
     * @param array|string $where Criteria for row deletion.
     * @return integer $affectedRows  
     * @throws Mapper_Exception
     */
    public function delete($where);

    /**
     * Saves a record. Either updates or inserts, as required.
     *   
     * @param $object Object_Abstract
     * @return integer $lastInsertId
     * @throws Mapper_Exception
     */
    public function save($object);
}

And you would interact with the mapper like:

$fooObjectMapper = new Foo_Mapper;
$fooObjectList = $fooObjectMapper->fetchAll();
var_dump($fooObjectList->first());

or

$fooObjectMapper = new Foo_Mapper;
$fooObject = $fooObject->fetch(array('id = ?' => 1));
$fooObject->setActive(false);
$fooObjectMapper->save($fooObject);

I usually write a mapper abstract for any 'PDO' enabled databases. One of the attributes of that concrete mapper is then the Zend_Db_Adapter to issue commands against. Makes for a flexible solution, easy to use mock data sources in testing.

Upvotes: 3

Jani Hartikainen
Jani Hartikainen

Reputation: 43243

You could consider using Doctrine 2. It's an ORM that does not use the ActiveRecord pattern.

In Doctrine, your models (entities) are all just normal PHP objects with zero knowledge of the database. You use mapping (xml, yaml or annotations) to tell Doctrine how they appear in the database, and the Entity Manager and repositories are used as a gateway for persisting entities or doing other database actions.

Upvotes: 0

Related Questions