From 40930c4b403aa30bd62bc320d1199df0e5c94a85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20M=C3=B6nch?= Date: Sun, 22 Jan 2017 12:09:34 +0100 Subject: [PATCH] Moved createEntity into EntityFactory --- UPGRADE.md | 24 +- src/SimpleThings/EntityAudit/AuditReader.php | 264 +---------------- .../EntityAudit/EntityFactory.php | 277 ++++++++++++++++++ 3 files changed, 306 insertions(+), 259 deletions(-) create mode 100644 src/SimpleThings/EntityAudit/EntityFactory.php diff --git a/UPGRADE.md b/UPGRADE.md index d4fb9b11..13ca9a64 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -3,7 +3,7 @@ ## BC BREAK: Current user name resolution -Previously the username that was recorded againsts revisions was resolved by `SimpleThings\EntityAudit\Request\CurrentUserListener` (``simplethings_entityaudit.request.current_user_listener` service). +Previously the username that was recorded against revisions was resolved by `SimpleThings\EntityAudit\Request\CurrentUserListener` (``simplethings_entityaudit.request.current_user_listener` service). This has been removed and replaced with `SimpleThings\EntityAudit\User\TokenStorageUsernameCallable`. @@ -23,4 +23,24 @@ simple_things_entity_audit: username_callable: simplethings_entityaudit.username_callable.token_storage ``` -The above after configuration is the default and does not need setting explicitly. \ No newline at end of file +The above after configuration is the default and does not need setting explicitly. + +## BC BREAK: +Following methods has been removed: +```php +AuditReader::setLoadAuditedCollections($loadAuditedCollections) +AuditReader::setLoadAuditedEntities($loadAuditedEntities) +AuditReader::setLoadNativeCollections($loadNativeCollections) +AuditReader::setLoadNativeEntities($loadNativeEntities) +``` + +And with with $options arguments at AuditReader::__c'tor or AuditReader::find. + +```php +$auditReader->find(Foo::class, 1, 1, [ + AuditReader::LOAD_AUDITED_COLLECTIONS => false, + AuditReader::LOAD_AUDITED_ENTITIES => false, + AuditReader::LOAD_NATIVE_COLLECTIONS => false, + AuditReader::LOAD_NATIVE_ENTITIES => false, +]); +``` \ No newline at end of file diff --git a/src/SimpleThings/EntityAudit/AuditReader.php b/src/SimpleThings/EntityAudit/AuditReader.php index 6e83b5f2..659409cc 100644 --- a/src/SimpleThings/EntityAudit/AuditReader.php +++ b/src/SimpleThings/EntityAudit/AuditReader.php @@ -23,16 +23,13 @@ namespace SimpleThings\EntityAudit; -use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Util\ClassUtils; use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\ORM\Mapping\QuoteStrategy; -use Doctrine\ORM\PersistentCollection; use Doctrine\ORM\Query; -use SimpleThings\EntityAudit\Collection\AuditedCollection; use SimpleThings\EntityAudit\Exception\DeletedException; use SimpleThings\EntityAudit\Exception\InvalidRevisionException; use SimpleThings\EntityAudit\Exception\NoRevisionFoundException; @@ -87,11 +84,9 @@ class AuditReader private $quoteStrategy; /** - * Entity cache to prevent circular references - * - * @var array + * @var EntityFactory */ - private $entityCache; + private $entityFactory; /** * @param EntityManagerInterface $em @@ -111,53 +106,9 @@ public function __construct( $this->platform = $this->em->getConnection()->getDatabasePlatform(); $this->quoteStrategy = $this->em->getConfiguration()->getQuoteStrategy(); - $this->options = array_merge([ - self::LOAD_AUDITED_COLLECTIONS => true, - self::LOAD_AUDITED_ENTITIES => true, - self::LOAD_NATIVE_COLLECTIONS => true, - self::LOAD_NATIVE_ENTITIES => true, - ], $options); - } - - /** - * @deprecated use $options arguments - * - * @param boolean $loadAuditedCollections - */ - public function setLoadAuditedCollections($loadAuditedCollections) - { - $this->options[self::LOAD_AUDITED_COLLECTIONS] = $loadAuditedCollections; - } - - /** - * @deprecated use $options arguments - * - * @param boolean $loadAuditedEntities - */ - public function setLoadAuditedEntities($loadAuditedEntities) - { - $this->options[self::LOAD_AUDITED_ENTITIES] = $loadAuditedEntities; - } - - /** - * @deprecated use $options arguments - * - * @param boolean $loadNativeCollections - */ - public function setLoadNativeCollections($loadNativeCollections) - { - $this->options[self::LOAD_NATIVE_COLLECTIONS] = $loadNativeCollections; + $this->entityFactory = new EntityFactory($this, $em, $factory, $options); } - /** - * @deprecated use $options arguments - * - * @param boolean $loadNativeEntities - */ - public function setLoadNativeEntities($loadNativeEntities) - { - $this->options[self::LOAD_NATIVE_ENTITIES] = $loadNativeEntities; - } /** * @return \Doctrine\DBAL\Connection @@ -180,7 +131,7 @@ public function getConfiguration() */ public function clearEntityCache() { - $this->entityCache = []; + $this->entityFactory->clearEntityCache(); } /** @@ -319,170 +270,7 @@ public function find($className, $id, $revision, array $options = array()) unset($row[$this->config->getRevisionTypeFieldName()]); - return $this->createEntity($class->name, $columnMap, $row, $revision, $options); - } - - /** - * Simplified and stolen code from UnitOfWork::createEntity. - * - * @param string $className - * @param array $columnMap - * @param array $data - * @param int $revision - * @param array $options - * - * @return object - * - * @throws DeletedException - * @throws NoRevisionFoundException - * @throws NotAuditedException - * @throws \Doctrine\DBAL\DBALException - * @throws \Doctrine\ORM\Mapping\MappingException - * @throws \Doctrine\ORM\ORMException - * @throws \Exception - */ - private function createEntity($className, array $columnMap, array $data, $revision, array $options = []) - { - $options = array_merge($this->options, $options); - - /** @var ClassMetadataInfo|ClassMetadata $classMetadata */ - $classMetadata = $this->em->getClassMetadata($className); - $cacheKey = $this->createEntityCacheKey($classMetadata, $data, $revision); - - if (isset($this->entityCache[$cacheKey])) { - return $this->entityCache[$cacheKey]; - } - - if (! $classMetadata->isInheritanceTypeNone()) { - if (! isset($data[$classMetadata->discriminatorColumn['name']])) { - throw new \RuntimeException('Expecting discriminator value in data set.'); - } - $discriminator = $data[$classMetadata->discriminatorColumn['name']]; - if (! isset($classMetadata->discriminatorMap[$discriminator])) { - throw new \RuntimeException("No mapping found for [{$discriminator}]."); - } - - if ($classMetadata->discriminatorValue) { - $entity = $this->em->getClassMetadata($classMetadata->discriminatorMap[$discriminator])->newInstance(); - } else { - //a complex case when ToOne binding is against AbstractEntity having no discriminator - $pk = array(); - - foreach ($classMetadata->identifier as $field) { - $pk[$classMetadata->getColumnName($field)] = $data[$field]; - } - - return $this->find($classMetadata->discriminatorMap[$discriminator], $pk, $revision); - } - } else { - $entity = $classMetadata->newInstance(); - } - - //cache the entity to prevent circular references - $this->entityCache[$cacheKey] = $entity; - - $connection = $this->getConnection(); - foreach ($data as $field => $value) { - if (isset($classMetadata->fieldMappings[$field])) { - $value = $connection->convertToPHPValue($value, $classMetadata->fieldMappings[$field]['type']); - $classMetadata->reflFields[$field]->setValue($entity, $value); - } - } - - foreach ($classMetadata->associationMappings as $field => $assoc) { - // Check if the association is not among the fetch-joined associations already. - if (isset($hints['fetched'][$className][$field])) { - continue; - } - - /** @var ClassMetadataInfo|ClassMetadata $targetClass */ - $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); - $isAudited = $this->metadataFactory->isAudited($assoc['targetEntity']); - - if ($assoc['type'] & ClassMetadata::TO_ONE) { - $value = null; - - if ($isAudited && $options[self::LOAD_AUDITED_ENTITIES]) { - // Primary Key. Used for audit tables queries. - $pk = array(); - // Primary Field. Used when fallback to Doctrine finder. - $pf = array(); - - if ($assoc['isOwningSide']) { - foreach ($assoc['targetToSourceKeyColumns'] as $foreign => $local) { - $pk[$foreign] = $pf[$foreign] = $data[$columnMap[$local]]; - } - } else { - /** @var ClassMetadataInfo|ClassMetadata $otherEntityMeta */ - $otherEntityAssoc = $this->em->getClassMetadata($assoc['targetEntity'])->associationMappings[$assoc['mappedBy']]; - - foreach ($otherEntityAssoc['targetToSourceKeyColumns'] as $local => $foreign) { - $pk[$foreign] = $pf[$otherEntityAssoc['fieldName']] = $data[$classMetadata->getFieldName($local)]; - } - } - - $pk = array_filter($pk); - - if (! empty($pk)) { - try { - $value = $this->find($targetClass->name, $pk, $revision, array_merge( - $options, - ['threatDeletionsAsExceptions' => true] - )); - } catch (DeletedException $e) { - $value = null; - } catch (NoRevisionFoundException $e) { - // The entity does not have any revision yet. So let's get the actual state of it. - $value = $this->em->getRepository($targetClass->name)->findOneBy($pf); - } - } - } elseif (! $isAudited && $options[self::LOAD_NATIVE_ENTITIES]) { - if ($assoc['isOwningSide']) { - $associatedId = array(); - foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) { - $joinColumnValue = isset($data[$columnMap[$srcColumn]]) ? $data[$columnMap[$srcColumn]] : null; - if ($joinColumnValue !== null) { - $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue; - } - } - - if (! empty($associatedId)) { - $value = $this->em->getReference($targetClass->name, $associatedId); - } - } else { - // Inverse side of x-to-one can never be lazy - $value = $this->getEntityPersister($assoc['targetEntity']) - ->loadOneToOneEntity($assoc, $entity); - } - } - - $classMetadata->reflFields[$field]->setValue($entity, $value); - } elseif ($assoc['type'] & ClassMetadata::ONE_TO_MANY) { - $collection = new ArrayCollection(); - - if ($isAudited && $options[self::LOAD_AUDITED_COLLECTIONS]) { - $foreignKeys = array(); - foreach ($targetClass->associationMappings[$assoc['mappedBy']]['sourceToTargetKeyColumns'] as $local => $foreign) { - $field = $classMetadata->getFieldForColumn($foreign); - $foreignKeys[$local] = $classMetadata->reflFields[$field]->getValue($entity); - } - - $collection = new AuditedCollection($this, $targetClass, $assoc, $foreignKeys, $revision); - } elseif (! $isAudited && $options[self::LOAD_NATIVE_COLLECTIONS]) { - $collection = new PersistentCollection($this->em, $targetClass, new ArrayCollection()); - - $this->getEntityPersister($assoc['targetEntity']) - ->loadOneToManyCollection($assoc, $entity, $collection); - } - - $classMetadata->reflFields[$assoc['fieldName']]->setValue($entity, $collection); - } else { - // Inject collection - $classMetadata->reflFields[$field]->setValue($entity, new ArrayCollection()); - } - } - - return $entity; + return $this->entityFactory->createEntity($class->name, $columnMap, $row, $revision, $options); } /** @@ -597,7 +385,7 @@ public function findEntitiesChangedAtRevision($revision) $id[$idField] = $row[$idField]; } - $entity = $this->createEntity($className, $columnMap, $row, $revision); + $entity = $this->entityFactory->createEntity($className, $columnMap, $row, $revision); $changedEntities[] = new ChangedEntity( $className, $id, @@ -737,16 +525,6 @@ public function getCurrentRevision($className, $id) return $queryBuilder->execute()->fetchColumn(); } - /** - * @param object $entity - * - * @return \Doctrine\ORM\Persisters\Entity\EntityPersister - */ - protected function getEntityPersister($entity) - { - return $this->em->getUnitOfWork()->getEntityPersister($entity); - } - /** * Get an array with the differences of between two specific revisions of * an object with a given id. @@ -898,7 +676,7 @@ public function getEntityHistory($className, $id) $rev = $row[$revisionFieldName]; unset($row[$revisionFieldName]); - $result[] = $this->createEntity($class->name, $columnMap, $row, $rev); + $result[] = $this->entityFactory->createEntity($class->name, $columnMap, $row, $rev); } return $result; @@ -917,32 +695,4 @@ private function createRevision(array $row) $row['username'] ); } - - /** - * @param ClassMetadata $classMetadata - * @param array $data - * @param int $revision - * - * @return string - */ - private function createEntityCacheKey(ClassMetadata $classMetadata, array $data, $revision) - { - //lookup revisioned entity cache - $keyParts = array(); - - foreach ($classMetadata->getIdentifierFieldNames() as $name) { - if ($classMetadata->hasAssociation($name)) { - if ($classMetadata->isSingleValuedAssociation($name)) { - $name = $classMetadata->getSingleAssociationJoinColumnName($name); - } else { - // Doctrine should throw a mapping exception if an identifier - // that is an association is not single valued, but just in case. - throw new \RuntimeException('Multiple valued association identifiers not supported'); - } - } - $keyParts[] = $data[$name]; - } - - return $classMetadata->name . '_' . implode('_', $keyParts) . '_' . $revision; - } } diff --git a/src/SimpleThings/EntityAudit/EntityFactory.php b/src/SimpleThings/EntityAudit/EntityFactory.php new file mode 100644 index 00000000..6a73cd3a --- /dev/null +++ b/src/SimpleThings/EntityAudit/EntityFactory.php @@ -0,0 +1,277 @@ +auditReader = $auditReader; + $this->em = $em; + $this->metadataFactory = $metadataFactory; + + $this->options = array_merge([ + AuditReader::LOAD_AUDITED_COLLECTIONS => true, + AuditReader::LOAD_AUDITED_ENTITIES => true, + AuditReader::LOAD_NATIVE_COLLECTIONS => true, + AuditReader::LOAD_NATIVE_ENTITIES => true, + ], $options); + } + + /** + * Simplified and stolen code from UnitOfWork::createEntity. + * + * @param string $className + * @param array $columnMap + * @param array $data + * @param int $revision + * @param array $options + * + * @return object + * + * @throws DeletedException + * @throws NoRevisionFoundException + * @throws NotAuditedException + * @throws \Doctrine\DBAL\DBALException + * @throws \Doctrine\ORM\Mapping\MappingException + * @throws \Doctrine\ORM\ORMException + * @throws \Exception + */ + public function createEntity($className, array $columnMap, array $data, $revision, array $options = []) + { + $options = array_merge($this->options, $options); + + $classMetadata = $this->em->getClassMetadata($className); + $cacheKey = $this->createEntityCacheKey($classMetadata, $data, $revision); + + if (isset($this->entityCache[$cacheKey])) { + return $this->entityCache[$cacheKey]; + } + + if (! $classMetadata->isInheritanceTypeNone()) { + if (! isset($data[$classMetadata->discriminatorColumn['name']])) { + throw new \RuntimeException('Expecting discriminator value in data set.'); + } + $discriminator = $data[$classMetadata->discriminatorColumn['name']]; + if (! isset($classMetadata->discriminatorMap[$discriminator])) { + throw new \RuntimeException("No mapping found for [{$discriminator}]."); + } + + if ($classMetadata->discriminatorValue) { + $entity = $this->em->getClassMetadata($classMetadata->discriminatorMap[$discriminator])->newInstance(); + } else { + //a complex case when ToOne binding is against AbstractEntity having no discriminator + $pk = array(); + + foreach ($classMetadata->identifier as $field) { + $pk[$classMetadata->getColumnName($field)] = $data[$field]; + } + + return $this->auditReader->find($classMetadata->discriminatorMap[$discriminator], $pk, $revision); + } + } else { + $entity = $classMetadata->newInstance(); + } + + //cache the entity to prevent circular references + $this->entityCache[$cacheKey] = $entity; + + $connection = $this->em->getConnection(); + foreach ($data as $field => $value) { + if (isset($classMetadata->fieldMappings[$field])) { + $value = $connection->convertToPHPValue($value, $classMetadata->fieldMappings[$field]['type']); + $classMetadata->reflFields[$field]->setValue($entity, $value); + } + } + + foreach ($classMetadata->associationMappings as $field => $assoc) { + // Check if the association is not among the fetch-joined associations already. + if (isset($hints['fetched'][$className][$field])) { + continue; + } + + $targetClass = $this->em->getClassMetadata($assoc['targetEntity']); + $isAudited = $this->metadataFactory->isAudited($assoc['targetEntity']); + + if ($assoc['type'] & ClassMetadata::TO_ONE) { + $value = null; + + if ($isAudited && $options[AuditReader::LOAD_AUDITED_ENTITIES]) { + // Primary Key. Used for audit tables queries. + $pk = array(); + // Primary Field. Used when fallback to Doctrine finder. + $pf = array(); + + if ($assoc['isOwningSide']) { + foreach ($assoc['targetToSourceKeyColumns'] as $foreign => $local) { + $pk[$foreign] = $pf[$foreign] = $data[$columnMap[$local]]; + } + } else { + $otherEntityAssoc = $this->em->getClassMetadata($assoc['targetEntity']) + ->associationMappings[$assoc['mappedBy']]; + + foreach ($otherEntityAssoc['targetToSourceKeyColumns'] as $local => $foreign) { + $pk[$foreign] = $pf[$otherEntityAssoc['fieldName']] = $data[$classMetadata->getFieldName($local)]; + } + } + + $pk = array_filter($pk); + + if (! empty($pk)) { + try { + $value = $this->auditReader->find($targetClass->name, $pk, $revision, array_merge( + $options, + ['threatDeletionsAsExceptions' => true] + )); + } catch (DeletedException $e) { + $value = null; + } catch (NoRevisionFoundException $e) { + // The entity does not have any revision yet. So let's get the actual state of it. + $value = $this->em->getRepository($targetClass->name)->findOneBy($pf); + } + } + } elseif (! $isAudited && $options[AuditReader::LOAD_NATIVE_ENTITIES]) { + if ($assoc['isOwningSide']) { + $associatedId = array(); + foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) { + $joinColumnValue = isset($data[$columnMap[$srcColumn]]) ? $data[$columnMap[$srcColumn]] : null; + if ($joinColumnValue !== null) { + $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue; + } + } + + if (! empty($associatedId)) { + $value = $this->em->getReference($targetClass->name, $associatedId); + } + } else { + // Inverse side of x-to-one can never be lazy + $value = $this->getEntityPersister($assoc['targetEntity']) + ->loadOneToOneEntity($assoc, $entity); + } + } + + $classMetadata->reflFields[$field]->setValue($entity, $value); + } elseif ($assoc['type'] & ClassMetadata::ONE_TO_MANY) { + $collection = new ArrayCollection(); + + if ($isAudited && $options[AuditReader::LOAD_AUDITED_COLLECTIONS]) { + $foreignKeys = array(); + foreach ($targetClass->associationMappings[$assoc['mappedBy']]['sourceToTargetKeyColumns'] as $local => $foreign) { + $field = $classMetadata->getFieldForColumn($foreign); + $foreignKeys[$local] = $classMetadata->reflFields[$field]->getValue($entity); + } + + $collection = new AuditedCollection( + $this->auditReader, + $targetClass, + $assoc, + $foreignKeys, + $revision + ); + } elseif (! $isAudited && $options[AuditReader::LOAD_NATIVE_COLLECTIONS]) { + $collection = new PersistentCollection($this->em, $targetClass, new ArrayCollection()); + + $this->getEntityPersister($assoc['targetEntity']) + ->loadOneToManyCollection($assoc, $entity, $collection); + } + + $classMetadata->reflFields[$assoc['fieldName']]->setValue($entity, $collection); + } else { + // Inject collection + $classMetadata->reflFields[$field]->setValue($entity, new ArrayCollection()); + } + } + + return $entity; + } + + /** + * Clears entity cache. Call this if you are fetching subsequent revisions using same AuditManager. + */ + public function clearEntityCache() + { + $this->entityCache = []; + } + + /** + * @param object $entity + * + * @return \Doctrine\ORM\Persisters\Entity\EntityPersister + */ + private function getEntityPersister($entity) + { + return $this->em->getUnitOfWork()->getEntityPersister($entity); + } + + /** + * @param ClassMetadata $classMetadata + * @param array $data + * @param int $revision + * + * @return string + */ + private function createEntityCacheKey(ClassMetadata $classMetadata, array $data, $revision) + { + $keyParts = array(); + + foreach ($classMetadata->getIdentifierFieldNames() as $name) { + if ($classMetadata->hasAssociation($name)) { + if ($classMetadata->isSingleValuedAssociation($name)) { + $name = $classMetadata->getSingleAssociationJoinColumnName($name); + } else { + // Doctrine should throw a mapping exception if an identifier + // that is an association is not single valued, but just in case. + throw new \RuntimeException('Multiple valued association identifiers not supported'); + } + } + $keyParts[] = $data[$name]; + } + + return $classMetadata->name . '_' . implode('_', $keyParts) . '_' . $revision; + } +} \ No newline at end of file