-
-
Notifications
You must be signed in to change notification settings - Fork 69
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adds configuration for the hydrator on a per property basis #42
base: 4.2.x
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,17 @@ | |
*/ | ||
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'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can probably inject it as a separate array, instead of injecting all options (makes it easier to deal with) |
||
|
||
/** | ||
* @var array Holds configuration for the object properties. | ||
*/ | ||
private $allowedProperties; | ||
|
||
/** | ||
* @var ReflectionClass | ||
*/ | ||
|
@@ -32,23 +43,113 @@ class HydratorMethodsVisitor extends NodeVisitorAbstract | |
private $accessibleProperties; | ||
|
||
/** | ||
* This variable only holds private properties. | ||
* | ||
* @var PropertyAccessor[] | ||
*/ | ||
private $propertyWriters = array(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
||
/** | ||
* @param ReflectionClass $reflectedClass | ||
*/ | ||
public function __construct(ReflectionClass $reflectedClass) | ||
public function __construct(ReflectionClass $reflectedClass, array $options = []) | ||
{ | ||
$this->reflectedClass = $reflectedClass; | ||
$this->accessibleProperties = $this->getProtectedProperties($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'); | ||
} | ||
} | ||
|
||
/** | ||
* Returns an array with properties as keys and hydrate/extract information | ||
* as values. | ||
* | ||
* @param type $allowedProperties | ||
*/ | ||
private function expandAllowedProperties($options) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This method is way too large |
||
{ | ||
$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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use custom exceptions for this. Named exception constructors is what I'd suggest (see http://rosstuck.com/formatting-exception-messages/ ) |
||
} | ||
|
||
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)); | ||
} | ||
|
||
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; | ||
} | ||
} | ||
|
||
// 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 | ||
]; | ||
} | ||
} | ||
|
||
return $allowedProperties; | ||
} | ||
|
||
/** | ||
* @param Node $node | ||
* | ||
|
@@ -80,11 +181,13 @@ private function replaceConstructor(ClassMethod $method) | |
$accessorName = $propertyWriter->props[0]->name; | ||
$originalProperty = $propertyWriter->getOriginalProperty(); | ||
$className = $originalProperty->getDeclaringClass()->getName(); | ||
$property = $originalProperty->getName(); | ||
$propertyName = $originalProperty->getName(); | ||
|
||
$bodyParts[] = "\$this->" . $accessorName . " = \\Closure::bind(function (\$object, \$value) {\n" | ||
. " \$object->" . $property . " = \$value;\n" | ||
. "}, null, " . var_export($className, true) . ");"; | ||
if (in_array($this->allowedProperties[$propertyName]['hydrate'], [true, 'optional'])) { | ||
$bodyParts[] = "\$this->" . $accessorName . " = \\Closure::bind(function (\$object, \$value) {\n" | ||
. " \$object->" . $propertyName . " = \$value;\n" | ||
. "}, null, " . var_export($className, true) . ");"; | ||
} | ||
} | ||
|
||
$parser = new Parser(new Lexer()); | ||
|
@@ -104,20 +207,50 @@ private function replaceHydrate(ClassMethod $method) | |
|
||
$body = ''; | ||
|
||
$replaceWithOption = function($option, $assignment, $keyName) { | ||
if ($option === true) { | ||
return $assignment; | ||
} elseif ($option === 'optional') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Early return = no need for |
||
return 'if (isset($data[' . $keyName . "])) {\n" | ||
. $assignment | ||
. "}\n"; | ||
} | ||
}; | ||
|
||
foreach ($this->accessibleProperties as $accessibleProperty) { | ||
$body .= '$object->' | ||
. $accessibleProperty->getName() | ||
$propertyName = $accessibleProperty->getName(); | ||
$keyName = var_export($accessibleProperty->getName(), true); | ||
$option = $this->allowedProperties[$propertyName]['hydrate']; | ||
|
||
if ($option === false) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. plz yoda There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
continue; | ||
} | ||
|
||
$assignment = '$object->' | ||
. $propertyName | ||
. ' = $data[' | ||
. var_export($accessibleProperty->getName(), true) | ||
. $keyName | ||
. "];\n"; | ||
|
||
$body .= $replaceWithOption($option, $assignment, $keyName); | ||
} | ||
|
||
foreach ($this->propertyWriters as $propertyWriter) { | ||
$body .= '$this->' | ||
. $propertyWriter->props[0]->name | ||
$propertyWriterName = $propertyWriter->props[0]->name; | ||
$keyName = var_export($propertyWriter->getOriginalProperty()->getName(), true); | ||
$option = $this->allowedProperties[$propertyWriter->getOriginalProperty()->name]['hydrate']; | ||
|
||
if ($option === false) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. plz yoda |
||
continue; | ||
} | ||
|
||
$assignment = '$this->' | ||
. $propertyWriterName | ||
. '->__invoke($object, $data[' | ||
. var_export($propertyWriter->getOriginalProperty()->getName(), true) | ||
. $keyName | ||
. "]);\n"; | ||
|
||
$body .= $replaceWithOption($option, $assignment, $keyName); | ||
} | ||
|
||
$body .= "\nreturn \$object;"; | ||
|
@@ -137,7 +270,7 @@ private function replaceExtract(ClassMethod $method) | |
$method->params = array(new Param('object')); | ||
|
||
if (empty($this->accessibleProperties) && empty($this->propertyWriters)) { | ||
// no properties to hydrate | ||
// the object does not have any properties | ||
|
||
$method->stmts = $parser->parse('<?php return array();'); | ||
|
||
|
@@ -146,40 +279,109 @@ private function replaceExtract(ClassMethod $method) | |
|
||
$body = ''; | ||
|
||
if (! empty($this->propertyWriters)) { | ||
// 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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Split into a separate private method |
||
if (in_array($this->allowedProperties[$p->getOriginalProperty()->name]['extract'], [true, 'optional'])) { | ||
$hasPrivatePropertiesWhichNeedExtract = true; | ||
} | ||
} | ||
|
||
if ($hasPrivatePropertiesWhichNeedExtract) { | ||
$body = "\$data = (array) \$object;\n\n"; | ||
} | ||
|
||
$body .= 'return array('; | ||
// Make code for the properties which can be assigned right away in the array | ||
$assignments = []; | ||
|
||
foreach ($this->accessibleProperties as $accessibleProperty) { | ||
if (empty($this->propertyWriters) || ! $accessibleProperty->isProtected()) { | ||
$body .= "\n " | ||
. var_export($accessibleProperty->getName(), true) | ||
. ' => $object->' . $accessibleProperty->getName() . ','; | ||
$propertyName = $accessibleProperty->getName(); | ||
|
||
if (! $hasPrivatePropertiesWhichNeedExtract || ! $accessibleProperty->isProtected()) { | ||
$propertyData = '$object->' . $propertyName; | ||
} else { | ||
$body .= "\n " | ||
. var_export($accessibleProperty->getName(), true) | ||
. ' => $data["\\0*\\0' . $accessibleProperty->getName() . '"],'; | ||
$propertyData = '$data["\\0*\\0' . $propertyName . '"]'; | ||
} | ||
|
||
$assignments[$propertyName] = "\n " | ||
. var_export($propertyName, true) | ||
. ' => ' . $propertyData . ','; | ||
} | ||
|
||
foreach ($this->propertyWriters as $propertyWriter) { | ||
$property = $propertyWriter->getOriginalProperty(); | ||
$propertyName = $property->getName(); | ||
|
||
$body .= "\n " | ||
$assignments[$propertyName] = "\n " | ||
. var_export($propertyName, true) | ||
. ' => $data["' | ||
. '\\0' . $property->getDeclaringClass()->getName() | ||
. '\\0' . $propertyName | ||
. '"],'; | ||
} | ||
|
||
$body .= "\n);"; | ||
// None of the extract properties are optional | ||
if (count(array_filter($this->allowedProperties, function($conf) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to wrap with |
||
return $conf['extract'] === 'optional'; | ||
})) === 0) { | ||
$body .= 'return array('; | ||
foreach ($assignments as $propertyName => $a) { | ||
if ($this->allowedProperties[$propertyName]['extract'] === true) { | ||
$body .= $a; | ||
} | ||
} | ||
$body .= "\n);"; | ||
|
||
$method->stmts = $parser->parse('<?php ' . $body); | ||
$method->stmts = $parser->parse('<?php ' . $body); | ||
|
||
return; | ||
} | ||
|
||
// Has extract properties which are optional | ||
$body .= '$ret = array('; | ||
foreach ($assignments as $propertyName => $a) { | ||
if ($this->allowedProperties[$propertyName]['extract'] === true) { | ||
$body .= $a; | ||
} | ||
} | ||
$body .= "\n);\n"; | ||
|
||
foreach ($this->accessibleProperties as $accessibleProperty) { | ||
$propertyName = $accessibleProperty->getName(); | ||
|
||
if ($this->allowedProperties[$propertyName]['extract'] === 'optional') { | ||
if (! $hasPrivatePropertiesWhichNeedExtract || ! $accessibleProperty->isProtected()) { | ||
$propertyData = '$object->' . $propertyName; | ||
} else { | ||
$propertyData = '$data["\\0*\\0' . $propertyName . '"]'; | ||
} | ||
|
||
$body .= 'if (isset(' . $propertyData . ")) {\n" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bit doubtful if i need to check if the object property is set before i can extract it.. not sure what to do here |
||
. ' $ret[' . var_export($propertyName, true) . '] = ' . $propertyData . ";\n" | ||
. "}\n"; | ||
} | ||
} | ||
|
||
foreach ($this->propertyWriters as $propertyWriter) { | ||
$property = $propertyWriter->getOriginalProperty(); | ||
$propertyName = $property->getName(); | ||
|
||
if ($this->allowedProperties[$propertyName]['extract'] === 'optional') { | ||
$propertyData = '$data["' | ||
. '\\0' . $property->getDeclaringClass()->getName() | ||
. '\\0' . $propertyName | ||
. '"]'; | ||
|
||
$body .= 'if (isset(' . $propertyData . ")) {\n" | ||
. ' $ret[' . var_export($propertyName, true) . '] = ' . $propertyData . ";\n" | ||
. "}\n"; | ||
} | ||
} | ||
|
||
$body .= "\nreturn \$ret;"; | ||
|
||
$method->stmts = $parser->parse('<?php ' . $body); | ||
} | ||
|
||
/** | ||
|
@@ -215,7 +417,7 @@ function (ClassMethod $method) use ($name) { | |
* | ||
* @return ReflectionProperty[] | ||
*/ | ||
private function getProtectedProperties(ReflectionClass $reflectedClass) | ||
private function getAccessibleProperties(ReflectionClass $reflectedClass) | ||
{ | ||
return array_filter( | ||
$reflectedClass->getProperties(), | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
update phpdoc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
changed on line 47