As Ruby on Rails took off, so did the idea of the ActiveRecord design pattern. ActiveRecord allows you to define a class that models a single table in the database. This works well for small tables that don’t rely on other tables for writing and loading, however, the patterns doesn’t work as well with a table that depends on many other tables.
This series will start of examining a class to handle small tables, and then progress through several parts to handling validation, using a separate data recorder, and finally to handle relationships. The code in here will all use PHP5.2+ and will be available on our GitHub account and to download at the end of each article.
For example, in our eCommerce application, SpEEdy Cart, the table that stores global, editable configuration data is very straightforward.
CREATE TABLE `config` ( `config_id` smallint(3) NOT NULL AUTO_INCREMENT, `group` varchar(32) collate utf8_unicode_ci NOT NULL, `name` varchar(64) collate utf8_unicode_ci NOT NULL, `value` text collate utf8_unicode_ci NOT NULL, `required` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`config_id`) ) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
Each piece of configuration data is independent of any other piece and only modifies the execution of the software. There are no other tables that store configuration information, so this table is a prime candidate to have an ActiveRecord Model written for it.
The ActiveRecord pattern allows the programmer to create a new Config object, set the data, and write it to the database. This is done through the interface of the class itself, and the database writing is hidden from the user.
/* Load up configuration record ID #10, set some new data, and write it. */ $config = new Config(10); $config->setGroup('category') ->setName('category_depth') ->setValue(3) ->write(); /* Create an empty configuration object, load the data from an array, and then write a new record. */ $cfg = array('group' => 'category', 'name' => 'category_depth', 'value' => 3); $config = new Config(); $config->loadFromArray($cfg)->write();
The write() method knows if the object is loaded or not (if the primary key of the object is set). If its loaded, an UPDATE command is issued, otherwise, an INSERT command is sent.
Surprisingly, the Config class is empty.
class Config extends Model { }
All of the work takes place in the class Model. First, take the time to analyze the class, and then it will be broken up into individual pieces. The Doxygen comments in this class have been omitted to reduce the number of lines and to improve the readability.
abstract class Model { protected $_id = 0; protected $_pkey = NULL; protected $_model = array(); protected $_table = NULL; protected $_methodCache = array(); protected $_forceLoad = false; const TABLE_ROOT = 'artisan_'; public function __construct($id=0) { $class = strtolower(get_class($this)); $this->_table = self::TABLE_ROOT . $class; $this->_pkey = $class . '_id'; $this->load($id); } public function __destruct() { $this->_id = 0; $this->_model = array(); } public function __call($method, $argv) { $argc = count($argv); if ( 0 === $argc && true === isset($this->_methodCache[$method]) ) { return $this->_methodCache[$method]; } else { $k = substr($method, 3); $k = strtolower(substr($k, 0, 1)) . substr($k, 1); $k = preg_replace('/[A-Z]/', '_\\0', $k); $k = strtolower($k); if ( 0 === $argc ) { /* If the length is 0, assume this is a get() */ $v = $this->__get($k); $this->_methodCache[$method] = $v; return $v; } else { /* Else assume its a set with the first element of $argv. */ $this->__set($k, current($argv)); return $this; } } } public function __set($k, $v) { $this->_model[$k] = $v; return true; } public function __get($k) { if ( true === isset($this->_model[$k]) ) { return $this->_model[$k]; } return NULL; } public function enabled() { if ( false === isset($this->_model['status']) ) { return true; } if ( 1 == $this->_model['status'] ) { return true; } return false; } public function getModelAsArray() { return $this->_model; } public function getId() { return $this->_id; } public function getPkey() { return $this->_pkey; } public function getTable() { return $this->_table; } public function setPkey($pkey) { $this->_pkey = $pkey; return $this; } public function setTable($table) { $this->_table = $table; return $this; } public function loadFromArray($array) { $this->_model = $array; if ( true === isset($array[$this->_pkey]) ) { if ( true === $this->_forceLoad ) { $this->load($array[$this->_pkey]); } else { $this->_id = $array[$this->_pkey]; } } return clone $this; } public function write() { if ( $this->_id > 0 ) { $this->_update(); } else { $this->_insert(); } return $this->_id; } public function load($id) { $id = intval($id); if ( $id > 0 ) { $result = LN::getDb()->select() ->from($this->_table) ->where($this->_pkey . ' = ?', $id) ->query(); if ( 1 == $result->numRows() ) { $this->_model = $result->fetch(); $this->_id = $this->_model[$this->_pkey]; } return true; } return false; } protected function _insert() { LN::getDb()->insert() ->into($this->_table) ->values($this->_model) ->query(); if ( 1 == LN::getDb()->affectedRows() ) { $this->_id = LN::getDb()->insertId(); return true; } return false; } protected function _update() { LN::getDb()->update() ->table($this->_table) ->set($this->_model) ->where($this->_pkey . ' = ?', $this->_id) ->query(); if ( 1 == LN::getDb()->affectedRows() ) { return true; } return false; } }
All of the members of the class are protected so they can be accessed in extended classes. This class assumes the primary key (ID) of the table is an integer. However, load() could be updated to assume its any data type.
The member variable $_pkey stores the name of the primary key of the table. It is automatically created by taking the name of the class and appending “_id” after it. It can be overwritten via setPkey().
$_model stores the actual data itself. It’s simply a key/value array with each key corresponding to a field of the table. $_table is the name of the table being worked on, and is automatically determined based on lowercasing the name of the class. Thus, if the class was named Product_Description, the value of $_table would be product_description and the value of $_pkey would be product_description_id.
$_methodCache stores a list of get*() methods and their resulting values for quick lookups later in __call(), and finally $_forceLoad causes the loadFromArray() method to load the data freshly from the database, regardless of if the data already exists in the model array.
The constructor takes an optional argument for the ID of the object to load. After the default table and primary key are established, the data is loaded (if possible). The loading takes place in load() which attempts to load the data based on the primary key.
load() is public in the case that it needs to be called if the primary key or table are changed.
loadFromArray() takes an array of key/value pairs and loads it into the object. This is useful if a list of objects is needed from a single query. For example, one could select all configuration options, and set them to a list of Config objects with a single query.
$config_list = array(); $config = new Config(); $result_config = LN::getDb()->select()->from('config')->query(); while ( $cfg = $result_config->fetch() ) { $config_list[] = $config->loadFromArray($cfg); }
Because each $cfg contains the primary key of the Config object (`config_id`) the object will be fully loaded and the $config_list array will contain a list of Config objects. Note: loadFromArray() returns a clone of $this. This ensures each element of $config_list will have a new Zend refcount value and will be an entirely different object.
The other methods, excluding __call() are fairly evident in their purpose. __call() is where a lot of the work takes place, so understanding it fully is crucial.
__call() is a Magic Method in PHP, meaning it is present in classes and is called silently if defined. It takes two arguments, the name of the method called and the arguments passed to that method.
__call() is executed in the case that a method that isn’t written as a member of the class is called. If __call() doesn’t exist in the class and a method is called that doesn’t exist as well, the normal error handling routines will throw the appropriate warning.
As a result, any object can now easily handle the get*() and set*() methods that are suitable only for that object.
The full name of the method, without the () is the value of $method in __call(). First, the value is checked against the $_methodCache array. If its found, that value is returned immediately and no extra processing is required.
However, if the call is a set*() type, or a get*() type that hasn’t been cached, further processing is necessary.
Fortunately, “get” and “set” are both three characters, so they are stripped off. Next, the first letter of the string is lowercased. Thus, “setModuleName” becomes “moduleName”. With the power of a simple regular expression, uppercased characters have an underscore appended before them. “moduleName” becomes “module_Name”. Finally, the entire string is lowercased, becoming “module_name”. This is the key that will be used to get or set a property of the model.
If there are no arguments, the class assumes this is a get*() method, and if that key corresponds to a value in $_model, the data is returned. It is also at this point that the method and the data is cached for future lookups.
If there are 1 or more arguments, a set*() method is assumed, and all but the first arguments are discarded. The first argument is the new value of the property, and it is set. $this is returned for chainability.
This simple class is very powerful for handling database tables easily. As shown earlier, setting up a class to handle global configuration data is simple.
Keep in mind, the ActiveRecord class described defines a 1:1 relationship between a record and its representation in your code. It should not represent a list or multiple objects. A separate class, or an Iterator is required to handle that case.
It should be evident where successive parts of this article series will go. An issue to resolve are classes that need to load/modify multiple other objects. For example, a Product class may need to load a list of Prices, Images, and Attributes. Each of these are separate tables, and thus have a separate class to handle them, however, in a read-only system (such as the front end of a shopping cart), knowing about a Product_Price is fairly meaningless without the context of the Product. Thus, it is the responsibility of the Product object to manage the Product_Price object.
The next article will cover using a separate recorder adapter to save the data in a different location, and to abstract the saving of data away from the Model object itself. From there, relationships like the scenario described above will be closely examined.
Subscribe to Leftnode’s RSS feed to read more about this topic in the upcoming days!