Practical PHP Patterns: Active Record
Join the DZone community and get the full member experience.
Join For FreeThe Active Record pattern effectively prescribes to wrap a row of a database table in a domain object with a 1:1 relationship, managing its state and adding business logic in the wrapping class code.
An Active Record implementation is in fact a classical C structure aka Record aka associative array of data, with the addition of utility methods that encapsulate behavior that acts on these data. The most useful method is usually the save() one, which updates the database reflecting in the row the current state of the record. Thus, the Active Record transparently works with SQL queries and provides an higher-level Api.
Although Active Record is similar in implementation to the Row Data Gateway pattern, it is distinguished from it in the fact that it defines methods with domain-specific logic. The consequence of the presence of domain-specific logic is that generic implementations of Active Record provided by libraries must be customized to met the need of the object model. Typically this customization is done with a thin subclassing, which at least renames the library class with a domain name (like User or Post) and may specify metadata on the database table where the Active Records state is kept, if they are not inferred.
The issue of subclassing
Subclassing allows the developer to create new methods and properties to represent business logic, and to build a richer and more specific interface than the one constituted by simple Row Data Gateway objects. Despite these advantage, this interface is not much segregated, as subclassing exposes all the public methods of the base library class, on which the developers of the domain model have no control.
Subclassing also ties the domain layer to the infrastructure one, being it a library or a framework or every kind of data persistence layer (examples of PHP ORMs that use Active Record are the Zend_Db component, Doctrine 1 and Propel). Domain objects cannot be created or even their class source code loaded without having the library code available. This is an issue when reusing the model in a different environment, and even in test suites. If the library is powerful enough, it may provide adapters for different databases so that a lightweight database instance can be created in the testing environment.
Another caveat of Active Record is the fundamental assumption that a domain entity is always a row of a table of a relational database; this constraint is forced even when it is not appropriate, and the database and object model must match. In fact, part of the database (like foreign keys) often scatter into the domain model, as an Active Record with an external one-to-one relationship will usually store not only the related object but also its foreign key.
Another example of mirroring of the relational model into the object graph is for the management of M-to-N associating entities, often forced to become real entities even when they do not make sense (the famous UserGroup classes that tie together User and Group rows).
Diffusion
Thus, the Active Record pattern puts at risk the freedom of implementing a powerful Domain Model, where the object graph is a mix of state-carrying and behavior-carrying objects, like Strategies and Specifications. It is however, a radical simplification in implementation of domain models where CRUD functions are all the rage, and there is no gain in implementing objects that do not simply map to a relational database.
Note that in the case of PHP, most of the custom web applications developed in this language are deeply influenced by the back end, assumed as a relational database or even as MySQL. But while the technologies for user-to-application and application-to.application interaction on the web continue to grow, the situation will continue to evolve and if PHP wants to keep up with the pace of other dynamic languages, Java and .NET, it needs to finally decouple from the relational database as the unique model of data.
By the way, current implementation of persistence frameworks are transitioning towards a Data Mapper approach (not only in PHP but also in Ruby, while Java has done that years ago with Hibernate and JPA), which is less invasive on the Domain Model source code, and does not introduce an hard dependency from the domain layer to infrastructure components.
Examples
This sample implementation of the Active Record pattern is taken from the Doctrine 1.2 ORM. The base class in this framework id Doctrine_Record (together with Doctrine_Record_Abstract), while a base class with the schema metadata is regenerated from a model, and it is subclassed for orthogonality of customization and synchronization by the developer.
The base Active Record class looks like this:
/**If we have an Article entity, it will be represented via subclassing of the generic Active Record. In Doctrine 1, a subclass can be generated by writing a compact Yaml model, or even reverse engineered from an existing database:
* Implements also __get() and __set(), not shown along with many other dozen methods.
*/
abstract class Doctrine_Record extends Doctrine_Record_Abstract implements Countable, IteratorAggregate, Serializable
{
/**
* Empty template method to provide concrete Record classes with the possibility
* to hook into the saving procedure.
*/
public function preSave($event)
{ }
/**
* Empty template method to provide concrete Record classes with the possibility
* to hook into the saving procedure.
*/
public function postSave($event)
{ }
/**
* applies the changes made to this object into database
* this method is smart enough to know if any changes are made
* and whether to use INSERT or UPDATE statement
*
* this method also saves the related components
*
* @param Doctrine_Connection $conn optional connection parameter
* @return void
*/
public function save(Doctrine_Connection $conn = null)
{
if ($conn === null) {
$conn = $this->_table->getConnection();
}
$conn->unitOfWork->saveGraph($this);
}
/**
* returns a string representation of this object
*/
public function __toString()
{
return (string) $this->_oid;
}
}
/**To support further regenerations of the subclass as the schema evolves, another subclassing step is necessary. This class will never be touched by the regeneration process and it is the one referred in client code.
* BaseOtk_Content_Article
*
* This class has been auto-generated by the Doctrine ORM Framework
*
* @property integer $id
* @property integer $section_id
* @property integer $author_id
* @property integer $image_id
* @property string $title
* @property string $description
* @property string $text
* @property integer $visits
* @property boolean $draft
* @property boolean $closed
* @property Otk_Content_Section $section
* @property Otk_User $author
* @property Otk_File $image
* @property Doctrine_Collection $sections
* @property Doctrine_Collection $Otk_Content_Tag
* @property Doctrine_Collection $comments
*
*/
abstract class BaseOtk_Content_Article extends Otk_Model_Record
{
public function setTableDefinition()
{
$this->setTableName('oss_content_articles');
$this->hasColumn('id', 'integer', 3, array('type' => 'integer', 'primary' => true, 'autoincrement' => true, 'length' => '3'));
$this->hasColumn('section_id', 'integer', 2, array('type' => 'integer', 'notnull' => true, 'length' => '2'));
$this->hasColumn('author_id', 'integer', 3, array('type' => 'integer', 'length' => '3'));
$this->hasColumn('image_id', 'integer', 3, array('type' => 'integer', 'length' => '3'));
$this->hasColumn('title', 'string', 255, array('type' => 'string', 'length' => '255'));
$this->hasColumn('description', 'string', 1000, array('type' => 'string', 'length' => '1000'));
$this->hasColumn('text', 'string', null, array('type' => 'string'));
$this->hasColumn('visits', 'integer', 3, array('type' => 'integer', 'length' => '3'));
$this->hasColumn('draft', 'boolean', null, array('type' => 'boolean', 'notnull' => true, 'default' => false));
$this->hasColumn('closed', 'boolean', null, array('type' => 'boolean'));
}
public function setUp()
{
$this->hasOne('Otk_Content_Section as section', array('local' => 'section_id',
'foreign' => 'id'));
$this->hasOne('Otk_User as author', array('local' => 'author_id',
'foreign' => 'id'));
$this->hasOne('Otk_File as image', array('local' => 'image_id',
'foreign' => 'id'));
$this->hasMany('Otk_Content_Section as sections', array('refClass' => 'Otk_Content_Tag',
'local' => 'article_id',
'foreign' => 'section_id'));
$this->hasMany('Otk_Content_Tag', array('local' => 'id',
'foreign' => 'article_id'));
$this->hasMany('Otk_Content_Comment as comments', array('local' => 'id',
'foreign' => 'article_id'));
$timestampable0 = new Doctrine_Template_Timestampable();
$sluggable0 = new Doctrine_Template_Sluggable(array('fields' => array(0 => 'title'), 'canUpdate' => true, 'unique' => true));
$this->actAs($timestampable0);
$this->actAs($sluggable0);
}
}
/**
* This class defines the domain logic via addition of methods.
*/
class Otk_Content_Article extends BaseOtk_Content_Article
{
public function getTags()
{
$tags = array();
foreach ($this->sections as $section) {
$tags[$section->slug] = $section->name;
}
return $tags;
}
}
Opinions expressed by DZone contributors are their own.
Comments