There’s no shortage of options when it comes to picking a PHP-based ORM. The current variety of options satisfies a broad number of tastes and palates, even the pickiest ones. Similar to some other utility packages available, the prolific nature of ORMs brings with it a healthy effect: whether you use a popular third-party MVC framework like CodeIgniter Reactor, CakePHP or Kohana (only to name a few) or employ your own MVC masterpiece, chances are that you can add the functionality of an ORM to your applications with only minor hassles.
Don’t let the large number of ORMs fool you, though; the development of such packages is very often challenging and complex. That's especially true if you’re planning to couple the management of the most common types of relationships (you know, one-to-one, one-to-many and many-to-many) to the functionality provided by enterprise-level design patterns, like Data Mappers, Identity Maps and a Unit of Work.
At this moment, Doctrine 2.x is undeniably the most powerful and full-featured ORM on PHP terrain. It not only implements the aforementioned patterns behind its API; it also provides a solid database abstraction layer. However, other libraries, like RedBeanPHP, dORM and Propel are solid contenders that yield quite impressive results too, so feel free to give them a shot.
Obviously, with so many ORMs at one's disposal for free, it seems pretty pointless to develop a custom one; are we trying to reinvent the wheel? No, of course not. But if you need to create a simple application that performs a few CRUD operations on some related domain objects and don’t want to climb the learning curve of a third-party library, then implementing a custom ORM might make sense. There's alos the educational aspect of the process (yes, learning one or two things never hurts).
If this topic has caught your attention, over the course of this tutorial I’ll be building from scratch a small ORM. You'll be able to customize and extend it at will to fit your personal requirements.
With that said, it’s time to start showing you some concrete and functional code. Let’s get going!
Building a Simple Data Persistence Layer
As you know, in many cases the functionality of an ORM is encapsulated behind a set of classes responsible for handling the relationships between different types of objects, which are persisted in a RDBMS via an Active Record implementation, or in more modern libraries, via the Data Mapper pattern (ala Doctrine 2.x, where domain objects are persistence agnostic).
In this case, for obvious reasons, I’m going to take a much simpler approach: the workhorse of my ORM will be a mapping layer, which will not only move data between a domain model and the storage, but will be capable of reconstituting entities having a one-to-one and one-to-many relationship with other entities (the so-called aggregate roots in Domain-Driven Design).
The first step is to create the layer tasked with interacting with the underlying persistence mechanism. For the sake of simplicity, this will rest on a MySQL database. This layer is simple; its implementation is as follows:
/** * Constructor */ public function __construct(array $config) { if (count($config) !== 4) { throw new \InvalidArgumentException('Invalid number of connection parameters.'); } $this->_config = $config; }
/** * Connect to MySQL */ public function connect() { // connect only once if ($this->_link === null) { list($host, $user, $password, $database) = $this->_config; if (!$this->_link = @mysqli_connect($host, $user, $password, $database)) { throw new \RuntimeException('Error connecting to the server : ' . mysqli_connect_error()); } unset($host, $user, $password, $database); } return $this->_link; }
/** * Execute the specified query */ public function query($query) { if (!is_string($query) || empty($query)) { throw new \InvalidArgumentException('The specified query is not valid.'); } // lazy connect to MySQL $this->connect(); if (!$this->_result = mysqli_query($this->_link, $query)) { throw new \RuntimeException('Error executing the specified query ' . $query . mysqli_error($this->_link)); } return $this->_result; }
/** * Perform a DELETE statement */ public function delete($table, $where = '') { $query = 'DELETE FROM ' . $table . (($where) ? ' WHERE ' . $where : ''); $this->query($query); return $this->getAffectedRows(); }
/** * Escape the specified value */ public function quoteValue($value) { $this->connect(); if ($value === null) { $value = 'NULL'; } else if (!is_numeric($value)) { $value = "'" . mysqli_real_escape_string($this->_link, $value) . "'"; } return $value; }
/** * Fetch a single row from the current result set (as an associative array) */ public function fetch() { if ($this->_result !== null) { if (($row = mysqli_fetch_array($this->_result, MYSQLI_ASSOC)) === false) { $this->freeResult(); } return $row; } return false; }
/** * Get the insertion ID */ public function getInsertId() { return $this->_link !== null ? mysqli_insert_id($this->_link) : null; }
/** * Get the number of rows returned by the current result set */ public function countRows() { return $this->_result !== null ? mysqli_num_rows($this->_result) : 0; }
/** * Get the number of affected rows */ public function getAffectedRows() { return $this->_link !== null ? mysqli_affected_rows($this->_link) : 0; }
/** * Free up the current result set */ public function freeResult() { if ($this->_result === null) { return false; } mysqli_free_result($this->_result); return true; }
/** * Close explicitly the database connection */ public function disconnect() { if ($this->_link === null) { return false; } mysqli_close($this->_link); $this->_link = null; return true; }
/** * Close automatically the database connection when the instance of the class is destroyed */ public function __destruct() { $this->disconnect(); } }
If you've ever come across some of my PHP articles here at Developer Shed, you may already be familiar with the inner workings of the above “MysqlAdapter” class and the interface that it implements. In either case, this adapter acts like a simple wrapper for the “mysqli” PHP extension, which among other things, runs queries in a specified database, fetches rows and retrieves insertion IDs. It's nothing really overcomplicated, to be honest.
Nevertheless, I’d like you to notice one subtle detail: the class’s top-level namespace is called “Blog.” I did this because I plan to demonstrate the functionality of my humble ORM by deploying a blog application. Due to the flexibility of the project, however, the domain model will be easily substitutable, so feel free to switch to the top-level namespace that suits your needs best.
Having clarified that, it’s safe to say that the ORM’s persistence layer is ready to go. Therefore, it’s time to develop a few more components. As I said a moment ago, the main functionality of the ORM will be built around the data mapper pattern. Therefore, in the coming section I’ll be implementing a simple hierarchy of mappers, which will be tasked with fetching, saving to and deleting entities from the aforementioned MySQL database.
To see how these brand new components will be created, click on the link that appears below and keep reading.