From 675ab1e2992fd24fee7d065b51837a54c73f8578 Mon Sep 17 00:00:00 2001 From: flip111 Date: Tue, 24 Nov 2015 00:54:26 +0100 Subject: [PATCH] Moved code around and small fixes 1. moved information on properties outside of the HydratorMethodsVisitor into the HydratorGenerator 2. moved exception code into it's own class InvalidOptionException 3. moved option parsing from HydratorMethodsVisitor to value object AllowedPropertiesOption 4. small fix where a null would not be hydrated (in case the object itself has another default value) --- .../AllowedPropertiesOption.php | 155 +++++++++++++++ .../ClassGenerator/HydratorGenerator.php | 53 +++++- .../Visitor/HydratorMethodsVisitor.php | 180 +++--------------- .../Exception/InvalidOptionException.php | 75 ++++++++ .../Factory/HydratorFactory.php | 5 +- 5 files changed, 314 insertions(+), 154 deletions(-) create mode 100644 src/GeneratedHydrator/ClassGenerator/AllowedPropertiesOption.php create mode 100644 src/GeneratedHydrator/Exception/InvalidOptionException.php diff --git a/src/GeneratedHydrator/ClassGenerator/AllowedPropertiesOption.php b/src/GeneratedHydrator/ClassGenerator/AllowedPropertiesOption.php new file mode 100644 index 00000000..e2e9ad7b --- /dev/null +++ b/src/GeneratedHydrator/ClassGenerator/AllowedPropertiesOption.php @@ -0,0 +1,155 @@ + + * @license MIT + */ +class AllowedPropertiesOption { + /** + * When this option is passed, only the properties in the given array are + * hydrated and extracted. + */ + const OPTION_ALLOWED_PROPERTIES = 'allowedProperties'; + + /** + * @var PropertyAccessor[] + */ + private $propertyNames; + + /** + * @var array Holds configuration for the object properties. + */ + private $allowedProperties; + + public function __construct(ReflectionClass $reflectedClass, array $options) { + $this->propertyNames = array_map(function($prop) { + return $prop->name; + }, $reflectedClass->getProperties()); + + $this->allowedProperties = $this->expandAllowedProperties($options); + } + + /** + * Returns an array with properties as keys and hydrate/extract information + * as values. + * + * @param array $options + */ + private function expandAllowedProperties($options) + { + $allowedProperties = []; + + // Option was not given + if (! isset($options[static::OPTION_ALLOWED_PROPERTIES])) { + foreach ($this->propertyNames as $propertyName) { + $allowedProperties[$propertyName] = [ + 'extract' => true, + 'hydrate' => true + ]; + } + + return $allowedProperties; + } + + if (! is_array($options[static::OPTION_ALLOWED_PROPERTIES])) { + throw InvalidOptionException::valueNotArray(gettype($options[static::OPTION_ALLOWED_PROPERTIES])); + } + + // Option was given + foreach ($options[static::OPTION_ALLOWED_PROPERTIES] as $k => $v) { + // simple format + if (is_int($k)) { + $this->makeSimpleFormat($k, $v, $allowedProperties); + + continue; + } + + // advanced format + if (is_string($k)) { + $this->makeAdvancedFormat($k, $v, $allowedProperties); + } + } + + // Disable all properties which are not specified in the allowedProperties + foreach ($this->propertyNames as $propertyName) { + if (! in_array($propertyName, array_keys($allowedProperties))) { + $allowedProperties[$propertyName] = [ + 'extract' => false, + 'hydrate' => false + ]; + } + } + + return $allowedProperties; + } + + private function makeSimpleFormat($key, $value, &$allowedProperties) { + if (! is_string($value)) { + throw InvalidOptionException::invalidValueExpectedString(gettype($value), $key); + } + + if (in_array($value, array_keys($allowedProperties))) { + throw InvalidOptionException::doubleProperty($value); + } + + $allowedProperties[$value] = [ + 'extract' => true, + 'hydrate' => true + ]; + } + + private function makeAdvancedFormat($key, $value, &$allowedProperties) { + if (! is_array($value)) { + throw InvalidOptionException::arrayExpected($k, gettype($value)); + } + + if (in_array($key, $allowedProperties)) { + throw InvalidOptionException::doubleProperty($value); + } + + $validateOptionConfigurationKey = function($property, $array, $key) { + if (! isset($array[$key])) { + throw InvalidOptionException::missingKey($property, $key); + } + + if (! in_array($array[$key], [true, false, 'optional'])) { + throw InvalidOptionException::invalidValue($property, $key); + } + }; + + $validateOptionConfigurationKey($key, $value, 'extract'); + $validateOptionConfigurationKey($key, $value, 'hydrate'); + + $allowedProperties[$key] = $value; + } + + public function getAllowedProperties() { + return $this->allowedProperties; + } +} diff --git a/src/GeneratedHydrator/ClassGenerator/HydratorGenerator.php b/src/GeneratedHydrator/ClassGenerator/HydratorGenerator.php index e54aaa60..ee69fe99 100644 --- a/src/GeneratedHydrator/ClassGenerator/HydratorGenerator.php +++ b/src/GeneratedHydrator/ClassGenerator/HydratorGenerator.php @@ -22,9 +22,11 @@ use CodeGenerationUtils\Visitor\ClassExtensionVisitor; use CodeGenerationUtils\Visitor\ClassImplementorVisitor; use CodeGenerationUtils\Visitor\MethodDisablerVisitor; +use GeneratedHydrator\ClassGenerator\Hydrator\PropertyGenerator\PropertyAccessor; use GeneratedHydrator\CodeGenerator\Visitor\HydratorMethodsVisitor; use PhpParser\NodeTraverser; use ReflectionClass; +use ReflectionProperty; /** * Generator for highly performing {@see \Zend\Stdlib\Hydrator\HydratorInterface} @@ -67,7 +69,12 @@ function () { // step 2: implement new methods and interfaces, extend original class $implementor = new NodeTraverser(); - $implementor->addVisitor(new HydratorMethodsVisitor($originalClass, $options)); + // prepare information which can be used by visitors + $accessibleProperties = $this->getAccessibleProperties($originalClass); + $propertyWriters = $this->getPropertyWriters($originalClass); + $allowedPropertiesOption = new AllowedPropertiesOption($originalClass, $options); + + $implementor->addVisitor(new HydratorMethodsVisitor($accessibleProperties, $propertyWriters, $allowedPropertiesOption)); $implementor->addVisitor(new ClassExtensionVisitor($originalClass->getName(), $originalClass->getName())); $implementor->addVisitor( new ClassImplementorVisitor($originalClass->getName(), array('Zend\\Stdlib\\Hydrator\\HydratorInterface')) @@ -75,4 +82,48 @@ function () { return $implementor->traverse($ast); } + + /** + * Retrieve instance public/protected properties + * + * @param ReflectionClass $reflectedClass + * + * @return ReflectionProperty[] + */ + private function getAccessibleProperties(ReflectionClass $reflectedClass) + { + return array_filter( + $reflectedClass->getProperties(), + function (ReflectionProperty $property) { + return ($property->isPublic() || $property->isProtected()) && ! $property->isStatic(); + } + ); + } + + /** + * Retrieve instance private properties + * + * @param ReflectionClass $reflectedClass + * + * @return ReflectionProperty[] + */ + private function getPrivateProperties(ReflectionClass $reflectedClass) + { + return array_filter( + $reflectedClass->getProperties(), + function (ReflectionProperty $property) { + return $property->isPrivate() && ! $property->isStatic(); + } + ); + } + + private function getPropertyWriters(ReflectionClass $reflectedClass) { + $propertyWriters = []; + + foreach ($this->getPrivateProperties($reflectedClass) as $property) { + $propertyWriters[$property->getName()] = new PropertyAccessor($property, 'Writer'); + } + + return $propertyWriters; + } } diff --git a/src/GeneratedHydrator/CodeGenerator/Visitor/HydratorMethodsVisitor.php b/src/GeneratedHydrator/CodeGenerator/Visitor/HydratorMethodsVisitor.php index d4b342f9..406119f8 100644 --- a/src/GeneratedHydrator/CodeGenerator/Visitor/HydratorMethodsVisitor.php +++ b/src/GeneratedHydrator/CodeGenerator/Visitor/HydratorMethodsVisitor.php @@ -2,6 +2,7 @@ namespace GeneratedHydrator\CodeGenerator\Visitor; +use GeneratedHydrator\ClassGenerator\AllowedPropertiesOption; use GeneratedHydrator\ClassGenerator\Hydrator\PropertyGenerator\PropertyAccessor; use PhpParser\Lexer; use PhpParser\Node; @@ -21,22 +22,11 @@ */ class HydratorMethodsVisitor extends NodeVisitorAbstract { - /** - * When this option is passed, only the properties in the given array are - * hydrated and extracted. - */ - const OPTION_ALLOWED_PROPERTIES = 'allowedProperties'; - /** * @var array Holds configuration for the object properties. */ private $allowedProperties; - /** - * @var ReflectionClass - */ - private $reflectedClass; - /** * @var ReflectionProperty[] */ @@ -50,104 +40,31 @@ class HydratorMethodsVisitor extends NodeVisitorAbstract private $propertyWriters = array(); /** - * @param ReflectionClass $reflectedClass + * This flag is being used to determine if protected properties get their + * data from an array or directly from the object itself + * + * @var bool */ - public function __construct(ReflectionClass $reflectedClass, array $options = []) - { - $this->reflectedClass = $reflectedClass; - $this->accessibleProperties = $this->getAccessibleProperties($reflectedClass); - $this->allowedProperties = $this->expandAllowedProperties($options); - - foreach ($this->getPrivateProperties($reflectedClass) as $property) { - $this->propertyWriters[$property->getName()] = new PropertyAccessor($property, 'Writer'); - } - } + private $hasPrivatePropertiesWhichNeedExtracting = false; /** - * Returns an array with properties as keys and hydrate/extract information - * as values. - * - * @param type $allowedProperties + * @param ReflectionClass $reflectedClass */ - private function expandAllowedProperties($options) + public function __construct(array $accessibleProperties, array $propertyWriters, AllowedPropertiesOption $option) { - $allowedProperties = []; - $propertyNames = array_map(function($prop) { - return $prop->name; - }, $this->reflectedClass->getProperties()); - - if (! isset($options[static::OPTION_ALLOWED_PROPERTIES])) { - foreach ($propertyNames as $propertyName) { - $allowedProperties[$propertyName] = [ - 'extract' => true, - 'hydrate' => true - ]; - } - - return $allowedProperties; - } - - if (! is_array($options[static::OPTION_ALLOWED_PROPERTIES])) { - throw new \InvalidArgumentException(sprintf('OPTION_ALLOWED_PROPERTIES is given but it\'s value is of type %s which should be an array.', gettype($options[static::OPTION_ALLOWED_PROPERTIES]))); - } - - foreach ($options[static::OPTION_ALLOWED_PROPERTIES] as $k => $v) { - // simple format - if (is_int($k)) { - if (! is_string($v)) { - throw new \InvalidArgumentException(sprintf('Invalid value of type %s found on index %s, expected a string.', gettype($v), $k)); - } - - if (in_array($v, array_keys($allowedProperties))) { - throw new \InvalidArgumentException(sprintf('Property "%s" was supplied in simple and advanced format, only one is allowed.', $v)); - } - - $allowedProperties[$v] = [ - 'extract' => true, - 'hydrate' => true - ]; - - continue; - } - - // advanced format - if (is_string($k)) { - if (! is_array($v)) { - throw new \InvalidArgumentException(sprintf('Property "%s" was supplied as key, but the value is of type %s and an array was expected.', $k, gettype($v))); - } - - if (in_array($k, $allowedProperties)) { - throw new \InvalidArgumentException(sprintf('Property "%s" was supplied in simple and advanced format, only one is allowed.', $v)); - } - - $validateOptionConfigurationKey = function($property, $array, $key) { - if (! isset($array[$key])) { - throw new \InvalidArgumentException(sprintf('Property "%s" is missing key "%s".', $property, $key)); - } + $this->propertyWriters = $propertyWriters; + $this->accessibleProperties = $accessibleProperties; + $this->allowedProperties = $option->getAllowedProperties(); - if (! in_array($array[$key], [true, false, 'optional'])) { - throw new \InvalidArgumentException(sprintf('Property "%s" has an invalid value for key "$s".', $property, $key)); - } - }; - - $validateOptionConfigurationKey($k, $v, 'extract'); - $validateOptionConfigurationKey($k, $v, 'hydrate'); - - $allowedProperties[$k] = $v; - } - } + foreach ($this->propertyWriters as $propertyWriter) { + var_dump(get_class($propertyWriter)); + $allowedPropertyExtract = $this->allowedProperties[$propertyWriter->getOriginalProperty()->name]['extract']; - // Disable all properties which are not specified in the allowedProperties - foreach ($propertyNames as $propertyName) { - if (! in_array($propertyName, array_keys($allowedProperties))) { - $allowedProperties[$propertyName] = [ - 'extract' => false, - 'hydrate' => false - ]; + if (in_array($allowedPropertyExtract, [true, 'optional'])) { + $this->hasPrivatePropertiesWhichNeedExtractinging = true; + break; } } - - return $allowedProperties; } /** @@ -210,8 +127,10 @@ private function replaceHydrate(ClassMethod $method) $replaceWithOption = function($option, $assignment, $keyName) { if ($option === true) { return $assignment; - } elseif ($option === 'optional') { - return 'if (isset($data[' . $keyName . "])) {\n" + } + + if ($option === 'optional') { + return 'if (isset($data[' . $keyName . ']) OR array_key_exists(' . $keyName . ", \$data)) {\n" . $assignment . "}\n"; } @@ -222,7 +141,7 @@ private function replaceHydrate(ClassMethod $method) $keyName = var_export($accessibleProperty->getName(), true); $option = $this->allowedProperties[$propertyName]['hydrate']; - if ($option === false) { + if (false === $option) { continue; } @@ -240,7 +159,7 @@ private function replaceHydrate(ClassMethod $method) $keyName = var_export($propertyWriter->getOriginalProperty()->getName(), true); $option = $this->allowedProperties[$propertyWriter->getOriginalProperty()->name]['hydrate']; - if ($option === false) { + if (false === $option) { continue; } @@ -279,16 +198,7 @@ private function replaceExtract(ClassMethod $method) $body = ''; - // This flag is being used to determine if protected properties get their - // data from an array or directly from the object itself - $hasPrivatePropertiesWhichNeedExtract = false; - foreach ($this->propertyWriters as $p) { - if (in_array($this->allowedProperties[$p->getOriginalProperty()->name]['extract'], [true, 'optional'])) { - $hasPrivatePropertiesWhichNeedExtract = true; - } - } - - if ($hasPrivatePropertiesWhichNeedExtract) { + if ($this->hasPrivatePropertiesWhichNeedExtracting) { $body = "\$data = (array) \$object;\n\n"; } @@ -298,7 +208,7 @@ private function replaceExtract(ClassMethod $method) foreach ($this->accessibleProperties as $accessibleProperty) { $propertyName = $accessibleProperty->getName(); - if (! $hasPrivatePropertiesWhichNeedExtract || ! $accessibleProperty->isProtected()) { + if (! $this->hasPrivatePropertiesWhichNeedExtracting || ! $accessibleProperty->isProtected()) { $propertyData = '$object->' . $propertyName; } else { $propertyData = '$data["\\0*\\0' . $propertyName . '"]'; @@ -322,9 +232,9 @@ private function replaceExtract(ClassMethod $method) } // None of the extract properties are optional - if (count(array_filter($this->allowedProperties, function($conf) { + if (! array_filter($this->allowedProperties, function($conf) { return $conf['extract'] === 'optional'; - })) === 0) { + })) { $body .= 'return array('; foreach ($assignments as $propertyName => $a) { if ($this->allowedProperties[$propertyName]['extract'] === true) { @@ -351,7 +261,7 @@ private function replaceExtract(ClassMethod $method) $propertyName = $accessibleProperty->getName(); if ($this->allowedProperties[$propertyName]['extract'] === 'optional') { - if (! $hasPrivatePropertiesWhichNeedExtract || ! $accessibleProperty->isProtected()) { + if (! $this->hasPrivatePropertiesWhichNeedExtracting || ! $accessibleProperty->isProtected()) { $propertyData = '$object->' . $propertyName; } else { $propertyData = '$data["\\0*\\0' . $propertyName . '"]'; @@ -409,38 +319,4 @@ function (ClassMethod $method) use ($name) { return $method; } - - /** - * Retrieve instance public/protected properties - * - * @param ReflectionClass $reflectedClass - * - * @return ReflectionProperty[] - */ - private function getAccessibleProperties(ReflectionClass $reflectedClass) - { - return array_filter( - $reflectedClass->getProperties(), - function (ReflectionProperty $property) { - return ($property->isPublic() || $property->isProtected()) && ! $property->isStatic(); - } - ); - } - - /** - * Retrieve instance private properties - * - * @param ReflectionClass $reflectedClass - * - * @return ReflectionProperty[] - */ - private function getPrivateProperties(ReflectionClass $reflectedClass) - { - return array_filter( - $reflectedClass->getProperties(), - function (ReflectionProperty $property) { - return $property->isPrivate() && ! $property->isStatic(); - } - ); - } } diff --git a/src/GeneratedHydrator/Exception/InvalidOptionException.php b/src/GeneratedHydrator/Exception/InvalidOptionException.php new file mode 100644 index 00000000..13c56892 --- /dev/null +++ b/src/GeneratedHydrator/Exception/InvalidOptionException.php @@ -0,0 +1,75 @@ + + * @license MIT + */ +class InvalidOptionException extends InvalidArgumentException implements ExceptionInterface { + public static function valueNotArray($type) + { + return new self(sprintf( + 'OPTION_ALLOWED_PROPERTIES is given but it\'s value is of type %s which should be an array.', + $type + )); + } + + public static function invalidValueExpectedString($type, $key) + { + return new self(sprintf( + 'Invalid value of type %s found on index %s, expected a string.', + $type, $key + )); + } + + public static function doubleProperty($value) + { + return new self(sprintf( + 'Property "%s" was supplied in simple and advanced format, only one is allowed.', + $value + )); + } + + public static function arrayExpected($key, $type) + { + return new self(sprintf( + 'Property "%s" was supplied as key, but the value is of type %s and an array was expected.', + $key, $type + )); + } + + public static function missingKey($property, $key) + { + return new self(sprintf( + 'Property "%s" is missing key "%s".', + $property, $key + )); + } + + public static function invalidValue($property, $key) + { + return new self(sprintf( + 'Property "%s" has an invalid value for key "$s".', + $property, $key + )); + } +} diff --git a/src/GeneratedHydrator/Factory/HydratorFactory.php b/src/GeneratedHydrator/Factory/HydratorFactory.php index 0680b0d9..8dd2e9d6 100644 --- a/src/GeneratedHydrator/Factory/HydratorFactory.php +++ b/src/GeneratedHydrator/Factory/HydratorFactory.php @@ -56,7 +56,10 @@ public function getHydratorClass(array $options = []) { $inflector = $this->configuration->getClassNameInflector(); $realClassName = $inflector->getUserClassName($this->configuration->getHydratedClassName()); - $hydratorClassName = $inflector->getGeneratedClassName($realClassName, array('factory' => get_class($this))); + $hydratorClassName = $inflector->getGeneratedClassName($realClassName, [ + 'factory' => get_class($this), + 'options' => $options + ]); if (! class_exists($hydratorClassName) && $this->configuration->doesAutoGenerateProxies()) { $generator = new HydratorGenerator();