| Current File : /home/jvzmxxx/wiki/extensions/Flow/includes/Data/ObjectManager.php |
<?php
namespace Flow\Data;
use Flow\DbFactory;
use Flow\Exception\DataModelException;
use Flow\Exception\FlowException;
use Flow\Model\UUID;
use SplObjectStorage;
/**
* ObjectManager orchestrates the storage of a single type of objects.
* Where ObjectLocator handles querying, ObjectManager extends that to
* add persistence.
*
* The ObjectManager has two required constructor dependencies:
* * An ObjectMapper instance that can convert back and forth from domain
* objects to database rows
* * An ObjectStorage implementation that implements persistence.
*
* Additionally there are two optional constructor arguments:
* * A set of Index objects that listen to life cycle events and maintain
* an up-to date cache of all objects. Individual Index objects typically
* answer a single set of query arguments.
* * A set of LifecycleHandler implementations that are notified about
* insert, update, remove and load events.
*
* A simple ObjectManager instances might be created as such:
*
* $om = new Flow\Data\ObjectManager(
* Flow\Data\Mapper\BasicObjectMapper::model( 'MyModelClass' ),
* new Flow\Data\Storage\BasicDbStorage(
* $dbFactory,
* 'my_model_table',
* array( 'my_primary_key' )
* )
* );
*
* Objects of MyModelClass can be stored:
*
* $om->put( $object );
*
* Objects can be retrieved via my_primary_key
*
* $object = $om->get( $pk );
*
* The object can be updated by calling ObjectManager:put at
* any time. If the object is to be deleted:
*
* $om->remove( $object );
*
* The data cached in the indexes about this object can be cleared
* with:
*
* $om->cachePurge( $object );
*
* In addition to the single-use put, get and remove there are also multi
* variants named multiPut, mulltiGet and multiRemove. They perform the
* same operation as their namesake but with fewer network operations when
* dealing with multiple objects of the same type.
*
* @todo Information about Indexes and LifecycleHandlers
*/
class ObjectManager extends ObjectLocator {
/**
* @var SplObjectStorage $loaded Maps from a php object to the database
* row that was used to create it. One use of this is to toggle between
* self::insert and self::update when self::put is called.
*/
protected $loaded;
/**
* @param ObjectMapper $mapper Convert to/from database rows/domain objects.
* @param ObjectStorage $storage Implements persistence(typically sql)
* @param Index[] $indexes Specialized listeners that cache rows and can respond
* to queries
* @param LifecycleHandler[] $lifecycleHandlers Listeners for insert, update,
* remove and load events.
*/
public function __construct(
ObjectMapper $mapper,
ObjectStorage $storage,
DbFactory $dbFactory,
array $indexes = array(),
array $lifecycleHandlers = array()
) {
parent::__construct( $mapper, $storage, $dbFactory, $indexes, $lifecycleHandlers );
// This needs to be SplObjectStorage rather than using spl_object_hash for keys
// in a normal array because if the object gets GC'd spl_object_hash can reuse
// the value. Stuffing the object as well into SplObjectStorage prevents GC.
$this->loaded = new SplObjectStorage;
}
/**
* Clear the internal cache of which objects have been loaded so far.
*
* Objects that were loaded prior to clearing the object manager must
* not use self::put until they have been merged via self::merge or
* an insert operation will be performed.
*/
public function clear() {
$this->loaded = new SplObjectStorage;
$this->mapper->clear();
foreach ( $this->lifecycleHandlers as $handler ) {
$handler->onAfterClear();
}
}
/**
* Merge an object loaded from outside the object manager for update.
* Without merge using self::put will trigger an insert operation.
*
* @var object $object
*/
public function merge( $object ) {
if ( !isset( $this->loaded[$object] ) ) {
$this->loaded[$object] = $this->mapper->toStorageRow( $object );
}
}
/**
* Purge all cached data related to this object.
*
* @param object $object
*/
public function cachePurge( $object ) {
if ( !isset( $this->loaded[$object] ) ) {
throw new FlowException( 'Object was not loaded through this object manager, use ObjectManager::merge if necessary' );
}
$row = $this->loaded[$object];
foreach ( $this->indexes as $index ) {
$index->cachePurge( $object, $row );
}
}
/**
* Persist a single object to storage.
*
* @var object $object
* @var array $metadata Additional information about the object for
* listeners to operate on.
*/
public function put( $object, array $metadata = array() ) {
$this->multiPut( array( $object ), $metadata );
}
/**
* Persist multiple objects to storage.
*
* @var object[] $objects
* @var array $metadata Additional information about the object for
* listeners to operate on.
*/
public function multiPut( array $objects, array $metadata = array() ) {
$updateObjects = array();
$insertObjects = array();
foreach( $objects as $object ) {
if ( isset( $this->loaded[$object] ) ) {
$updateObjects[] = $object;
} else {
$insertObjects[] = $object;
}
}
if ( count( $updateObjects ) ) {
$this->update( $updateObjects, $metadata );
}
if ( count( $insertObjects ) ) {
$this->insert( $insertObjects, $metadata );
}
}
/**
* Remove an object from persistent storage.
*
* @var object $object
* @var array $metadata Additional information about the object for
* listeners to operate on.
*/
public function remove( $object, array $metadata = array() ) {
if ( !isset( $this->loaded[$object] ) ) {
throw new FlowException( 'Object was not loaded through this object manager, use ObjectManager::merge if necessary' );
}
$old = $this->loaded[$object];
$old = $this->mapper->normalizeRow( $old );
$this->storage->remove( $old );
foreach ( $this->lifecycleHandlers as $handler ) {
$handler->onAfterRemove( $object, $old, $metadata );
}
unset( $this->loaded[$object] );
}
/**
* Remove multiple objects from persistent storage.
*
* @var object[] $objects
* @var array $metadata
*/
public function multiRemove( $objects, array $metadata ) {
foreach ( $objects as $obj ) {
$this->remove( $obj, $metadata );
}
}
/**
* Return a string value that can be provided to self::find or self::findMulti
* as the offset-id option to facilitate pagination.
*
* @param object $object
* @param array $sortFields
* @return string
*/
public function serializeOffset( $object, array $sortFields ) {
$offsetFields = array();
// @todo $row = $this->loaded[$object] ?
$row = $this->mapper->toStorageRow( $object );
// @todo Why not self::splitFromRow?
foreach( $sortFields as $field ) {
$value = $row[$field];
if ( is_string( $value )
&& strlen( $value ) === UUID::BIN_LEN
&& substr( $field, -3 ) === '_id'
) {
$value = UUID::create( $value );
}
if ( $value instanceof UUID ) {
$value = $value->getAlphadecimal();
}
$offsetFields[] = $value;
}
return implode( '|', $offsetFields );
}
/**
* Insert new objects into storage.
*
* @param object[] $objects
* @param array $metadata
*/
protected function insert( array $objects, array $metadata ) {
$rows = array_map( array( $this->mapper, 'toStorageRow' ), $objects );
$storedRows = $this->storage->insert( $rows );
if ( !$storedRows ) {
throw new DataModelException( 'failed insert', 'process-data' );
}
$numObjects = count( $objects );
for( $i = 0; $i < $numObjects; ++$i ) {
$object = $objects[$i];
$stored = $storedRows[$i];
// Propagate stuff that was added to the row by storage back
// into the object. Currently intended for storage URLs etc,
// but may in the future also bring in auto-ids and so on.
$this->mapper->fromStorageRow( $stored, $object );
foreach ( $this->lifecycleHandlers as $handler ) {
$handler->onAfterInsert( $object, $stored, $metadata );
}
$this->loaded[$object] = $stored;
}
}
/**
* Update the set of objects representation within storage.
*
* @param object[] $objects
* @param array $metadata
*/
protected function update( array $objects, array $metadata ) {
foreach( $objects as $object ) {
$this->updateSingle( $object, $metadata );
}
}
/**
* Update a single objects representation within storage.
*
* @param object $object
* @param array $metadata
*/
protected function updateSingle( $object, array $metadata ) {
$old = $this->loaded[$object];
$old = $this->mapper->normalizeRow( $old );
$new = $this->mapper->toStorageRow( $object );
if ( self::arrayEquals( $old, $new ) ) {
return;
}
$this->storage->update( $old, $new );
foreach ( $this->lifecycleHandlers as $handler ) {
$handler->onAfterUpdate( $object, $old, $new, $metadata );
}
$this->loaded[$object] = $new;
}
/**
* {@inheritDoc}
*/
protected function load( $row ) {
$object = parent::load( $row );
$this->loaded[$object] = $row;
return $object;
}
/**
* Compare two arrays for equality.
* @todo why not $x === $y ?
*
* @param array $old
* @param array $new
* @return bool
*/
static public function arrayEquals( array $old, array $new ) {
return array_diff_assoc( $old, $new ) === array()
&& array_diff_assoc( $new, $old ) === array();
}
/**
* Convert the input argument into an array. This is preferred
* over casting with (array)$value because that will cast an
* object to an array rather than wrap it.
*
* @param mixed $input
*
* @return array
*/
static public function makeArray( $input ) {
if ( is_array( $input ) ) {
return $input;
} else {
return array( $input );
}
}
/**
* Return an array containing all the top level changes between
* $old and $new. Expects $old and $new to be representations of
* database rows and contain only strings and numbers.
*
* It does not validate that it is a legal update (See DbStorage->calcUpdates).
*
* @param array $old
* @param array $new
* @return array
*/
static public function calcUpdatesWithoutValidation( array $old, array $new ) {
$updates = array();
foreach ( array_keys( $new ) as $key ) {
/*
* $old[$key] and $new[$key] could both be the same value going into the same
* column, but represented as different data type here: one could be a string
* and another an int, of even an object (e.g. Blob)
* What we should be comparing is their "value", regardless of the data type
* (different between them doesn't matter here, both are for the same database
* column), so I'm casting them to string before performing comparison.
*/
if ( !array_key_exists( $key, $old ) || (string) $old[$key] !== (string) $new[$key] ) {
$updates[$key] = $new[$key];
}
unset( $old[$key] );
}
// These keys don't exist in $new
foreach ( array_keys( $old ) as $key ) {
$updates[$key] = null;
}
return $updates;
}
/**
* Separate a set of keys from an array. Returns null if not
* all keys are set.
*
* @param array $row
* @param array $keys
* @return array|null
*/
static public function splitFromRow( array $row, array $keys ) {
$split = array();
foreach ( $keys as $key ) {
if ( !isset( $row[$key] ) ) {
return null;
}
$split[$key] = $row[$key];
}
return $split;
}
}