openstackweb/openstack/code/utils/DDD/infrastructure/UnitOfWork.php
2014-10-31 16:59:18 -03:00

441 lines
16 KiB
PHP

<?php
/**
* Copyright 2014 Openstack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
/**
* Class UnitOfWork
*/
final class UnitOfWork {
/**
* @var UnitOfWork
*/
private static $instance;
private function __construct(){}
private function __clone(){}
private $new_entities_identity_map = array();
private $delete_entities_identity_map = array();
private $update_entities_identity_map = array();
private $loaded_collections_many_2_many_identity_map = array();
private $loaded_collections_one_2_many_identity_map = array();
private $loaded_many_2_one_identity_map = array();
private $identity_map = array();
/**
* @return UnitOfWork
*/
public static function getInstance(){
if(!is_object(self::$instance)){
self::$instance = new UnitOfWork();
}
return self::$instance;
}
public function loadMany2OneAssociation(Many2OneAssociation $association){
$owner_key = spl_object_hash($association->getOwner());
$association_key = md5(sprintf('%s_%s',$owner_key,$association->getName()));
if(!array_key_exists($owner_key,$this->loaded_many_2_one_identity_map)){
$this->loaded_many_2_one_identity_map[$owner_key] = array();
}
$associations = $this->loaded_many_2_one_identity_map[$owner_key];
$associations[$association_key] = $association;
$this->loaded_many_2_one_identity_map[$owner_key] = $associations;
}
public function getMany2OneAssociation(IEntity $owner, $name){
$owner_key = spl_object_hash($owner);
$association_key = md5(sprintf('%s_%s',$owner_key,$name));
if(array_key_exists($owner_key,$this->loaded_many_2_one_identity_map)){
$associations = $this->loaded_many_2_one_identity_map[$owner_key];
if(array_key_exists($association_key,$associations)){
return $associations[$association_key];
}
}
return false;
}
public function getCollection(IEntity $owner, $child_class,QueryObject $query, $type){
$owner_key = spl_object_hash($owner);
$query_key = md5(sprintf("%s_%s_%s", $query->__toString() , implode(',',$query->getAlias()), implode(',',$query->getOrder())));
$collection_key = md5(sprintf('%s_%s_%s',$owner_key,$child_class,$query_key));
$collections = array();
if($type == '1-to-many') {
if(array_key_exists($owner_key,$this->loaded_collections_one_2_many_identity_map)){
$collections = $this->loaded_collections_one_2_many_identity_map[$owner_key];
}
}
else{
if(array_key_exists($owner_key,$this->loaded_collections_many_2_many_identity_map)){
$collections = $this->loaded_collections_many_2_many_identity_map[$owner_key];
}
}
if(array_key_exists($collection_key,$collections)){
return $collections[$collection_key];
}
return false;
}
public function loadCollection(PersistentCollection $collection){
$info = $collection->getComponentInfo();
$owner_key = spl_object_hash($info['ownerObj']);
$query = $collection->getQuery();
$query_key = md5(sprintf("%s_%s_%s", $query->__toString() , implode(',',$query->getAlias()),implode(',',$query->getOrder())));
$collection_key = md5(sprintf('%s_%s_%s',$owner_key,$info['childClass'],$query_key));
if($info['type'] == '1-to-many') {
if(!array_key_exists($owner_key,$this->loaded_collections_one_2_many_identity_map)){
$this->loaded_collections_one_2_many_identity_map[$owner_key] = array();
}
$collections = $this->loaded_collections_one_2_many_identity_map[$owner_key];
$collections[$collection_key] = $collection;
$this->loaded_collections_one_2_many_identity_map[$owner_key] = $collections;
}
else{
if(!array_key_exists($owner_key,$this->loaded_collections_many_2_many_identity_map)){
$this->loaded_collections_many_2_many_identity_map[$owner_key] = array();
}
$collections = $this->loaded_collections_many_2_many_identity_map[$owner_key];
$collections[$collection_key] = $collection;
$this->loaded_collections_many_2_many_identity_map[$owner_key] = $collections;
}
}
public function getCollectionsForEntity(IEntity $entity){
$owner_key = spl_object_hash($entity);
if(array_key_exists($owner_key,$this->loaded_collections_one_2_many_identity_map)){
$one_2_many = $this->loaded_collections_one_2_many_identity_map[$owner_key];
}
if(array_key_exists($owner_key,$this->loaded_collections_many_2_many_identity_map)){
$many_2_many = $this->loaded_collections_many_2_many_identity_map[$owner_key];
}
return array($one_2_many,$many_2_many);
}
public function scheduleForInsert(IEntity $entity){
$key = spl_object_hash($entity);
$this->new_entities_identity_map[$key] = $entity;
}
public function scheduleForUpdate(IEntity $entity){
$key = spl_object_hash($entity);
if (isset($this->delete_entities_identity_map[$key])) {
throw new Exception('Entity is removed');
}
if (!isset($this->new_entities_identity_map[$key])) {
$this->update_entities_identity_map[$key] = $entity;
}
}
/**
* @param string $class_name
* @param int $id
* @return null|IEntity
*/
public function getFromCache($class_name, $id){
if(array_key_exists($class_name,$this->identity_map)){
$cache = $this->identity_map[$class_name];
if(array_key_exists($id,$cache))
return $cache[$id];
}
return null;
}
public function setToCache(IEntity $entity){
$id = $entity->getIdentifier();
$class_name = get_class($entity);
if(!array_key_exists($class_name,$this->identity_map))
$this->identity_map[$class_name] = array();
$this->identity_map[$class_name][$id] = $entity;
}
public function scheduleForDelete(IEntity $entity){
$key = spl_object_hash($entity);
if (isset($this->new_entities_identity_map[$key])) {
unset($this->new_entities_identity_map[$key]);
//do nothing
return;
}
if (isset($this->update_entities_identity_map[$key])) {
unset($this->update_entities_identity_map[$key]);
}
$this->delete_entities_identity_map[$key] = $entity;
}
private function isMarkedDirtyAtTargetSide(Many2OneAssociation $association){
$target = $association->getTarget();
$collection_name = $association->getInversedBy();
$query = $association->getTargetCriteria();
$child_class = get_class($association->getOwner());
$one_to_many = $this->getCollection($target,$child_class,$query,'1-to-many');
$many_to_many = $this->getCollection($target,$child_class,$query,'many-to-many');
$collection = $one_to_many?$one_to_many:$many_to_many;
if($collection && $collection->isDirty()){
return true;
}
return false;
}
/**
* @param array $extraFields
* @return array
* @throws Exception
*/
private function processExtraFields(array $extraFields){
$single_values = array();
$multi_values = array();
$multi_values_qty = 0;
$queries_set = array();
//iterate over metadata and figure which are single values and which ones multi ones
foreach($extraFields as $name => $values) {
if(count($values)>1){
//if we got a multi value then store it, so we can post process it
$multi_values[$name] = $values;
//check if multi value set has the same items qty than others, iow,
//if we have several multi values sets, each one should have the same qty of items, otherwise , throw error
if($multi_values_qty!=0 && $multi_values_qty !=count($values))
throw Exception('Multi values extra fields must be set on equally quantity!');
//store count
$multi_values_qty = count($values);
}
else{
//store single value row
$single_values[$name] = $values[0];
}
}
if($multi_values_qty>0){
//if we have multi values sets , then iterate over it
//and convert on rows to flatten the repetitive group structure
foreach($multi_values as $name => $values){
$i=0;
foreach ($values as $value) {
if((count($queries_set)-1) < $i){
array_push($queries_set,array());
}
$row = $queries_set[$i];
$row[$name] = $value;
$queries_set[$i] = $row;
$i++;
}
}
//once we processed and flatened structure, add the single values
//to each new row
if(count($single_values)>0){
foreach($queries_set as $row){
foreach($single_values as $name=>$value){
$row[$name] = $value;
}
}
}
}
else{
//if we have only single values, then create a single row with those values
$queries_set[0] = $single_values;
}
return $queries_set;
}
/**
* Commits the UnitOfWork, executing all operations that have been postponed
* up to this point. The state of all managed entities will be synchronized with
* the database.
*
* The operations are executed in the following order:
*
* 1) All entity insertions
* 2) All entity updates
* 3) All collection deletions
* 4) All collection updates
* 5) All entity deletions
*
*
* @return void
*
* @throws \Exception
*/
public function commit(){
foreach($this->loaded_many_2_one_identity_map as $key => $associations){
//if marked for delete, then skip...
if(array_key_exists($key, $this->delete_entities_identity_map)) continue;
//if owner its already marked for update or insert , then skip write
$delay_entity_update = array_key_exists($key,$this->new_entities_identity_map) ||
array_key_exists($key,$this->update_entities_identity_map);
foreach($associations as $association_key => $association){
$owner = $association->getOwner();
if($association->isInverseSide()){
//if association is reverse side, dont change from here, and check if target
//is already marked to update...
$delay_entity_update = $this->isMarkedDirtyAtTargetSide($association);
if($delay_entity_update){
$key = spl_object_hash($owner);
unset($this->new_entities_identity_map[$key]);
unset($this->update_entities_identity_map[$key]);
}
continue;
}
if(!$association->isDirty()) continue;
//update ownership
$join_field = $association->getName().'ID';
$target = $association->getTarget();
//if child does not exists, then create it
if($target->ID == 0){
$key = spl_object_hash($target);
unset($this->new_entities_identity_map[$key]);
unset($this->update_entities_identity_map[$key]);
$target->write();
}
$owner->$join_field = $target->ID;
if(!$delay_entity_update){
//mark as dirty...
if($owner->exists())
$this->scheduleForUpdate($owner);
else
$this->scheduleForInsert($owner);
$delay_entity_update = true;
}
}
}
foreach($this->new_entities_identity_map as $key => $entity){
$entity->write();
}
foreach($this->update_entities_identity_map as $key => $entity){
//is marked for deletion, skip
if(array_key_exists($key,$this->delete_entities_identity_map)) continue;
//is not changed, skip ...
if(!$entity->isChanged()) continue;
$entity->write();
}
foreach($this->loaded_collections_one_2_many_identity_map as $owner_key => $collections){
foreach($collections as $collection_key => $collection){
if($collection->isDirty()){
$info = $collection->getComponentInfo();
//get deleted ones...
$to_delete = $collection->getDeleteDiff();
if(count($to_delete)>0){
foreach($to_delete as $entity){
$entity->delete();
}
}
//get inserted ones
$to_insert = $collection->getInsertDiff();
if(count($to_insert)>0){
foreach($to_insert as $entity){
$entity->$info['joinField'] = $info['ownerObj']->getIdentifier();
$entity->write();
}
}
//get changed ones
$to_update = $collection->getDirties();
foreach($to_update as $entity){
$entity->write();
}
}
}
}
foreach($this->loaded_collections_many_2_many_identity_map as $owner_key => $collections){
foreach($collections as $collection_key => $collection){
if($collection->isDirty()){
$info = $collection->getComponentInfo();
$parentField = $info['joinField'];
$childField = ($info['childClass'] == $info['ownerClass']) ? "ChildID" : ($info['localField']);
$owner_id = $info['ownerObj']->ID == 0 ? $info['ownerObj']->OldID:$info['ownerObj']->ID;
//get deleted ones...
$to_delete = $collection->getDeleteDiff();
//list of queries to execute
$queries_2_exec = array();
if(count($to_delete)>0){
foreach($to_delete as $entity){
array_push($queries_2_exec,"DELETE FROM \"{$info['tableName']}\" WHERE \"$parentField\" = {$owner_id} AND \"$childField\" = {$entity->getIdentifier()}" );
}
}
//get inserted ones
$to_insert = $collection->getInsertDiff();
if(count($to_insert)>0){
foreach($to_insert as $entity){
//if we have extra fields (data that will be added to join table as relationship metatdata...)
if($extraFields = $collection->getExtraFields($entity)){
foreach($this->processExtraFields($extraFields) as $row){
$extraKeys = $extraValues = '';
foreach($row as $name => $value){
$extraKeys .= ", \"$name\"";
$extraValues .= ", '" . Convert::raw2sql($value) . "'";
}
array_push($queries_2_exec,"INSERT INTO \"{$info['tableName']}\" (\"$parentField\",\"$childField\" $extraKeys) VALUES ({$info['ownerObj']->getIdentifier()}, {$entity->getIdentifier()} $extraValues)");
}
}//end of extra fields...
else
array_push($queries_2_exec,"INSERT INTO \"{$info['tableName']}\" (\"$parentField\",\"$childField\") VALUES ({$info['ownerObj']->getIdentifier()}, {$entity->getIdentifier()})");
}
}
$to_update = $collection->getUpdateDiff();
if(count($to_update)>0){
foreach($to_update as $entity){
if($extraFields = $collection->getExtraFields($entity)){
array_push($queries_2_exec,"DELETE FROM \"{$info['tableName']}\" WHERE \"$parentField\" = {$owner_id} AND \"$childField\" = {$entity->getIdentifier()}" );
foreach($this->processExtraFields($extraFields) as $row){
$extraKeys = $extraValues = '';
foreach($row as $name => $value){
$extraKeys .= ", \"$name\"";
$extraValues .= ", '" . Convert::raw2sql($value) . "'";
}
array_push($queries_2_exec,"INSERT INTO \"{$info['tableName']}\" (\"$parentField\",\"$childField\" $extraKeys) VALUES ({$info['ownerObj']->getIdentifier()}, {$entity->getIdentifier()} $extraValues)");
}
}
}
}
//execute all stacked queries so far..
foreach($queries_2_exec as $query_2_exec){
DB::query($query_2_exec);
}
//get changed ones
$to_update = $collection->getDirties();
foreach($to_update as $entity){
$entity->write();
}
}
}
}
// Entity deletions come last and need to be in reverse commit order
$this->delete_entities_identity_map = array_reverse($this->delete_entities_identity_map, true);
foreach($this->delete_entities_identity_map as $key => $entity){
$entity->delete();
}
// Clear up
$this->loaded_many_2_one_identity_map =
$this->new_entities_identity_map =
$this->update_entities_identity_map =
$this->loaded_collections_one_2_many_identity_map =
$this->loaded_collections_many_2_many_identity_map =
$this->delete_entities_identity_map = array();
}
}