I want to create an educational example of a Data mapper type ORM. You know, it is always interesting to know how the stuff works, but when you take a fully functional software package, it's just impossible to understand its core principles.
If you take even a relatively concise implementation, an Atlas ORM for example, and try to dig it out in order to understand what a Data mapper is, you'd simply give up, unable to tell what is related to the basic pattern and what is added to satisfy other patterns/good practices/real life issues.
So my goal is to create a simplest Data Mapper possible, just to demonstrate the principle, implementing basic CRUD operations. I wanted to make it less than 100 lines of code and succeeded, with help of two default considerations:
- An unique identifier for the record is assumed to be called "id"
- A table name is equal to the lowercase form of a base class name.
I'd like to know is it possible to make it better, so, the questions are:
- how to make it better compliant to the original Data Mapper pattern
- how to make it closer to the real life usage (in its current form it's hardly usable at all)
- any other suggestions or improvements you might have
The code and the usage example are below:
<?php
include 'init.php';
$pdo->query("create temporary table dmtestuser (id int primary key auto_increment, name varchar(25))");
$dm = new PrimitiveDM($pdo);
class dmTestUser {}
$user = new dmTestUser();
$user->name = "Joe";
$dm->save($user);
$dm->delete($user);
$user = new dmTestUser();
$user->name = "Bob";
$dm->save($user);
$id = $user->id;
$user = $dm->find('dmTestUser', $id);
$user->name = "Jane";
$dm->save($user);
$userList = $dm->findBySql('dmTestUser', "SELECT * FROM dmtestuser");
var_dump($userList);
class PrimitiveDM {
protected $db;
protected $driver;
public function __construct(\PDO $pdo, $driver = 'mysql')
{
$this->db = $pdo;
$this->driver = $driver;
}
public function query($sql, $params = [])
{
$stmt = $this->db->prepare($sql);
$stmt->execute($params);
return $stmt;
}
protected function escapeIdent($ident)
{
switch ($this->driver)
{
case 'mysql':
return "`".str_replace("`", "``", $ident)."`";
default:
throw new \Exception("You must define escape rules for the driver ($driver)");
}
}
public function findBySql($class, $sql, $params = [])
{
return $this->query($sql, $params)->fetchAll(\PDO::FETCH_CLASS, $class);
}
public function find($class, $id)
{
$table = $this->escapeIdent(strtolower(basename($class)));
$sql = "SELECT * FROM $table WHERE id = ?";
return $this->query($sql, [$id])->fetchObject($class);
}
public function delete($object)
{
$table = $this->escapeIdent(strtolower(basename(get_class($object))));
$sql = "DELETE FROM $table WHERE id = ?";
$this->query($sql, [$object->id]);
}
public function save($object)
{
$table = $this->escapeIdent(strtolower(basename(get_class($object))));
$properties = get_object_vars($object);
$params = array_values($properties);
if (!empty($object->id))
{
$set = '';
foreach($properties as $name => $value)
{
$set .= $this->escapeIdent($name) . " = ?,";
}
$set = substr($set, 0, -1);
$sql = "UPDATE $table SET $set WHERE id = ?";
$params[] = $object->id;
$this->query($sql, $params);
} else {
$names = '';
foreach($properties as $name => $value)
{
$names .= $this->escapeIdent($name) . ",";
}
$names = substr($names, 0, -1);
$values = str_repeat('?,', count($params) - 1) . '?';
$sql = "INSERT INTO $table ($names) VALUES ($values)";
$this->query($sql, $params);
$object->id = $this->db->lastInsertId();
}
}
}