Kore Nordmann’s post “Why Active Record sucks” explains some of the issues you get by using the popular Active Record pattern for ORM purpose in more complex situations.
“AR is just a wrapper for database access and not appropriate as an ORM. Used carefully, or with very simple applications it really may proof useful.”
I often find myself considering an ORM tool when I start working on a new database library for a client. But every time I fall back to the approach I’m most familiar with and know would give the best performance and the best API design - do it myself.
I normally use a simple solution using the Singleton pattern for managing the database connection, the Value Object pattern for all business objects and the Data Mapper pattern to manage the mapping between business objects and the database schema. It’s as simple as that – handcrafted and no magic going on.
Here’s a simplified directory layout:
lib/
client/
trunk/
src/
DataMapper.php
ValueObject.php
Singleton.php
ClientSingleton.php – extends Singleton
Product.php – extends ValueObject
ProductMapper.php – extends DataMapper
tests/
ProductMapperTest.php
I tend to use a set of abstract classes to more easily follow the patterns used. The following sections shows an example of each of these classes.
Singleton
“A class that can be instantiated only once.”
abstract class Singleton {
private static $instance;
private function __construct() {}
private function __clone() {}
abstract public static function getInstance();
}
A simple implementation of this class could look something like this:
class ClientSingleton extends Singleton {
public static function getConnection($hostname='',$database='',$username='',$password='') {
if(!parent::$instance) {
parent::$instance = new PDO("mysql:host={$hostname};dbname={$database}",$username,$password,array());
}
return parent::$instance;
}
public static function getInstance() {
return self::getConnection();
}
}
Assuming you have your connection settings in a $config-array you can initialize the singleton in the start of your script:
ClientSingleton::getConnection($config['db.hostname'], $config['db.database'], $config['db.username'], $config['db.password']);
When the connection is set up all mapper classes can get the database connection by calling ClientSingleton::getConnection().
The Value Object
“An object whose conceptual identity is based on a combination of values of its properties.”
class ValueObject implements ArrayAccess {
public function __get($name) {
$getter = 'get'.$name;
if(method_exists($this, $getter)) {
return $this->$getter();
} else {
throw new Exception("Undefined property: $name");
}
}
public function __set($name, $value) {
$setter = 'set'.$name;
if(method_exists($this, $setter)) {
$this->$setter($value);
} else {
throw new Exception("Undefined property: $name");
}
}
// functions to implement ArrayAccess
public function offsetExists($offset) {
return isset($this->$offset);
}
public function offsetGet($offset) {
$getter = 'get'.$offset;
return $this->$getter($offset);
}
public function offsetSet($offset, $value) {
$setter = 'set'.$offset;
$this->$setter($value);
}
public function offsetUnset($offset) {
$setter = 'set'.$offset;
$this->$setter(null);
}
}
I find it useful to be able to access data inside a value object in various ways. Having these methods in place and given the following Product class:
class Product extends ValueObject {
private $id;
private $name;
private $attributes;
public function __construct() {
$this->id = 0;
$this->name = '';
$this->attributes = new ArrayObject();
}
public function getID() {
return $this->id;
}
public function setID($value) {
$this->id = $value;
}
public function getName() {
return $this->name;
}
public function setName($value) {
$this->name = $value;
}
public function getAttributes() {
return $this->attributes;
}
public function setAttributes($value) {
$this->attributes = $value;
}
}
I can set values with:
$product->setName("One name");
$product->Name = "Another name";
$product['Name'] = "A third name";
and get values with:
echo $product->getName();
echo $product->Name;
echo $product['Name'];
I also find it very useful to use the ArrayObject from SPL to do stuff like:
$product->Attributes['weight'] = "2.8 kg";
or:
foreach($product->Attributes as $key => $value) {
echo "$key: $value\n";
}
The Data Mapper
“Moves data between objects and a database while keeping them independent of each other and the mapper itself.”
abstract class DataMapper {
abstract public static function findByID($id);
abstract public static function save($object);
abstract public static function delete($object);
abstract protected static function insert($object);
abstract protected static function update($object);
}
These are just the basic set of methods I use in my data mappers. The save() is just a proxy for either insert() or update(). And I always end up with a few more finder methods for various purposes.
I hope you find some of this useful. This approach has at least helped me a lot. By doing ORM this way you are able to manage both domain-driven design and database-driven design and twist them both in the way you want.
Recent Comments