Data Shuffler

Data Shuffler - PHP data mapper implementation - Maps relational data to an object oriented paradigm

Download
API Documentation
Browse Source Online
Forums

Table of Contents

Overview

Dependencies

Currently needs a copy of Zend_Db for the database adapter. Obtain at Zend Framework is NOT bundled with Data Shuffler. Zend_Loader is also used for auto loading.

Terminology

  • Relational: Any tabular representation of data, including any constraints on the possible values and combinations of values.
  • Object-Oriented: A programming paradigm that uses "objects" and their interactions. Techniques may include features such as encapsulation, modularity, polymorphism, and inheritence. http://en.wikipedia.org/wiki/Object-oriented_programming
  • Data Mapper: A layer of Mappers that moves data between objects and a database while keeping them independent of each other and the mapper itself.

Problem / Design Forces

Objects and relational databases have different mechanisms for structuring data. Many parts of an object, such as collections and inheritance, aren't present in relational databases. When you build an object model with a lot of business logic it's valuable to use these mechanisms to better organize the data and the behavior that goes with it. Doing so leads to variant schemas; that is, the object schema and the relational schema need not match up.

You still need to transfer data between the two schemas, and this data transfer becomes a complexity in its own right. If the in-memory objects know about the relational database structure, changes in one tend to ripple to the other.

Existing ORM such as the active record pattern try to treat the database as if it does not exist. This is a mistake since SQL is already a good query language that allows a plethora of techniques for querying data. The goal of data mapper is not to replace the need for SQL but simply to factor it out of the domain layer where only business logic belongs. If you have a complicated query you can subclass the mapper and setup specialized finders. This is the beauty of the data mapper pattern. Once persistence logic is factored out of your models, you can use advanced techniques such as having multiple mappers mapping multiple schemas but use the same objects. Employing a data mapper in your architecutre will free the programming team from having to coordinate very closely with the DBA. It will allow you to refactor code without interrupting existing databases in production, and vice versa.

Solution

The Data Mapper is a layer of software that separates the in-memory objects from the database. Its responsibility is to transfer data between the two and also to isolate them from each other. 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. (The database schema is always ignorant of the objects that use it.) Since it's a form of Mapper, Data Mapper itself is even unknown to the domain layer.

Data Mapper by Martin Fowler

Summary of Functionality

  • Currently enforces a 1-1 mapping between tables and classes
  • Maps data via "dependency injection" to either setters or constructors, or in the case of a collection, "add" and "remove" methods.
  • Uses a seperate mapping class to map table fields to class properties
  • Automatically reads table metadata and adds mappings for each field. Any fields ending in "_id" will be detected as foreign keys. Adding fields manually will cause those fields to be skipped over by this automatic process. The data mapper uses multiple mapping classes to perform the heavy lifting of these complex mappings.

Creating Models

Before mapping any data you first need to create a model to map to. Models need to extend Shuffler_Model which provides base functionality such a common interface for identifying models. If you implement a constructor you must accept an id primary key and pass it to the parent class. Other then this you are free to implement your classes how you want. If you want immutable data common practice is to pass data thru the constructor so your objects are fully formed upon being instantiated, not providing a setter for immutable data will make your programs easier to debug. ex:

class Person extends Shuffler_Model
{
    public function __construct( $id, $someOption )
    {
        $this->someOption = $someOption;
        parent::( $id );
    }

    protected $title;

    public function setTitle( $title )
    {
        $this->title = $title;
    }

    public function getTitle()
    {
        return $this->title;
    }
}

If you have multiple models that refer to eachother in their constructor, loading one will cause the other to be loaded. Normally this would result in cyclic references, Shuffler will automatically handle this by using code generation to create temporary objects to break the cycle. You need not do anything special to take advantage of this.

Creating Mappers

Once your model is created, it needs a data mapper. In some cases you may be able to instantiate Shuffler_Mapper directly, for the purposes of this tutorial we will extend the Shuffler_Mapper class to keep things nice and encapsulated.

Setting up Table

By default Shuffler Mapper will attempt to guess the name of a model's table by converting the class name to all lowercase. If you need a different table name you can do it as follows

class Person_Mapper extends Shuffler_Mapper
{
    protected function init()
    {
        $this->setTable( 'people' );
    }
}

Setting up Mappings

By default Shuffler Mapper will attempt to guess as many mappings from the database field names automatically. In most cases you will want to override this behavior. You will need to set up mappings to configure your fields, classes, & properties.

Embedded Mappings

To setup mappings override the protected init() method. Embedded mappings tell the mapper a field is a scalar value ( integer, string, boolean ) and not an object.

class Person_Mapper extends Shuffler_Mapper
{
    protected function init()
    {
        $this->addEmbedded(  'name', array( 'immutable' => true ) ); 
        $this->addEmbedded(  'age' ); 
    }
}

The second paramater of addEmbedded() is an optional array of paramaters. Right now we will just use this to tell Shuffler_Mapper that the name field is immutable, meaning it should be passed via the constructor and that no setter exists on the model. This helps enforce keeping your data truly immutable, although if you create a setter anyways obviously your data is no longer immutable. Leaving this off causes Shuffler_Mapper to inject the data via a setter instead of in the constructor, if no setter is found, an exception will be thrown.

Foreign Key Mapping ( single valued relationships )

Data Shuffler currently supports single valued as well as multi-valued relations. To tell the mapper to load related models when loading your model, use addSingle() and addCollection() methods. A mapper delegates to a "foreign mapper" to handle loading relations. It is recommended to create a concrete implementation for all mappers but you don't have to, if a concrete implementation for foreign mappers doesn't exist, the mapper will still work, although in most cases you will need to create concrete implementations to set up the actual mappings for each of your mappers.

 class Content_Document_Mapper extends Shuffler_Mapper
{
    protected function init()
    {
        $this->addSingle( 'Content_Type', array( 'immutable' => true, 'property' => 'Type' ) );
    }
}

The property paramater tells the mapper how it can set & get this object. In this case, upon loading your models, the mapper will delegate to the Content_Type Mapper to load the Content_Type model. Since we told the mapper it was immutable it will be injected into the constructor. Constructor arguments are injected in the order the mappings are added to the mapper. In the future Data Shuffler may support more robust dependency injection techniques.

Foreign Key Mapping ( multi-valued / collection relationships )

Add a relation of more than one model by using addCollection(). Syntax is the same as addSingle().

 class Content_Document_Mapper extends Shuffler_Mapper
{
    protected function init()
    {
        $this->addCollection( 'Content_Field_Value', array( 'field' => 'content_document_id', 'property' => 'fieldValue' ) );
    }
}

In relational databases when you have a collection of related objects, the foreign key goes on the foreign table. To tell the mapper how to perform the join pass the field paramater. If you do not pass this it will default to name of the table plus "_id". In most cases you will not need to pass this. You can also see we have overrode the property name as in the example with single valued mappings. Where as with a single valued mapping setFieldValue() and getFieldValue() would have been used, with a collection of relations addFieldValue() and getFieldValues() will be called. In the future it is planned to support more fine grained control over naming conventions.

Association Table Mapping ( bi-directional collection relationships )

Association table mapping is supported as of v0.2 This functionality is still in "alpha" and thus may not be stable. Query optimization and more documentation will come in v0.3 Add an association table mapping like so

 class Person_Mapper extends Shuffler_Mapper
{
    protected function init()
    {
        $this->addEmbedded(  'title', array( 'immutable' => true ) );
        $this->addAssociation(  'Skill' );
    }
}

Plugin Mapping ( inheritence )

Single table inheritence is possible using a plugin mapping. This will turn any field into a descriminator. The name of the field will be used as the name of the model to instantiate allowing you to structure inheritence in an intuitive manner. To setup a plugin mapping tell the mapper the name of the field to desciminate on and the name of the abstract class.


protected function init()
{
    $this->addPlugin(  'class_name', 'Widget_Type' );
} 

Finders

Finders are methods on the mapper you use to query / locate domain models. Several common finders exist and you can extend the mapper & create finders for ad-hoc SQL querying. Make sure to check out the API documentation for more information on all of the mapper's available methods.

Finding Models by Primary Key

To use your mapper get an instance of it and invoke one of the various finder methods.

 // instantiate mapper
$mapper = Shuffler_Mapper::getMapper( 'Person' );

// finding by primary key
$person = $mapper->find( 3 );

Finding Models by a field value

You can pull back all models having a certain field value in the database using the findByField() method. Internally this constructs an SQL WHERE clause and delegates to findWhere()

 // instantiate mapper
$mapper = Shuffler_Mapper::getMapper( 'Person' );
   
// find by field
$people = $mapper->findByField( 'foo', 'bar' ); // returns Shuffler_Collection

// iterate the collection
foreach( $people as $person )
{
    var_dump( $person );
}

Finding Models by specifying an SQL WHERE clause

You can pass in an SQL where clause ( as string ) to the findWhere() finder, which will return a collection of all found models

 
// instantiate mapper
$mapper = Shuffler_Mapper::getMapper( 'Person' );
   
// find by field
$people = $mapper->findWhere( 'some_field IN( 5, 6 ) OR some_field = 3 ' ); // returns Shuffler_Collection

// iterate the collection
foreach( $people as $person )
{
    var_dump( $person );
}

Custom Finders

You can create additional finders on your concrete mappers for custom SQL logic. To augment querying logic for all finders override select(), update(), insert(), and delete() respectively. Since Data Shuffler uses Zend_Select you can reset, augment, or append to the query or parts of the query. Zend_Select is based on PDO and in the near future there will be more adapters. Zend_Db itself already has multiple adapters out of the box so various RDMS are supported ( including: MySQLi, Oracle, PDO for IBM DB2 and Informix Dynamic Server (IDS), PDO Microsoft SQL Server, PDO MySQL, PDO Oracle, PDO PostgreSQL, PDO SQLite ).

 
public function findByType( Content_Type $type )
{
    $result = $this->findByField( 'content_type_id', $type->getId() );
    return $result;
}

If you need to just augment the query being generated but not replace it entirely, you may do so. This allows you to use built in mappings as well as some custom SQL, to do this override the doSelect() template method

 

protected function doSelect( Zend_Db_Select $select )
{
    // either hard code table
    $select->joinLeftUsing( 'content_field_value', 'content_document_id' );
    // use foreign mapper in case the table name changes later
    $select->joinLeftUsing( $this->getMapper( 'Content_Field_Value' )->getTableName(), 'content_document_id' );
    return $select;
}

Saving Models

To save a model, pass it the save() method of the mapper. If there is a non-zero identifier the mapper will call update() internally. Otherwise it will call insert().

 
$mapper = Shuffler_Mapper::getMapper( 'Person' );
$person = $mapper->find( 3 );
$person->setTitle( 'fooBar' );
$mapper->save( $person );

Overriding Model Loading

In some cases the built in mappings may not be sophisticated enough to handle what you need to do. This is why you are able to override loading behavior. Override doLoad() which must accept a primary key and a result set ( array of field's values ).

 

protected function doLoad( $id, $rs )
{
    // create the model with it's immutable mappings, insantiate model direct if you don't want this behavior
    $model = $this->createModel( $id, $rs );  
    // find some foreign models
    $valueMapper = $this->getMapper( 'Content_Field_Value' );
    $values = $valueMapper->findByField( 'content_document_id', $model->getId() );
    // iterate them and do stuff with the mappings
    foreach( $values as $value )
    {
        $model->setFieldValue( $value->getField(), $value );
    }
    return $model;
}

Identity Map

In order to avoid repetitive trips to the database an identity map ( cache ) exists. If you want to clear this you can do so as in the following example:

 
$mapper = $this->getMapper( 'Website_Category' );
// load some data here

$mapper->clearIdentityMap();

// models will be re-loaded from the database upon subsequent calls to finders.

Hierchial Data

Basic support for hierchial data exists.

 

class Website_Category_Mapper extends Shuffler_Mapper_Tree
{
    protected function init()
    {
        $this->setTable( 'navigation' );
        $this->addEmbedded(  'title', array( 'immutable' => true ) );
    }
}
class Website_Category extends Shuffler_Model_Tree_Node
{
    /**
     * Node Title
     */
    protected $title = null;

    public function __construct( $id = 0, $title = '' )
    {
        parent::__construct( $id, $title );
        $this->title        = $title;
    }
}

$mapper = $this->getMapper( 'Website_Category' );
$node = $mapper->find( 3 );
$parent = $node->getParent(); // get parent node
$depth = $node->getDepth(); // get depth of node
$descendants = $node->getDescendants(); // get all descendants of all depth below this node
$descendants = $node->getChildren(); // get descendants immediately under this node only
foreach( $descendants as $node )
{
    echo str_repeat( ' ', 5 ) . $node->getTitle(); // indent output
}

Schema Abstraction

Data shuffler also provides basic schema abstraction. This is still in early alpha stages so things like indexes or uniques keys aren't implemented quite yet. To start grab an instance of the DB factory.

 
$factory = new Shuffler_Db_Factory();   

Create a table and add some fields, first paramater is name of field. Second paramater is field type ( as defined by your DBMS ), 3rd paramater is field length, and finally 4th paramater is an array of options. Call setPrimary() on a field after it is created to denote it as the primary key for the table. Trying to create a table with more than one primary key will cause an exception to be thrown. Call the create() method to create the table. Once the table is created you can insert rows as arrays.

 
$content_type = $factory->createTable( 'content_type' );
$content_type->createField( 'id', 'bigint', 20, array( 'extra' => 'auto_increment' ) )->setPrimary();
$content_type->createField( 'title' );
$content_type->create();
$content_type->insert( array( 3, 'Article' ) )
    ->insert( array( 4, 'Author' ) )
    ->insert( array( 5, 'Portfolio' ) );

This allows your schema to be versioned by your version management system. In the near future database migrations and other techniques will be implemented.

Object Collections

Data shuffler utilizes a collection class for return types of finders. You may also instantiate and use this in your code.

 
$collection = new Shuffler_Collection(); 
$collection->equals( new Shuffler_Collection() ) ); // true | collection starts empty
$collection->addModel( $article );
$collection->contains( $article ); // true

foreach( $collection as $model )
{
    // do something interesting before the boss fires us
}

Documentation generated on Mon, 02 Mar 2009 02:24:58 -0500 by phpDocumentor 1.4.1

Back to Top

DownloadAPI DocumentationForumsContactBlog


© Ne8 2008-2009 • Syntax Highlighting: Kodify

Link to Data Shuffler php data mapper