I whipped up a quick example that you can put in a .PHP file and run. It only shows very basic mapping and only loading an object by key. But the infrastructure is there to fill out the mapping support and add the insert, update and delete methods.
My idea is to make this core code completely manually configured. Once it is all working then we could add Identity Map and Unit of Work functionality (either plugin or wrapper). Then we could wrapper it in things like creating the mapper classes from an XML definition or by inspecting the class and database.
Code: Select all
<?php
// this is the Data Mapper that map db table name <-> class name and holds the field mappings
// it implements the load(), insert(), update() and delete() Mapper interface
class Db_Datamapper {
var $_data = array(); // debug data for this example
var $class_name = '';
var $table_name = '';
var $mappings = array();
function Db_Datamapper($class_name, $table_name='') {
$this->setClass($class_name);
$this->setTable($table_name);
}
function setClass($class_name) {
$this->class_name = $class_name;
}
function setTable($table_name) {
$this->table_name = $table_name;
}
function setMapping($property_name, $field_name, $type, $size, $key=false, $table_name='', $filters=array()) {
$this->mappings[$property_name] =& new Db_Datamapper_Mapping($property_name, $field_name, $type, $size, $key, $table_name, $filters);
}
function & load($key) {
$row = $this->_data[$key]; // replace this with SQL generation and fetch data
$class_name = $this->class_name;
$object =& new $class_name ();
foreach (array_keys($this->mappings) as $property_name) {
$this->mappings[$property_name]->setObject($object, $row);
}
return $object;
}
function insert() {
}
function update() {
}
function delete() {
}
}
// this maps a db field name <-> class property name
class Db_Datamapper_Mapping {
var $property_name = '';
var $field_name = '';
// none of thes following are used but they show some basic info needed for a mapping
var $type = '';
var $size = 0;
var $key = false;
var $table_name = '';
var $filters=array();
function Db_Datamapper_Mapping($property_name, $field_name, $type, $size, $key=false, $table_name='', $filters=array()) {
$this->property_name = $property_name;
$this->field_name = $field_name;
$this->type = $type;
$this->size = $size;
$this->key = $key;
$this->table_name = $table_name;
$this->filters = $filters;
}
function setObject($object, $row) {
if ($this->property_name && $this->field_name) {
$property = $this->property_name;
if (isset($row[$this->field_name])) {
$object->$property = $row[$this->field_name];
}
}
}
function setRow($row, $object) {
if ($this->property_name && $this->field_name) {
$property = $this->property_name;
if (isset($object->$property)) {
$row[$this->field_name] = $object->$property;
}
}
}
}
// this class maps the "users" data table to "User" class
class User_Mapper extends Db_Datamapper {
function User_Mapper () {
$this->setClass('User');
$this->setTable('users');
$this->setMapping('username', 'userid', 'string', 20, true, '', array());
$this->setMapping('password', 'password', 'string', 24, false, '', array());
$this->setMapping('active', 'inactive', 'boolean', 1, false, '', array());
}
}
class User {
public $username = '';
public $password = '';
public $active = false;
// methods for application
}
$mapper =& new User_Mapper;
// put some test data into the mapper for this example using schema
// id INTEGER
// usercode VARCHAR(20)
// password VARCHAR(24)
// inactive CHAR(1)
$mapper->_data = array(
'Steve' => array(
'id' => 1,
'userid' => 'Steve',
'password' => 'lollypop',
'inactive' => 'N',
),
'Sally' => array(
'id' => 2,
'userid' => 'Sally',
'password' => 'sallybop',
'inactive' => 'N',
),
);
$User = $mapper->load('Steve');
$User2 = $mapper->load('Sally');
echo '<pre>' . print_r($User, 1) . '</pre>';
echo '<pre>' . print_r($User2, 1) . '</pre>';
There are a couple of small things I want to note. One is that this example does not correctly map the database field "inactive" to the class property "active". It should probably use a Filter to convert 'Y' <-> false and 'N' <-> true as it needs to convert from Y/N to true/false and flip the mean from inactive <-> active.
Also the issue of joins and writing back data to multiple tables always comes up. I have added a "table" field to the Mapping that would override the Mapper's defaults -- would have to see how messy that gets.
The internals need to support Identity Map and Unit of Work later so maybe implementing replaceable default support objects that implement these interfaces but are unintelligent might make sense.