* CREATE TABLE persons
* (
* id integer unsigned not null auto_increment,
* full_name varchar(255),
* age integer,
* PRIMARY KEY (id)
* ) TYPE=InnoDB;
*
*
* We would like to map this person to an object in PHP. First we need to define
* a class that holds one person:
*
* class Person
* {
* private id = null;
* public name = null;
* public age = null;
*
* public function getState()
* {
* $result = array();
* $result['id'] = $this->id;
* $result['name'] = $this->name;
* $result['age'] = $this->age;
* return $result;
* }
*
* public function setState( $properties )
* {
* foreach( $state as $key => $value )
* {
* $this->$key = $value;
* }
* }
* }
*
* Since we will map the id field to the primary key auto_increment value it is important
* that it defaults to null. You can set default values on the other fields as you like.
* The setState and getState methods are used by the ezcPersistentSession when storing
* and retrieving persistent objects.
* For simplicity we have made the name and age properties public. In a real application
* you would of course have access to these through real properties or through access
* methods.
*
* In order to make our class a real persistent object we need to create a mapping between
* the database table and the class. This is done with the ezcPersistentObjectDefinitionClass.
* We will use the ezcPersistentCodeManager to manage our definitions. It requires each
* definition to be in a separate file that has the same name as our class in lowercase.
* person.php:
*
* table = "person";
* $def->class = "Person";
*
* $def->idProperty = new ezcPersistentObjectIdProperty;
* $def->idProperty->columnName = 'id';
* $def->idProperty->propertyName = 'id';
*
* $def->properties['name'] = new ezcPersistentObjectProperty;
* $def->properties['name']->columnName = 'full_name';
* $def->properties['name']->propertyName = 'name';
* $def->properties['name']->propertyType = ezcPersistentObjectProperty::PHP_TYPE_STRING;
*
* $def->properties['age'] = new ezcPersistentObjectProperty;
* $def->properties['age']->columnName = 'age';
* $def->properties['age']->propertyName = 'age';
* $def->properties['age']->propertyType = ezcPersistentObjectProperty::PHP_TYPE_INT;
* return $def;
* ?>
*
* Each persistent object is required to have an id field. This id field must be an integer
* both in PHP and in the database. If you look at the API of ezcPersistentObjectDefinition
* it also has a property named 'columns' which is a reverse mapping of the 'properties'
* property on the column name. We don't need to define it ourselves as it is automatically
* set up by ezcPersistentCodeManager.
*
* It is time to try out our creation. In all applications using persistent objects
* we need to initialize the session object:
*
* $session = new ezcPersistentSession( ezcDbInstance::get(),
* new ezcPersistentCodeManager( "path/to/definitions" ) );
*
* This sets up the session to work on the default database instance. The code manager will
* feed the session with persistent object definitions. "path/to/definitions" must of course be
* replaced with the path where you put person.php.
*
* Lets make a new persistent object and store it to the database:
*
* $object = new Person();
* $object->name = "John Doe";
* $object->age = 31;
*
* $session->save( $object );
*
* This code saves our newly created object to the database and generates an id for it. The id
* is set to the id property of the object.
* If we want to update the same object with new data we can continue to work on the same
* object:
*
* $object->age = 32;
* $session->update( $object );
*
* Note that we used update() to store the object this time. This is because we want to trigger
* an UPDATE query instead of an INSERT query. Similarly to update and save we also have delete.
* To retrieve a stored persistent object use one of the load methods:
*
* $object = $session->load( 'Person', 1 );
* $session->delete( $object );
*
* Loads the object with id 1 and deletes it.
*
* If you have stored a lot of persistent objects to the database and you want to retrieve a
* list you can use the find method. The find method requires a query parameter which you
* can retrieve from the session first.
*
* $q = $session->createFindQuery( 'Person' );
* $q->where( $q->expr->gt( 'age', 15 ) )
* ->orderBy( 'full_name' )
* ->limit( 10 );
* $objects = $session->find( $q, 'Person' );
*
* This code will fill the $objects variable with 10 Person persistent objects where
* age is higher than 15 sorted on their name.
*
* Note that this beta release requires you to use the table and column names when building the
* query. In the final release you will be able to use the persistent object name and the
* property names instead.
*
* @see ezcPersisentObject
* @todo if there is a high probability that an exception was caused by
* a bad definition or by a bad set/getState method we should run a checker or some sort.
* @todo - remove required and default value fields in definition for now
*
* @package PersistentObject
*/
class ezcPersistentSession
{
/**
* The database instance this session works on.
*
* @var PDO
*/
private $db = null;
/**
* The persistent object definition manager.
*
* @var ezcPersistentDefinitionManager
*/
private $manager = null;
/**
* Constructs a new persistent session that works on the database $db.
*
* The $manager provides valid persistent object definitions to the
* session.
*
* @param PDO $db
* @param ezcPersistentDefinitionManager
*/
public function __construct( PDO $db, ezcPersistentDefinitionManager $manager )
{
$this->db = $db;
$this->manager = $manager;
}
/*
* Returns a delete query for the given persistent object $class.
*
* The query is initialized to delete from the correct table and
* it is only neccessary to set the where clause.
*
* Example:
*
*
*
* @param string $class
* @return ezcQueryDelete
*/
// public function createDeleteQuery( $class )
// {
// }
/**
* Deletes the persistent object $pObject.
*
* This method will perform a DELETE query based on the identifier
* of the persistent object.
* After delete() the identifier in $pObject will be reset to null.
* It is possible to save() $pObject afterwords. The object will then
* be stored with a new id.
*
* @throws ezcPersistentException if the object is not recognized as a persistent object.
* @throws ezcPersistentException if the object is not persistent already.
* @throws ezcPersistentException if the object could not be deleted.
* @param object $pObject
* @return void
*/
public function delete( $pObject )
{
$def = $this->manager->fetchDefinition( get_class( $pObject ) ); // propagate exception
$state = $pObject->getState();
$idValue = $state[$def->idProperty->propertyName];
// check that the object is persistent already
if( $idValue == null || $idValue < 0 )
{
$class = get_class( $pObject );
throw new ezcPersistentObjectException( "Tried to delete object of type $class which is not persistent" );
}
// create and execute query
$q = $this->db->createDeleteQuery();
$q->deleteFrom( $def->table )
->where( $q->expr->eq( $def->idProperty->columnName, $q->bindValue( $idValue ) ) );
$stmt = $q->prepare();
$stmt->execute();
if( $stmt->errorCode() != 0 )
{
throw new ezcPersistentObjectException( "The delete query failed." );
}
}
/*
* Deletes persistent objects using the query $query.
*
* The $query should be created using getDeleteQuery().
*
* Currently this method only executes the provided query. Future
* releases PersistentSession may introduce caching of persistent objects.
* When caching is introduced it will be required to use this method to run
* cusom delete queries. To avoid being incompatible with future releases it is
* advisable to always use this method when running custom delete queries on
* persistent objects.
*
* @param ezcQueryDelete $query
* @return void
*/
// public function deleteFromQuery( ezcQueryDelete $query )
// {
// }
/*
* Returns an update query for the given persistent object $class.
*
* The query is initialized to update the correct table and
* it is only neccessary to set the correct values.
*
* Example:
*
*
*
* @param string $class
* @return ezcQueryUpdate
*/
// public function createUpdateQuery( $class )
// {
// }
/*
* Updates persistent objects using the query $query.
*
* The $query should be created using getUpdateQuery().
*
* Currently this method only executes the provided query. Future
* releases PersistentSession may introduce caching of persistent objects.
* When caching is introduced it will be required to use this method to run
* cusom delete queries. To avoid being incompatible with future releases it is
* advisable to always use this method when running custom delete queries on
* persistent objects.
*
* @param ezcQueryUpdate $query
* @return void
*/
// public function updateFromQuery( ezcQueryUpdate $query )
// {
// }
/**
* Returns a select query for the given persistent object $class.
*
* The query is initialized to fetch all columns from the correct table.
*
* Example:
*
* $q = $session->createFindQuery( 'Person' );
* $allPersons = $session->find( $q, 'Person' );
*
*
* @throws ezcPersistentObjectException if there is no such persistent class.
* @param string $class
* @return ezcQuerySelect
*/
public function createFindQuery( $class )
{
$def = $this->manager->fetchDefinition( $class ); // propagate exception
// init query
$q = $this->db->createSelectQuery();
$q->select( '*' )->from( $def->table );
return $q;
}
/**
* Returns the result of the query $query as a list of objects.
*
* Example:
*
* $q = $session->createFindQuery( 'Person' );
* $allPersons = $session->find( $q, 'Person' );
*
*
* If you are retrieving large result set, consider using findIterator()
* instead.
*
* @throws ezcPersistentObjectException if there is no such persistent class.
* @param ezcQuerySelect $query
* @param string $class
* @return array(object)
*/
public function find( ezcQuerySelect $query, $class )
{
$def = $this->manager->fetchDefinition( $class ); // propagate exception
$stmt = $query->prepare();
$stmt->execute();
$rows = $stmt->fetchAll( PDO::FETCH_ASSOC );
// convert all the rows states and then objects
$result = array();
foreach( $rows as $row )
{
$object = new $def->class;
$object->setState( $this->rowToStateArray( $row, $def ) );
$result[] = $object;
}
return $result;
}
/*
* Returns the result of the query $query as an object iterator.
*
* This method is similar to find() but returns an iterator
* instead of a list of objects. This is useful if you are going
* to loop over the objects and just need them one at the time.
* Because you only instantiate one object is is faster than find().
*
* @param ezcQuerySelect $query
* @return Iterator
*/
// public function findIterator( $selectQuery )
// {
// }
/**
* Returns the persistent object of class $class with id $id.
*
* @throws ezcPersistentException if the object is not available.
* @throws ezcPersistentException if there is no such persistent class.
* @param string $class
* @param int $id
* @return object
*/
public function load( $class, $id )
{
$def = $this->manager->fetchDefinition( $class ); // propagate exception
$object = new $def->class;
$this->loadIntoObject( $object, $id );
return $object;
}
/**
* Returns the persistent object of class $class with id $id.
*
* This method is equivalent to load() except that it returns
* null instead of throwing an exception if the object does not
* exist.
*
* @param string $class
* @param int $id
* @return object|null
*/
public function loadIfExists( $class, $id )
{
$result = null;
try {
$result = $this->load( $class, $id );
} catch( Exception $e ){} // eat, we return null on error
return $result;
}
/**
* Loads the persistent object with the id $id into the object $pObject.
*
* The class of the persistent object to load is determined by the class
* of $pObject.
*
* @throws ezcPersistentException if the object is not available.
* @throws ezcPersistentException if $pObject is not of a valid persistent object type.
* @param object $pObject
* @param int $id
* @return void
*/
public function loadIntoObject( $pObject, $id )
{
if( !is_int( $id ) )
{
throw new ezcPersistentObjectException( "The parameter 'id' was not a valid integer" );
}
$def = $this->manager->fetchDefinition( get_class( $pObject ) ); // propagate exception
$q = $this->db->createSelectQuery();
$q->select( '*' )->from( $def->table )
->where( $q->expr->eq( $def->idProperty->columnName, $id ) );
$stmt = $q->prepare();
if( !$stmt->execute() )
{
// SQL error, need some sort of special exception for this.
// and there should be way to retrieve the error
// for debugging
throw new ezcPersistentObjectException( "An internal SQL query failed." );
}
$row = $stmt->fetch( PDO::FETCH_ASSOC );
if( $row !== false ) // we got a result
{
// we could check if there was more than one result here
try{
$state = $this->rowToStateArray( $row, $def );
} catch( Exception $e ){
// rethrow, todo
throw new ezcPersistentObjectException( "The row data could not be correctly converted to set data. Most probably there is something wrong with a custom rowToStateArray implementation" );
}
$pObject->setState( $state );
}
else
{
$class = get_class( $pObject );
throw new ezcPersistentObjectException( "No such object $class with id $id" );
}
}
/**
* Syncronizes the contents of $pObject with those in the database.
*
* Note that calling this method is equavalent with calling
* loadIntoObject on $pObject with the id of $pObject. Any
* changes made to $pObject will be discarded.
*
* @throws ezcPersistentException if $pObject is not of a valid persistent object type.
* @throws ezcPersistentException if $pObject is not persistent already
* @param object $pObject.
* @return void
*/
public function refresh( $pObject )
{
$def = $this->manager->fetchDefinition( get_class( $pObject ) ); // propagate exception
$state = $pObject->getState();
$idValue = $state[$def->idProperty->propertyName];
if( $idValue !== null )
{
$this->loadIntoObject( $pObject, $idValue );
}
else
{
$class = get_class( $pObject );
throw new ezcPersistentObjectException( "Can't refresh non persistent object of type $class" );
}
}
/**
* Saves the new persistent object $pObject to the database using an INSERT INTO query.
*
* The correct ID is set to $pObject.
*
* @throws ezcPersistentException if $pObject is not of a valid persistent object type.
* @throws ezcPersistentException if $pObject is already stored to the database.
* @param object $pObject
* @return void
*/
public function save( $pObject )
{
$def = $this->manager->fetchDefinition( get_class( $pObject ) );// propagate exception
$state = $pObject->getState();
$idValue = $state[$def->idProperty->propertyName];
// check that this object is stored to db already
if( $idValue !== null )
{
$class = get_class( $pObject );
throw new ezcPersistentObjectException( "Save() was called on the type $class which is already persistent with the id: $idValue" );
}
// set up and execute the query
$q = $this->db->createInsertQuery();
$q->insertInto( $def->table );
foreach( $state as $name => $value )
{
if( $name != $def->idProperty->propertyName ) // skip the id field
{
// set each of the properties
$q->set( $def->properties[$name]->columnName, $q->bindValue( $value ) );
}
}
$stmt = $q->prepare();
$stmt->execute();
if( $stmt->errorCode() != 0 )
{
throw new ezcPersistentObjectException( "The insert query failed." );
}
// fetch the newly created id, and set it to the object
// todo: pgsql requires sequence here. This must be stored in the
// definition somewhere
$state[$def->idProperty->propertyName] = (int)$this->db->lastInsertId();
$pObject->setState( $state );
}
/**
* Saves or update the persistent object $pObject to the database.
*
* If the object is a new object an INSERT INTO query will be executed. If the
* object is persistent already it will be updated with an UPDATE query.
*
* @throws ezcPersistentException if $pObject is not of a valid persistent object type.
* @throws ezcPersistentException if any of the definition requirements are not met.
* @param object $pObject
* @return void
*/
public function saveOrUpdate( $pObject )
{
$def = $this->manager->fetchDefinition( get_class( $pObject ) );// propagate exception
$state = $pObject->getState();
$idValue = $state[$def->idProperty->propertyName];
if( $idValue === null )
{
$this->save( $pObject );
}
else
{
$this->update( $pObject );
}
}
/**
* Saves the new persistent object $pObject to the database using an UPDATE query.
*
* @throws ezcPersistentException if $pObject is not of a valid persistent object type.
* @throws ezcPersistentException if $pObject is not stored in the database already.
* @throws ezcPersistentException if any of the definition requirements are not met.
* @param object $pObject
* @return void
*/
public function update( $pObject )
{
$def = $this->manager->fetchDefinition( get_class( $pObject ) ); // propagate exception
$state = $pObject->getState();
$idValue = $state[$def->idProperty->propertyName];
// check that this object is stored to db already
if( $idValue < 1 )
{
throw new ezcPersistentObjectException( "Update() was called on an object that is not stored in the database already. Use save() or saveOrUpdate() for this." );
}
// set up and execute the query
$q = $this->db->createUpdateQuery();
$q->update( $def->table );
foreach( $state as $name => $value )
{
if( $name != $def->idProperty->propertyName ) // skip the id field
{
// set each of the properties
$q->set( $def->properties[$name]->columnName, $q->bindValue( $value ) );
}
}
$q->where( $q->expr->eq( $def->idProperty->columnName, $q->bindValue( $idValue ) ) );
$stmt = $q->prepare();
$stmt->execute();
if( $stmt->errorCode() != 0 )
{
throw new ezcPersistentObjectException( "The update query failed." );
}
}
// ignore this for now
/*
* Goes through the requirements of the array $state and and check that the requirements
* set in $def are met.
*
* Currently this method checks if a field marked as required is set to null.
* If this is the case this method will replace it with the default value if present.
* If it is not possible to meet the requirements an exception is thrown.
*
* @param array $row
* @param ezcPersistentDefinition $def
* @return void
*/
// protected function meetRequirements( array &$state, ezcPersistentObjectDefinition $def )
// {
// foreach( $state as $key => $value )
// }
/**
* Returns the the row $row retrieved from PDO transformed into a state array
* that can be used to set the state on a persistent object.
*
* $def holds the definition of the persistent object the $row maps to.
*
* The most basic task is to transform the database column names into
* property names.
*
* Reimplementations could add additional functionality like transformation of
* data.
*
* @throws ezcPersistentException if a fatal error occured during the transformation
* @param array $row
* @param ezcPersistentDefinition $def
* @return array
*/
protected function rowToStateArray( array $row, ezcPersistentObjectDefinition $def )
{
$result = array();
foreach( $row as $key => $value )
{
// todo: everything in $row is of type string
// should we convert to the correct PHP type?
if( $key == $def->idProperty->columnName )
{
$result[$def->idProperty->propertyName] = $value;
}
else
{
$result[$def->columns[$key]->propertyName] = $value;
}
}
return $result;
}
}
?>