From 3cc01dd1444d2a622273bb8142f5a83e5e8608fd Mon Sep 17 00:00:00 2001 From: kaleta Date: Fri, 17 Jun 2022 13:20:45 +0200 Subject: [PATCH] speed up hydration a bit --- Hydrator/ReadOnlyHydrator.php | 157 +++++++-------- Hydrator/SimpleObjectHydrator.php | 312 +++++++++++++++--------------- 2 files changed, 218 insertions(+), 251 deletions(-) mode change 100644 => 100755 Hydrator/ReadOnlyHydrator.php mode change 100644 => 100755 Hydrator/SimpleObjectHydrator.php diff --git a/Hydrator/ReadOnlyHydrator.php b/Hydrator/ReadOnlyHydrator.php old mode 100644 new mode 100755 index 46ec8de..8b7c9dc --- a/Hydrator/ReadOnlyHydrator.php +++ b/Hydrator/ReadOnlyHydrator.php @@ -9,29 +9,20 @@ class ReadOnlyHydrator extends SimpleObjectHydrator { - const HYDRATOR_NAME = 'readOnly'; + public const HYDRATOR_NAME = 'readOnly'; - /** @var string[] */ - protected $proxyFilePathsCache = []; + protected array $proxyFilePathsCache = []; + protected array $proxyNamespacesCache = []; + protected array $proxyClassNamesCache = []; + protected ?string $proxyDirectoryCache = null; - /** @var string[] */ - protected $proxyNamespacesCache = []; - - /** @var string[] */ - protected $proxyClassNamesCache = []; - - /** - * @param ClassMetadata $classMetaData - * @param array $data - * @return mixed - * @throws \Exception - */ - protected function createEntity(ClassMetadata $classMetaData, array $data) + protected function createEntity(ClassMetadata $classMetaData, array $data): object { $className = $this->getEntityClassName($classMetaData, $data); - $this->generateProxyFile($classMetaData, $data); + $proxyFilePath = $this->generateProxyFile($classMetaData, $data); + + require_once($proxyFilePath); - require_once($this->getProxyFilePath($className)); $proxyClassName = $this->getProxyNamespace($className) . '\\' . $this->getProxyClassName($className); $entity = new $proxyClassName(array_keys($data)); @@ -40,103 +31,95 @@ protected function createEntity(ClassMetadata $classMetaData, array $data) return $entity; } - /** - * @param ClassMetadata $classMetaData - * @param array $data - * @return $this - */ - protected function generateProxyFile(ClassMetadata $classMetaData, array $data) + protected function generateProxyFile(ClassMetadata $classMetaData, array $data): string { $entityClassName = $this->getEntityClassName($classMetaData, $data); + $proxyFilePath = $this->getProxyFilePath($entityClassName); - if (file_exists($proxyFilePath) === false) { - $proxyMethodsCode = implode("\n\n", $this->getPhpForProxyMethods($classMetaData, $entityClassName)); - $proxyNamespace = $this->getProxyNamespace($entityClassName); - $proxyClassName = $this->getProxyClassName($entityClassName); - $generator = static::class; - $readOnlyInterface = ReadOnlyEntityInterface::class; - - $php = <<getPhpForProxyMethods($classMetaData, $entityClassName)); + + $proxyNamespace = $this->getProxyNamespace($entityClassName); + $proxyClassName = $this->getProxyClassName($entityClassName); + + $generator = static::class; + $readOnlyInterface = ReadOnlyEntityInterface::class; + + $php = <<loadedProperties = \$loadedProperties; - } +public function __construct(array \$loadedProperties) +{ + \$this->loadedProperties = \$loadedProperties; +} $proxyMethodsCode - public function isReadOnlyPropertiesLoaded(array \$properties) - { - \$return = true; - foreach (\$properties as \$property) { - if (in_array(\$property, \$this->loadedProperties) === false) { - \$return = false; - break; - } +public function isReadOnlyPropertiesLoaded(array \$properties) +{ + \$return = true; + foreach (\$properties as \$property) { + if (in_array(\$property, \$this->loadedProperties) === false) { + \$return = false; + break; } - - return \$return; } - public function assertReadOnlyPropertiesAreLoaded(array \$properties) - { - foreach (\$properties as \$property) { - if (in_array(\$property, \$this->loadedProperties) === false) { - throw new \steevanb\DoctrineReadOnlyHydrator\Exception\PropertyNotLoadedException(\$this, \$property); - } + return \$return; +} + +public function assertReadOnlyPropertiesAreLoaded(array \$properties) +{ + foreach (\$properties as \$property) { + if (in_array(\$property, \$this->loadedProperties) === false) { + throw new \steevanb\DoctrineReadOnlyHydrator\Exception\PropertyNotLoadedException(\$this, \$property); } } } +} PHP; - file_put_contents($proxyFilePath, $php); - } + file_put_contents($proxyFilePath, $php); - return $this; + return $proxyFilePath; } - /** - * @param string $entityClassName - * @return string - */ - public function getProxyFilePath($entityClassName) + public function getProxyFilePath(string $entityClassName): string { - if (isset($this->proxyFilePathsCache[$entityClassName]) === false) { + if (!isset($this->proxyFilePathsCache[$entityClassName])) { $fileName = str_replace('\\', '_', $entityClassName) . '.php'; - $this->proxyFilePathsCache[$entityClassName] = $this->getProxyDirectory() . DIRECTORY_SEPARATOR . $fileName; + + if($this->proxyDirectoryCache === null) { + $this->proxyDirectoryCache = $this->getProxyDirectory(); + } + + $this->proxyFilePathsCache[$entityClassName] = $this->proxyDirectoryCache . DIRECTORY_SEPARATOR . $fileName; } return $this->proxyFilePathsCache[$entityClassName]; } - /** - * @param string $entityClassName - * @return string - */ - protected function getProxyNamespace($entityClassName) + protected function getProxyNamespace(string $entityClassName): string { if (isset($this->proxyNamespacesCache[$entityClassName]) === false) { - $this->proxyNamespacesCache[$entityClassName] = - 'ReadOnlyProxies\\' . substr($entityClassName, 0, strrpos($entityClassName, '\\')); + $this->proxyNamespacesCache[$entityClassName] = 'ReadOnlyProxies\\' . substr($entityClassName, 0, strrpos($entityClassName, '\\')); } return $this->proxyNamespacesCache[$entityClassName]; } - /** - * @param string $entityClassName - * @return string - */ - protected function getProxyClassName($entityClassName) + protected function getProxyClassName(string $entityClassName): string { if (isset($this->proxyClassNamesCache[$entityClassName]) === false) { $this->proxyClassNamesCache[$entityClassName] = @@ -150,17 +133,15 @@ protected function getProxyClassName($entityClassName) * As Doctrine\ORM\EntityManager::newHydrator() call new FooHydrator($this), we can't set parameters to Hydrator. * So, we will use proxyDirectory from Doctrine\Common\Proxy\AbstractProxyFactory. * It's directory used by Doctrine\ORM\Internal\Hydration\ObjectHydrator. - * - * @return string */ - protected function getProxyDirectory() + protected function getProxyDirectory(): string { /** @var ProxyGenerator $proxyGenerator */ $proxyGenerator = $this->getPrivatePropertyValue($this->_em->getProxyFactory(), 'proxyGenerator'); - $directory = $this->getPrivatePropertyValue($proxyGenerator, 'proxyDirectory'); + $readOnlyDirectory = $directory . DIRECTORY_SEPARATOR . 'ReadOnly'; - if (is_dir($readOnlyDirectory) === false) { + if (!is_dir($readOnlyDirectory)) { mkdir($readOnlyDirectory, 0775, true); } @@ -200,13 +181,7 @@ protected function getUsedProperties(\ReflectionMethod $reflectionMethod, $prope return array_keys($return); } - /** - * @param ClassMetadata $classMetaData - * @param string $entityClassName - * @return array - * @throws PrivateMethodShouldNotAccessPropertiesException - */ - protected function getPhpForProxyMethods(ClassMetadata $classMetaData, $entityClassName) + protected function getPhpForProxyMethods(ClassMetadata $classMetaData, string $entityClassName): array { $return = []; $reflectionClass = new \ReflectionClass($entityClassName); @@ -333,7 +308,7 @@ protected function getPhpForParameter(\ReflectionParameter $parameter) } else { $types = [$types]; } - + $values = []; $hasNull = false; $needsNull = false; @@ -399,7 +374,7 @@ protected function getFullQualifiedClassName($className) { return '\\' . ltrim($className, '\\'); } - + /** * @param \ReflectionType $reflectionType * @return string diff --git a/Hydrator/SimpleObjectHydrator.php b/Hydrator/SimpleObjectHydrator.php old mode 100644 new mode 100755 index 30b66e3..8cd5af6 --- a/Hydrator/SimpleObjectHydrator.php +++ b/Hydrator/SimpleObjectHydrator.php @@ -6,49 +6,47 @@ use Doctrine\ORM\Internal\Hydration\ArrayHydrator; use Doctrine\ORM\Internal\HydrationCompleteHandler; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\ClassMetadataInfo; class SimpleObjectHydrator extends ArrayHydrator { - const HYDRATOR_NAME = 'simpleObject'; - const READ_ONLY_PROPERTY = '__SIMPLE_OBJECT_HYDRATOR__READ_ONLY__'; + public const HYDRATOR_NAME = 'simpleObject'; + public const READ_ONLY_PROPERTY = '__SIMPLE_OBJECT_HYDRATOR__READ_ONLY__'; - /** @var string */ - protected $rootClassName; + protected ?string $rootClassName = null; + protected array $newEntityReflectionCache = []; + protected array $reflectionPropertyCache = []; + protected array $enumAttributeCache = []; + protected array $entityClassNameCache = []; - protected function prepare() + protected function prepare(): void { parent::prepare(); $this->rootClassName = null; } - protected function cleanup() + protected function cleanup(): void { parent::cleanup(); $this->_uow->hydrationComplete(); } - /** - * @return array - */ - protected function hydrateAllData() + protected function hydrateAllData(): array { $arrayResult = parent::hydrateAllData(); $readOnlyResult = []; if (is_array($arrayResult)) { foreach ($arrayResult as $data) { - $readOnlyResult[] = $this->doHydrateRowData($this->getRootclassName(), $data); + $readOnlyResult[] = $this->doHydrateRowData($this->getRootClassName(), $data); } } return $readOnlyResult; } - /** - * @return string - */ - protected function getRootclassName() + protected function getRootClassName(): string { // i don't understand when we can have more than one item in ArrayHydrator::$_rootAliases // so, i assume first one is the right one @@ -60,13 +58,21 @@ protected function getRootclassName() return $this->rootClassName; } - /** - * @param string $className - * @param array $data - * @return object - * @throws \Exception - */ - protected function doHydrateRowData($className, array $data) + protected function getReflectionClassProperty(ClassMetadata $classMetaData, string $property): ?\ReflectionProperty + { + $cacheKey = spl_object_id($classMetaData).$property; + if(!isset($this->reflectionPropertyCache[$cacheKey])) { + try { + $this->reflectionPropertyCache[$cacheKey] = $classMetaData->getReflectionClass()->getProperty($property); + } catch (\ReflectionException) { + $this->reflectionPropertyCache[$cacheKey] = null; + } + } + + return $this->reflectionPropertyCache[$cacheKey]; + } + + protected function doHydrateRowData(string $className, array $data): object { $classMetaData = $this->_em->getClassMetadata($className); $mappings = $classMetaData->getAssociationMappings(); @@ -74,79 +80,88 @@ protected function doHydrateRowData($className, array $data) foreach ($data as $name => $value) { if (isset($mappings[$name]) && is_array($value)) { - switch ($mappings[$name]['type']) { - case ClassMetadata::ONE_TO_ONE: - $value = $this->hydrateOneToOne($mappings[$name], $value); - break; - case ClassMetadata::ONE_TO_MANY: - $value = $this->hydrateOneToMany($mappings[$name], $value); - break; - case ClassMetadata::MANY_TO_ONE: - $value = $this->hydrateManyToOne($mappings[$name], $value); - break; - case ClassMetadata::MANY_TO_MANY: - $value = $this->hydrateManyToMany($mappings[$name], $value); - break; - default: - throw new \Exception('Unknow mapping type "' . $mappings[$name]['type'] . '".'); - } + $value = match ($mappings[$name]['type']) { + ClassMetadataInfo::ONE_TO_ONE => $this->hydrateOneToOne($mappings[$name], $value), + ClassMetadataInfo::ONE_TO_MANY => $this->hydrateOneToMany($mappings[$name], $value), + ClassMetadataInfo::MANY_TO_ONE => $this->hydrateManyToOne($mappings[$name], $value), + ClassMetadataInfo::MANY_TO_MANY => $this->hydrateManyToMany($mappings[$name], $value), + default => throw new \Exception('Unknow mapping type "' . $mappings[$name]['type'] . '".'), + }; } - if ( - $classMetaData->inheritanceType === ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE - || $classMetaData->inheritanceType === ClassMetadata::INHERITANCE_TYPE_JOINED - ) { - try { - $property = $classMetaData->getReflectionClass()->getProperty($name); - } catch (\ReflectionException $e) { - continue; - } - } else { - $property = $classMetaData->getReflectionClass()->getProperty($name); + $property = $this->getReflectionClassProperty($classMetaData, $name); + if($property === null) { + continue; } if ($property->isPublic()) { $entity->$name = $value; - } else { - /** @var \UnitEnum|\BackedEnum $enumClass */ - $enumClass = null; - foreach ($property->getAttributes() as $attribute) { - $enumClass = $attribute->getArguments()['enumType'] ?? null; - if($enumClass !== null) { - break; - } - } + continue; + } - if($enumClass !== null) { - $enumInterfaces = array_keys(class_implements($enumClass)); + $enumValue = $this->getEnumForValue($classMetaData, $property, $value); + if($enumValue !== null) { + $value = $enumValue; + } + + //$property->setAccessible(true); + $property->setValue($entity, $value); + //$property->setAccessible(false); + } + + return $entity; + } - if(in_array(\BackedEnum::class, $enumInterfaces)) { - $value = $enumClass::tryFrom($value); - } elseif (in_array(\UnitEnum::class, $enumInterfaces)) { - $value = $enumClass::$value; - } + protected function getEnumForValue(ClassMetadata $classMetaData, \ReflectionProperty $property, mixed $value): \UnitEnum|\BackedEnum|null + { + $cacheKey = spl_object_id($classMetaData).$property->name; + if(!isset($this->enumAttributeCache[$cacheKey])) { + + /** @var \UnitEnum|\BackedEnum $enumClass */ + $enumClass = null; + foreach ($property->getAttributes() as $attribute) { + $enumClass = $attribute->getArguments()['enumType'] ?? null; + if ($enumClass !== null) { + break; } + } - $property->setAccessible(true); - $property->setValue($entity, $value); - $property->setAccessible(false); + if($enumClass === null) { + return $this->enumAttributeCache[$cacheKey] = null; } + + $enumInterfaces = array_keys(class_implements($enumClass)); + + $this->enumAttributeCache[$cacheKey] = [ + 'class' => $enumClass, + 'interfaces' => $enumInterfaces, + ]; } - return $entity; + if($this->enumAttributeCache[$cacheKey] === null) { + return null; + } + + if (in_array(\BackedEnum::class, $this->enumAttributeCache[$cacheKey]['interfaces'], true)) { + return $this->enumAttributeCache[$cacheKey]['class']::tryFrom($value); + } + + if (in_array(\UnitEnum::class, $this->enumAttributeCache[$cacheKey]['interfaces'], true)) { + return $this->enumAttributeCache[$cacheKey]['class']::$value; + } + + return null; } - /** - * @param ClassMetadata $classMetaData - * @param array $data - * @return mixed - * @throws \Exception - */ - protected function createEntity(ClassMetadata $classMetaData, array $data) + protected function createEntity(ClassMetadata $classMetaData, array $data): object { - $className = $this->getEntityClassName($classMetaData, $data); - $reflection = new \ReflectionClass($className); - $entity = $reflection->newInstanceWithoutConstructor(); + $cacheKey = spl_object_id($classMetaData); + if(!isset($this->newEntityReflectionCache[$cacheKey])) { + $className = $this->getEntityClassName($classMetaData, $data); + $this->newEntityReflectionCache[$cacheKey] = new \ReflectionClass($className); + } + + $entity = $this->newEntityReflectionCache[$cacheKey]->newInstanceWithoutConstructor(); $entity->{static::READ_ONLY_PROPERTY} = true; $this->deferPostLoadInvoking($classMetaData, $entity); @@ -154,12 +169,7 @@ protected function createEntity(ClassMetadata $classMetaData, array $data) return $entity; } - /** - * @param ClassMetadata $classMetaData - * @param object $entity - * @return $this - */ - protected function deferPostLoadInvoking(ClassMetadata $classMetaData, $entity) + protected function deferPostLoadInvoking(ClassMetadata $classMetaData, object $entity): self { /** @var HydrationCompleteHandler $handler */ $handler = $this->getPrivatePropertyValue($this->_uow, 'hydrationCompleteHandler'); @@ -168,113 +178,95 @@ protected function deferPostLoadInvoking(ClassMetadata $classMetaData, $entity) return $this; } - /** - * @param ClassMetadata $classMetaData - * @param array $data - * @return string - * @throws \Exception - */ - protected function getEntityClassName(ClassMetadata $classMetaData, array $data) + protected function resolveClassMetadataInheritance(ClassMetadata $classMetaData, array $data): string { - switch ($classMetaData->inheritanceType) { - case ClassMetadata::INHERITANCE_TYPE_NONE: - $return = $classMetaData->name; - break; - case ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE: - case ClassMetadata::INHERITANCE_TYPE_JOINED: - if (isset($data[$classMetaData->discriminatorColumn['name']]) === false) { - $exception = 'Discriminator column "' . $classMetaData->discriminatorColumn['name'] . '" '; - $exception .= 'for "' . $classMetaData->name . '" does not exists in $data.'; - throw new \Exception($exception); - } - $discriminator = $data[$classMetaData->discriminatorColumn['name']]; - $return = $classMetaData->discriminatorMap[$discriminator]; - break; - default: - throw new \Exception('Unknow inheritance type "' . $classMetaData->inheritanceType . '".'); + if (isset($data[$classMetaData->discriminatorColumn['name']]) === false) { + $exception = 'Discriminator column "' . $classMetaData->discriminatorColumn['name'] . '" '; + $exception .= 'for "' . $classMetaData->name . '" does not exists in $data.'; + throw new \Exception($exception); + } + + $discriminator = $data[$classMetaData->discriminatorColumn['name']]; + return $classMetaData->discriminatorMap[$discriminator]; + } + + protected function getEntityClassName(ClassMetadata $classMetaData, array $data): string + { + $cacheKey = spl_object_id($classMetaData); + if(!isset($this->entityClassNameCache[$cacheKey])) { + $this->entityClassNameCache[$cacheKey] = match ($classMetaData->inheritanceType) { + ClassMetadataInfo::INHERITANCE_TYPE_NONE => $classMetaData->name, + + ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_TABLE, + ClassMetadataInfo::INHERITANCE_TYPE_JOINED => $this->resolveClassMetadataInheritance($classMetaData, $data), + + default => throw new \Exception('Unknow inheritance type "' . $classMetaData->inheritanceType . '".'), + }; } - return $return; + return $this->entityClassNameCache[$cacheKey]; } - /** - * @param array $mapping - * @param array $data - * @return ArrayCollection - */ - protected function hydrateOneToOne(array $mapping, $data) + protected function hydrateOneToOne(array $mapping, mixed $data): object { return $this->doHydrateRowData($mapping['targetEntity'], $data); } - /** - * @param array $mapping - * @param array $data - * @return ArrayCollection - */ - protected function hydrateOneToMany(array $mapping, $data) + protected function hydrateOneToMany(array $mapping, mixed $data): ArrayCollection { - $entities = []; + $entities = new ArrayCollection(); foreach ($data as $key => $linkedData) { - $entities[$key] = $this->doHydrateRowData($mapping['targetEntity'], $linkedData); + $entities->set($key, $this->doHydrateRowData($mapping['targetEntity'], $linkedData)); } - return new ArrayCollection($entities); + return $entities; } - /** - * @param array $mapping - * @param array $data - * @return ArrayCollection - */ - protected function hydrateManyToOne(array $mapping, $data) + protected function hydrateManyToOne(array $mapping, mixed $data): object { return $this->doHydrateRowData($mapping['targetEntity'], $data); } - /** - * @param array $mapping - * @param array $data - * @return ArrayCollection - */ - protected function hydrateManyToMany(array $mapping, $data) + protected function hydrateManyToMany(array $mapping, mixed $data): ArrayCollection { - $entities = []; + $entities = new ArrayCollection(); foreach ($data as $key => $linkedData) { - $entities[$key] = $this->doHydrateRowData($mapping['targetEntity'], $linkedData); + $entities->set($key, $this->doHydrateRowData($mapping['targetEntity'], $linkedData)); } - return new ArrayCollection($entities); + return $entities; } - /** - * @param object $object - * @param string $property - * @return mixed - * @throws \Exception - */ - protected function getPrivatePropertyValue($object, $property) + protected function getPrivatePropertyValue(object $object, string $property) { $classNames = array_merge([get_class($object)], array_values(class_parents(get_class($object)))); - $classNameIndex = 0; - do { - try { - $reflection = new \ReflectionProperty($classNames[$classNameIndex], $property); - $continue = false; - } catch (\ReflectionException $e) { - $classNameIndex++; - $continue = true; + + $cacheKey = implode("", $classNames); + if(!isset($this->reflectionPropertyCache[$cacheKey])) { + $classNameIndex = 0; + do { + try { + $reflection = new \ReflectionProperty($classNames[$classNameIndex], $property); + $continue = false; + } catch (\ReflectionException) { + $classNameIndex++; + $continue = true; + } + } while ($continue); + + if (!isset($reflection) || $reflection instanceof \ReflectionProperty === false) { + throw new \Exception(get_class($object) . '::$' . $property . ' does not exists.'); } - } while ($continue); - if (isset($reflection) === false || $reflection instanceof \ReflectionProperty === false) { - throw new \Exception(get_class($object) . '::$' . $property . ' does not exists.'); + $this->reflectionPropertyCache[$cacheKey] = $reflection; } - $accessible = $reflection->isPublic(); - $reflection->setAccessible(true); + $reflection = $this->reflectionPropertyCache[$cacheKey]; + + //$accessible = $reflection->isPublic(); + //$reflection->setAccessible(true); $value = $reflection->getValue($object); - $reflection->setAccessible($accessible === false); + //$reflection->setAccessible($accessible); return $value; }