Introduction to DataModeler
DataModeler is an Open Source ORM I began writing at the beginning of the year. I had experience with ActiveRecord, but was dissuaded from using it all too often because it was difficult to unit test (at least I felt so). Wanting to learn Test Driven Development better, I decided to create my own small ORM framework: DataModeler. DataModeler allows you to create easily testable Models that are not dependent on any datasource. The majority of your logic should take place in the Model, and not the Controller (making your application even easier to test as data sources can be mocked).
I try to keep my code as simple as possible, so DataModeler is fairly small. The primary library is 1024LOC (v0.1.0 at the time of this writing), and the test suite is 2188LOC.
DataModeler makes extensive use of Dependency Injection. For example, the Sql class requires you to attach a valid PDO object before the class is useful.
DataModeler Classes
\DataModeler\Exception– A custom Exception class for nicer formatting.\DataModeler\Iterator– A custom Iterator class to allow filtering of data.\DataModeler\Model– An abstract class that defines a 1:1 relationship between a Model and datastore table.\DataModeler\Sql– The primary interface to a datastore. Requires a validPDOinstance.\DataModeler\SqlResult– The object created after executing a preparedSELECTquery. Models and Iterators are returned from this object.
Defining Models
The core piece of DataModeler is the \DataModeler\Model class. Each Model has a 1:1 relationship between an object and a datastore table. Currently, you can’t define relationships between Models, but that will change in the future.
The simplest Models are ones that extend the \DataModeler\Model class and define a few fields.
namespace myApp;
use \DataModeler\Model;
require_once 'DataModeler/Model.php';
class User extends Model {
protected $table = 'user';
protected $pkey = 'user_id';
public $user_id = 0;
public $created = NULL;
public $username = NULL;
public $password = NULL;
public $status = false;
}
The User class is pretty plain and not very sophisticated. Primarily, each of the fields don’t have a datatype. When saving a User object, the \DataModeler\Sql class will assume each of the fields are of type STRING and pass a PDO::PARAM_STR parameter when binding them to the query.
Fortunately, fixing this is simple. With the use of simple multiline doc-comments, you can define the datatypes and some other metadata about each field. As of this release, each field must be public.
class User extends Model {
protected $table = 'user';
protected $pkey = 'user_id';
/** [type INTEGER] */
public $user_id = 0;
/** [type DATETIME] */
public $created = self::SCHEMA_TYPE_DATETIME_VALUE;
/** [type STRING] [maxlength 64] */
public $username = NULL;
/** [type STRING] [maxlength 96] */
public $password = NULL;
/** [type BOOL] */
public $status = false;
}
Much better! Each field now has a type associated with it. Furthermore, two of the fields, $username and $password have a maxlength property to ensure they are never longer than the value set. Even more, the \DataModeler\Model class is Unicode aware. If the mb (multibyte) extension is installed, the mb_strlen() method will be used to determine the maxlength, otherwise, strlen() will be used. Using the mb extension can be disabled by calling $model->multibyte(false);.
Models also support two date formats: MySQL’s DATE and DATETIME fields. DATE is in the format YYYY-MM-DD and DATETIME is YYYY-MM-DD HH:MM:SS. By setting the value of the field to self::SCHEMA_TYPE_DATE_VALUE, the variable will have a default value of the current YYYY-MM-DD. Setting the value to self::SCHEMA_TYPE_DATETIME_VALUE does the same for the DATETIME format.
Floating point fields also support a precision.
/** [type FLOAT] [precision 3] */ public $percentage = 0.734;
Each time you set the percentage, it will be rounded to 3 decimal points.
Note: The doc comments can be split to several lines as well.
/** * [type STRING] * [maxlength 64] */ public $username = NULL;
Accessing Models
While the fields to a Model are public, it is not recommended you access them through the fields directly because the metadata information can not be applied. Instead, through the usage of a __call() method, each field can be accessed through a get*() and set*() method.
Through the __call() magic method, each field name is camelCased and can be accessed or modified. Field names with underscores are converted to camelCase.
Continuing with the User class above, each of the fields can be accessed through the following methods: getUserId(), getCreated(), getUsername(), getPassword(), and getStatus().
Each of the fields can modified through the following methods: setUserId(), setCreated(), setUsername(), setPassword(), and setStatus(). They each accept one argument: the modified value. When the __call() method determines a set*() method has been called, it returns $this for chaining the calls.
Calling a get*() method on a field that doesn’t exist returns a PHP NULL. Calling a set*() method on a field that doesn’t exist does nothing and returns $this.
$user = new User;
$user->setUsername('leftnode')
->setPassword('myPassword')
->setStatus(true);
The \DataModeler\Model class has a special method for altering the primary key field of a Model: id().
$user = new User; echo $user->id(); // 0 $user->id(10); // Set the user_id field to value 10 echo $user->id(); // 10
Models can have their data loaded in one fell swoop with the load() method when passed a key/value array where the keys align to the field names.
$modelData = array( 'user_id' => 10, 'username' => 'leftnode', 'password' => 'myPassword', 'status' => true ); $user = new User; $user->load($modelData);
Comparing Models
Model objects can easily be compared to each other through three methods: equalTo(), isA(), and similarTo().
equalTo()returns true if the argument is exactly equal to theModelbeing accessed. This means the fields and the data must be exactly equal.isA()returns true if the argument is of the sameModelas the one being accessed. This means the table name they refer to is the same, and the class name is the same.similarTo()returns true if the argumentisA()Modelas the one being access, and the fields all have the same name. The values do not need to be equal.
Conclusion
It is my hope that you’ll investigate DataModeler with interest and consider using it for your next project. In my next article, I’ll demonstrate how to actually save a Model to a datastore.
You can download DataModeler from Github at http://github.com/leftnode/DataModeler/downloads or visit the primary DataModeler Github page.
My name is Vic Cherubini. You can find me @leftnode or at http://leftnode.com.




September 1, 2010 at 11:56 am
Hi,
Very interesting article, thanks for sharing it.
There is a little thing that bother me a bit : your Model’s setters and getters aren’t strict and never warn in case of the field doesn’t exists.
That’s your choice, but I think it could be really painful for debugging.
What about an optionnal strict param to allow the Model’s class to throws exception when a non-existent field is getted or setted ?
September 1, 2010 at 1:26 pm
Hi nightou,
Absolutely! That makes good sense. Shoot me an email at vmc@leftnode.com with your name/email address/Github account and I’ll include you as a contributor in the next release. I appreciate it.
October 31, 2010 at 12:07 pm
Hi there, nice article but I believe true ORM is build from 3 element: Model(object), mapper, and resource. I have written article on ORM and zend framework where you may find some more usefull info to use for your own development. Have a look at it: http://havl.net/devnotes/2010/10/zendframework-model-orm/