* 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->idProperty->generator = new ezcPersistentGeneratorDefinition( 'ezcPersistentSequenceGenerator', * array( /*'sequence' => 'person_sequence'* / ) ); * * $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; } /** * 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 ezcPersistentDefinitionNotFoundxception if $the object is not recognized as a persistent object. * @throws ezcPersistentObjectNotPersistentException if the object is not persistent already. * @throws ezcPersistentQueryException 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 ezcPersistentObjectNotPersistentException( $class ); } // create and execute query $q = $this->db->createDeleteQuery(); $q->deleteFrom( $def->table ) ->where( $q->expr->eq( $def->idProperty->columnName, $q->bindValue( $idValue ) ) ); try { $stmt = $q->prepare(); $stmt->execute(); if( $stmt->errorCode() != 0 ) { throw new ezcPersistentQueryException( "The delete query failed." ); } } catch( PDOException $e ) { throw new ezcPersistentQueryException( $e->getMessage() ); } } /* * 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: * * $q = $session->createDeleteQuery(); * $q->where( $q->expr->gt( 'age', 15 ) ); * $session->deleteFromQuery( $q ); * * * @throws ezcPersistentObjectException if there is no such persistent class. * @param string $class * @return ezcQueryDelete */ public function createDeleteQuery( $class ) { $def = $this->manager->fetchDefinition( $class ); // propagate exception // init query $q = $this->db->createDeleteQuery(); $q->setAliases( $this->generateAliasMap( $def ) ); $q->deleteFrom( $def->table ); return $q; } /* * 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. * * @throws ezcPersistentQueryException if the delete query failed. * @param ezcQueryDelete $query * @return void */ public function deleteFromQuery( ezcQueryDelete $query ) { try { $stmt = $query->prepare(); $stmt->execute(); } catch( PDOException $e ) { throw new ezcPersistentQueryException( $e->getMessage() ); } } /* * 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: * * * * @throws ezcPersistentDefinitionNotFoundException if there is no such persistent class. * @param string $class * @return ezcQueryUpdate */ public function createUpdateQuery( $class ) { $def = $this->manager->fetchDefinition( $class ); // propagate exception // init query $q = $this->db->createUpdateQuery(); $q->setAliases( $this->generateAliasMap( $def ) ); $q->update( $def->table ); return $q; } /* * 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. * * @throws ezcPersistentQueryException if the update query failed. * @param ezcQueryUpdate $query * @return void */ public function updateFromQuery( ezcQueryUpdate $query ) { try { $stmt = $query->prepare(); $stmt->execute(); } catch( PDOException $e ) { throw new ezcPersistentQueryException( $e->getMessage() ); } } /** * 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->setAliases( $this->generateAliasMap( $def ) ); $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 ezcPersistentDefinitionNotFoundException if there is no such persistent class. * @throws ezcPersistentQueryException if the find query failed * @param ezcQuerySelect $query * @param string $class * @return array(object) */ public function find( ezcQuerySelect $query, $class ) { $def = $this->manager->fetchDefinition( $class ); // propagate exception try { $stmt = $query->prepare(); $stmt->execute(); $rows = $stmt->fetchAll( PDO::FETCH_ASSOC ); }catch( PDOException $e ) { throw new ezcPersistentQueryException( $e->getMessage() ); } // convert all the rows states and then objects $result = array(); foreach( $rows as $row ) { $object = new $def->class; $object->setState( ezcPersistentStateTransformer::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(). * * @throws ezcPersistentDefinitionNotFoundException if there is no such persistent class. * @throws ezcPersistentQueryException if the find query failed * @param ezcQuerySelect $query * @return Iterator */ public function findIterator( ezcQuerySelect $query, $class ) { $def = $this->manager->fetchDefinition( $class ); // propagate exception try { $stmt = $query->prepare(); $stmt->execute(); }catch( PDOException $e ) { throw new ezcPersistentQueryException( $e->getMessage() ); } return new ezcPersistentFindIterator( $stmt, $def ); } /** * 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 ezcPersistentDefinitionNotFoundException if $pObject is not of a valid persistent object type. * @throws ezcPersistentQueryException if the find query failed * @param object $pObject * @param int $id * @return void */ public function loadIntoObject( $pObject, $id ) { if( !is_int( $id ) ) { throw new ezcPersistentQueryException( "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 ) ); try { $stmt = $q->prepare(); $stmt->execute(); }catch( PDOException $e ) { throw new ezcPersistentQueryException( $e->getMessage() ); } $row = $stmt->fetch( PDO::FETCH_ASSOC ); if( $row !== false ) // we got a result { // we could check if there was more than one result here // but we don't because of the overhead and since the Persistent // Object would be faulty by design in that case and the user would have // to execute custom code to get into an invalid state. try{ $state = ezcPersistentStateTransformer::rowToStateArray( $row, $def ); } catch( Exception $e ){ 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 ezcPersistentQueryException( "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 * @throws ezcPersistentException if the select query failed. * @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 ezcPersistentObjectNotPersistentException( $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. * @throws ezcPersistentException if it was not possible to generate a unique identifier for the new object * @throws ezcPersistentException if the insert query failed. * @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 ezcPersistentObjectAlreadyPersistentException( $class ); } // 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 ) ); } } // fetch the id generator $idGenerator = null; if( class_exists( $def->idProperty->generator->class ) ) { $idGenerator = new $def->idProperty->generator->class; if( !( $idGenerator instanceof ezcPersistentIdentifierGenerator ) ) { throw new ezcPersistentIdentifierGenerationException( get_class( $pObject ), "Could not initialize identifier generator: ". "{$def->idProperty->generator->class} ." ); } } $this->db->beginTransaction(); // let presave id generator do its work $idGenerator->preSave( $def, $this->db, $q ); // execute the insert query try { $stmt = $q->prepare(); $stmt->execute(); } catch( PDOException $e ) { throw new ezcPersistentObjectException( "The insert query failed." ); } // fetch the newly created id, and set it to the object $id = $idGenerator->postSave( $def, $this->db ); if( $id === null ) { $this->db->rollback(); throw new ezcPersistentIdentifierGenerationException( $def->class ); } // everything seems to be fine, lets commit the queries to the database // and update the object with its newly created id. $this->db->commit(); $state[$def->idProperty->propertyName] = $id; $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. * @throws ezcPersistentException if the insert or update query failed. * @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 ezcPersistentDefinitionNotFoundException if $pObject is not of a valid persistent object type. * @throws ezcPersistentObjectNotPersistentException if $pObject is not stored in the database already. * @throws ezcPersistentQueryException * @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 ezcPersistentObjectNotPersistentException( get_class( $pObject ) ); } // 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 ) ) ); try { $stmt = $q->prepare(); $stmt->execute(); }catch( PDOException $e ) { throw new ezcPersistentQueryException( $e->getMessage() ); } } // 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 a hash map between property and column name for the given definition $def. * * The alias map can be used with the query classes. * * @param ezcPersistentObjectDefinition $def * @return array(string=>string) */ private function generateAliasMap( ezcPersistentObjectDefinition $def ) { $table = array(); foreach( $def->properties as $prop ) { $table[$prop->propertyName] = $prop->columnName; } $table[$def->class] = $def->table; return $table; } } ?>