Skip to content
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

Open
wants to merge 2 commits into
base: 4.2.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/GeneratedHydrator/ClassGenerator/HydratorGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class HydratorGenerator
*
* @return \PhpParser\Node[]
*/
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update phpdoc

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed on line 47

public function generate(ReflectionClass $originalClass)
public function generate(ReflectionClass $originalClass, array $options = [])
{
$builder = new ClassBuilder();

Expand All @@ -67,7 +67,7 @@ function () {
// step 2: implement new methods and interfaces, extend original class
$implementor = new NodeTraverser();

$implementor->addVisitor(new HydratorMethodsVisitor($originalClass));
$implementor->addVisitor(new HydratorMethodsVisitor($originalClass, $options));
$implementor->addVisitor(new ClassExtensionVisitor($originalClass->getName(), $originalClass->getName()));
$implementor->addVisitor(
new ClassImplementorVisitor($originalClass->getName(), array('Zend\\Stdlib\\Hydrator\\HydratorInterface'))
Expand Down
254 changes: 228 additions & 26 deletions src/GeneratedHydrator/CodeGenerator/Visitor/HydratorMethodsVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Copy link
Owner

Choose a reason for hiding this comment

The 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
*/
Expand All @@ -32,23 +43,113 @@ class HydratorMethodsVisitor extends NodeVisitorAbstract
private $accessibleProperties;

/**
* This variable only holds private properties.
*
* @var PropertyAccessor[]
*/
private $propertyWriters = array();
Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
Copy link
Owner

Choose a reason for hiding this comment

The 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));
Copy link
Owner

Choose a reason for hiding this comment

The 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
*
Expand Down Expand Up @@ -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());
Expand All @@ -104,20 +207,50 @@ private function replaceHydrate(ClassMethod $method)

$body = '';

$replaceWithOption = function($option, $assignment, $keyName) {
if ($option === true) {
return $assignment;
} elseif ($option === 'optional') {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Early return = no need for elseif

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) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plz yoda

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

invert you will - constant go left

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) {
Copy link
Owner

Choose a reason for hiding this comment

The 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;";
Expand All @@ -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();');

Expand All @@ -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) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$p?

Copy link
Owner

Choose a reason for hiding this comment

The 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) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to wrap with count: can simply use ! in front of it

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"
Copy link
Author

Choose a reason for hiding this comment

The 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);
}

/**
Expand Down Expand Up @@ -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(),
Expand Down
6 changes: 4 additions & 2 deletions src/GeneratedHydrator/Factory/HydratorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,11 @@ public function __construct(Configuration $configuration)
/**
* Retrieves the generated hydrator FQCN
*
* @param array $options
*
* @return string
*/
public function getHydratorClass()
public function getHydratorClass(array $options = [])
{
$inflector = $this->configuration->getClassNameInflector();
$realClassName = $inflector->getUserClassName($this->configuration->getHydratedClassName());
Expand All @@ -59,7 +61,7 @@ public function getHydratorClass()
if (! class_exists($hydratorClassName) && $this->configuration->doesAutoGenerateProxies()) {
$generator = new HydratorGenerator();
$originalClass = new ReflectionClass($realClassName);
$generatedAst = $generator->generate($originalClass);
$generatedAst = $generator->generate($originalClass, $options);
$traverser = new NodeTraverser();

$traverser->addVisitor(new ClassRenamerVisitor($originalClass, $hydratorClassName));
Expand Down
You are viewing a condensed version of this merge commit. You can view the full changes here.