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();