Going to implement Data Mapper - looking for resources

Not for 'how-to' coding questions but PHP theory instead, this forum is here for those of us who wish to learn about design aspects of programming with PHP.

Moderator: General Moderators

User avatar
John Cartwright
Site Admin
Posts: 11470
Joined: Tue Dec 23, 2003 2:10 am
Location: Toronto
Contact:

Post by John Cartwright »

Kind of off topic, but I wouldn't consider this very good practice

Code: Select all

public function __construct()
    {
        if (is_null(self::$_db))
        {
            if(DNS_Registry::getInstance()->isRegistered('database'))
            {
                self::setAdapter(DNS_Registry::getInstance()->get('database'));
                return;
            }
            throw new DNS_Db_DataMapper_Exception(
                'You must set a database adapter before you can instantiate a mapper'
                        );
        }
    }
- I dislike the multiple calls to the register, when a single call can be stored.
- if (is_null(self::$_db)) will always evaluate to true, so why bother?
- My personal preference, and have noticed it is likely the norm, that it is best to first check whether to or everything is setup correctly and then proceed. That way we can avoid ugly returns simply to terminate the method

Code: Select all

public function __construct()
    {
        $registry = DNS_Registry::getInstance();

        if (!$registry->isRegistered('database'))
        {
            throw new DNS_Db_DataMapper_Exception('You must set a database adapter before you can instantiate a mapper');
        }
                
        self::setAdapter($registry->get('database'));
    }
Seems much cleaner and easier to read.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

I'm a little confused by the code too. I guess the first thing that confuses me is that in the use case they create Model and a Mapper, but in the code the Mapper is an Abstract class and the Model extends it. I would hope the goal would be to have a User class that just had properties and methods to support the application, but that did not need to know how to load and save itself. The Mapper would deal with that.

The thing that stands out is that the Model is full of SQL. According to Fowler "With Data Mapper the in-memory objects needn't know even that there's a database present; they need no SQL interface code, and certainly no knowledge of the database schema. "

Maybe we should start with a User class that knows nothing about any datasource and a User database table. Then we can see how we can create a Mapper that can do the insert, delete, and update while the User class remains blissfully ignorant.
(#10850)
User avatar
neophyte
DevNet Resident
Posts: 1537
Joined: Tue Jan 20, 2004 4:58 pm
Location: Minnesota

Post by neophyte »

Pssst ... I recognize the example. ;)

Great modification! I like this pattern. It seems like a great way to isolate/encapsulate sql. But I was just wondering what this class might look like if it needed to pull an "INNER JOIN" type of select. How would you deal with complex queries involving more than one table? Or did I miss something?
User avatar
Luke
The Ninja Space Mod
Posts: 6424
Joined: Fri Aug 05, 2005 1:53 pm
Location: Paradise, CA

Post by Luke »

Jcart wrote:if (is_null(self::$_db)) will always evaluate to true, so why bother?
not true...

Code: Select all

<?php
DNS_DataMapper::setAdapter($db);
$mapper = new DNS_DataMapper;
?>
I do like this better though...

Code: Select all

public function __construct()
    {
        if (is_null(self::$_db))
        {
            $registry = DNS_Registry::getInstance();
            if(!$registry->isRegistered('database'))
            {
                throw new DNS_Db_DataMapper_Exception(
                    'You must set a database adapter before you can instantiate a mapper'
                );
            }
            self::setAdapter($registry->get('database'));
        }
    }
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

It seems like the direction you are headed is much more like Active Record or Table Data Gateway. Those are simpler solutions than Data Mapper and would get the SQL out of the Model proper and into the inherited gateway class.
(#10850)
User avatar
Luke
The Ninja Space Mod
Posts: 6424
Joined: Fri Aug 05, 2005 1:53 pm
Location: Paradise, CA

Post by Luke »

maybe I am... but that's not where I want to be going. I want to understand the data mapper pattern.
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

With a Data Mapper you would have a User class that knows nothing about persisting itself and a Mapper that does. So the question becomes, what does the Mapper need to know to do its job?

Code: Select all

class User {
     public $username = '';
     public $password = '';
     public $active = false;

     function setUsername($username ) {
          $this->username = $username;
     }

     function getUsername() {
          return$this->username;
     }

     function setPassword($password) {
          $this->password = sha1($password);
     }

     function checkPassword($password) {
          return $this->password == sha1($password);
     }

     function activate() {
          $this->active = true;
     }

     function inactivate() {
          $this->active = false;
     }

     function isActive() {
          return $this->active;
     }
}

class User_Mapper extends DNS_Db_DataMapper {

// map properties to db fields and know things like true/false is Y/N in the database
// generate SQL to do insert, delete, and update
// do the loading and saving queries

}

$User = new User;
$User->setUsername('Steve');
$User->setPassword('lollylee');
$User->activate();

$mapper = new User_Mapper;
$mapper->insert($User); 

// or

$User = $mapper->load('Steve');
And let's say we have a data table that looks like this:

Code: Select all

id INTEGER
usercode VARCHAR(20)
password VARCHAR(24)
inactive CHAR(1)
(#10850)
User avatar
Maugrim_The_Reaper
DevNet Master
Posts: 2704
Joined: Tue Nov 02, 2004 5:43 am
Location: Ireland

Post by Maugrim_The_Reaper »

I don't care for the level of automation johno was talking about. I would prefer to have as much control as possible. I do not mind doing things like this:
Both approaches (using public methods, or public getter/setters) can be the same thing if you implement the __get/__set magic methods in PHP5. You can manage preventing non-set public properties by letting __set check whether the property name exists in a private array variable which contains a list of all valid field names with a default NULL value.

This also has the benefit that the magic methods become standard for each Model, and can be refactored to a common parent class - leaving only the field name definition within each subclass.
User avatar
johno
Forum Commoner
Posts: 36
Joined: Fri May 05, 2006 6:54 am
Location: Bratislava/Slovakia
Contact:

Post by johno »

arborint wrote:I am not sure the distinction of explicit vs implicit given is very accurate either because there are a number of points within such a system that operations can be automatic or manual and for that matter immediate or deferred. I don't think a choice necessarily needs to be made because many of those decisions that you want the application to be able to make at run time -- the Mapper providing several behavior options.
I absolutely agree with you. There is definitely a distinction between them, but I've never written you need to make a decision. Sorry I did not make this clear in a first go.
The Ninja Space Goat wrote:maybe I am... but that's not where I want to be going. I want to understand the data mapper pattern.
Yes, airborint is right, what you showed definitely is a sort of ActiveRecord.

Airborint's example of a data mapper is right and clean, but it is going get messy when it comes to loading of related objects (collections, associations). You probably won't be able to make persistent classes (User class in this case) absolutely unaware of mapper. At least I don't managed to do it without some major design drawbacks.

Persistence is in general recognized as a crosscutting concern (in aspect oriented terminology) so its really hard to modularize. A notoriously known crosscutting concern is logging. It is scattered through multiple layers, polluting and mixing other business logic. When it comes to persistence it is pretty much same.
User avatar
Luke
The Ninja Space Mod
Posts: 6424
Joined: Fri Aug 05, 2005 1:53 pm
Location: Paradise, CA

Post by Luke »

OK I see... so you have a model class for each table and then a mapper that knows how to persist that model. That makes sense. So this type of solution seems like it would have to be really unportable, because it would have to be so built in to your particular system... is that right?
User avatar
neophyte
DevNet Resident
Posts: 1537
Joined: Tue Jan 20, 2004 4:58 pm
Location: Minnesota

Post by neophyte »

Isn't true though, that once you are at the point of defining/writing sql that portability is all said and done?
User avatar
Luke
The Ninja Space Mod
Posts: 6424
Joined: Fri Aug 05, 2005 1:53 pm
Location: Paradise, CA

Post by Luke »

hmm... maybe? Could you elaborate?
User avatar
Christopher
Site Administrator
Posts: 13596
Joined: Wed Aug 25, 2004 7:54 pm
Location: New York, NY, US

Post by Christopher »

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.
(#10850)
User avatar
neophyte
DevNet Resident
Posts: 1537
Joined: Tue Jan 20, 2004 4:58 pm
Location: Minnesota

Post by neophyte »

Given the messy nature of putting something like this together, Aborint -- Is this sort of thing a viable data solution for PHP? Or does an application that implements this need to implement another strategy for dealing with complex queries (Joins, multiple table updates).
User avatar
Luke
The Ninja Space Mod
Posts: 6424
Joined: Fri Aug 05, 2005 1:53 pm
Location: Paradise, CA

Post by Luke »

don't have time to examine the code fully until lunch, but saw this...

Code: Select all

$mapper->_data = array(
        'Steve' => array(
                'id' => 1,
                'userid' => 'Steve',
                'password' => 'lollypop',
                'inactive' => 'N',
                ),
        'Sally' => array(
                'id' => 2,
                'userid' => 'Sally',
                'password' => 'sallybop',
                'inactive' => 'N',
                ),
        );
lollypop and sallybob... priceless. :lol:
Post Reply