From c31fbcfebebefdfb7e943cc546c3a5c4a7d109fd Mon Sep 17 00:00:00 2001 From: Mark Garrett Date: Tue, 15 Apr 2014 10:19:42 -0500 Subject: [PATCH 01/13] Remove unused variable. --- src/Hydrator/ClassMethods.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Hydrator/ClassMethods.php b/src/Hydrator/ClassMethods.php index 2bee3ed9d..0d730f2db 100644 --- a/src/Hydrator/ClassMethods.php +++ b/src/Hydrator/ClassMethods.php @@ -181,7 +181,7 @@ public function hydrate(array $data, $object) $this->hydratedMethodName[$property] = (isset($this->hydratedMethodName[$property])) ? $this->hydratedMethodName[$property] : 'set' . ucfirst($this->hydrateName($property, $data)); - $method = $this->hydratedMethodName[$property]; + if (is_callable(array($object, $this->hydratedMethodName[$property]))) { $value = $this->hydrateValue($property, $value, $data); $object->{$this->hydratedMethodName[$property]}($value); From f4a37a9c1a57e874090f6912805bcd3ef63853b4 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Sat, 10 May 2014 00:21:19 +0200 Subject: [PATCH 02/13] zendframework/zf2#6142 - using `string[]` instead of `array` for the hydrated method names --- src/Hydrator/ClassMethods.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Hydrator/ClassMethods.php b/src/Hydrator/ClassMethods.php index 0d730f2db..e0cde220b 100644 --- a/src/Hydrator/ClassMethods.php +++ b/src/Hydrator/ClassMethods.php @@ -25,7 +25,8 @@ class ClassMethods extends AbstractHydrator implements HydratorOptionsInterface { /** * Holds the hydrated method name - * @var array + * + * @var string[] */ private $hydratedMethodName = array(); From d9c721815a2e1a68b1d41f262848351315b2ff80 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Sat, 10 May 2014 00:38:57 +0200 Subject: [PATCH 03/13] zendframework/zf2#6142 - better description of what the hydration method names cache does --- src/Hydrator/ClassMethods.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Hydrator/ClassMethods.php b/src/Hydrator/ClassMethods.php index e0cde220b..bb3b9e5ec 100644 --- a/src/Hydrator/ClassMethods.php +++ b/src/Hydrator/ClassMethods.php @@ -24,7 +24,7 @@ class ClassMethods extends AbstractHydrator implements HydratorOptionsInterface { /** - * Holds the hydrated method name + * Holds the names of the methods used for hydration, indexed by property name * * @var string[] */ @@ -32,6 +32,7 @@ class ClassMethods extends AbstractHydrator implements HydratorOptionsInterface /** * Flag defining whether array keys are underscore-separated (true) or camel case (false) + * * @var bool */ protected $underscoreSeparatedKeys = true; From a58bdb3fe24b3706164e0390652408d17dcb41bd Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Sat, 10 May 2014 00:39:43 +0200 Subject: [PATCH 04/13] zendframework/zf2#6142 - better hydration cache property name --- src/Hydrator/ClassMethods.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Hydrator/ClassMethods.php b/src/Hydrator/ClassMethods.php index bb3b9e5ec..43ef2d26e 100644 --- a/src/Hydrator/ClassMethods.php +++ b/src/Hydrator/ClassMethods.php @@ -28,7 +28,7 @@ class ClassMethods extends AbstractHydrator implements HydratorOptionsInterface * * @var string[] */ - private $hydratedMethodName = array(); + private $hydrationMethodsCache = array(); /** * Flag defining whether array keys are underscore-separated (true) or camel case (false) @@ -180,13 +180,13 @@ public function hydrate(array $data, $object) } foreach ($data as $property => $value) { - $this->hydratedMethodName[$property] = (isset($this->hydratedMethodName[$property])) - ? $this->hydratedMethodName[$property] + $this->hydrationMethodsCache[$property] = (isset($this->hydrationMethodsCache[$property])) + ? $this->hydrationMethodsCache[$property] : 'set' . ucfirst($this->hydrateName($property, $data)); - if (is_callable(array($object, $this->hydratedMethodName[$property]))) { + if (is_callable(array($object, $this->hydrationMethodsCache[$property]))) { $value = $this->hydrateValue($property, $value, $data); - $object->{$this->hydratedMethodName[$property]}($value); + $object->{$this->hydrationMethodsCache[$property]}($value); } } From eddff067ce965b48b4b9d3696d43ac314af48d47 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Sat, 10 May 2014 01:22:09 +0200 Subject: [PATCH 05/13] zendframework/zf2#6142 - adding tests to verify that the `ClassMethods` hydrator can act on separate object types --- test/Hydrator/ClassMethodsTest.php | 35 +++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/test/Hydrator/ClassMethodsTest.php b/test/Hydrator/ClassMethodsTest.php index c8dbf8fd9..66307f094 100644 --- a/test/Hydrator/ClassMethodsTest.php +++ b/test/Hydrator/ClassMethodsTest.php @@ -10,13 +10,15 @@ namespace ZendTest\Stdlib\Hydrator; use Zend\Stdlib\Hydrator\ClassMethods; +use ZendTest\Stdlib\TestAsset\ClassMethodsCamelCaseMissing; use ZendTest\Stdlib\TestAsset\ClassMethodsOptionalParameters; +use ZendTest\Stdlib\TestAsset\ClassMethodsCamelCase; +use ZendTest\Stdlib\TestAsset\ArraySerializable; /** * Unit tests for {@see \Zend\Stdlib\Hydrator\ClassMethods} * * @covers \Zend\Stdlib\Hydrator\ClassMethods - * @group Zend_Stdlib */ class ClassMethodsTest extends \PHPUnit_Framework_TestCase { @@ -40,4 +42,35 @@ public function testCanExtractFromMethodsWithOptionalParameters() { $this->assertSame(array('foo' => 'bar'), $this->hydrator->extract(new ClassMethodsOptionalParameters())); } + + /** + * Verifies that the hydrator can act on different instance types + */ + public function testCanHydratedPromiscuousInstances() + { + /* @var $classMethodsCamelCase ClassMethodsCamelCase */ + $classMethodsCamelCase = $this->hydrator->hydrate( + array('fooBar' => 'baz-tab'), + new ClassMethodsCamelCase() + ); + /* @var $classMethodsCamelCaseMissing ClassMethodsCamelCaseMissing */ + $classMethodsCamelCaseMissing = $this->hydrator->hydrate( + array('fooBar' => 'baz-tab'), + new ClassMethodsCamelCaseMissing() + ); + /* @var $arraySerializable ArraySerializable */ + $arraySerializable = $this->hydrator->hydrate(array('fooBar' => 'baz-tab'), new ArraySerializable()); + + $this->assertSame('baz-tab', $classMethodsCamelCase->getFooBar()); + $this->assertSame('baz-tab', $classMethodsCamelCaseMissing->getFooBar()); + $this->assertSame( + array( + "foo" => "bar", + "bar" => "foo", + "blubb" => "baz", + "quo" => "blubb" + ), + $arraySerializable->getArrayCopy() + ); + } } From 3b54b72263d260c2a5d9cd1e33ae87894a3f7352 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Sat, 10 May 2014 01:23:49 +0200 Subject: [PATCH 06/13] zendframework/zf2#6142 - Caching the hydration methods per class to avoid calling non-existing methods and for performance --- src/Hydrator/ClassMethods.php | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/Hydrator/ClassMethods.php b/src/Hydrator/ClassMethods.php index 43ef2d26e..379fab64b 100644 --- a/src/Hydrator/ClassMethods.php +++ b/src/Hydrator/ClassMethods.php @@ -24,9 +24,10 @@ class ClassMethods extends AbstractHydrator implements HydratorOptionsInterface { /** - * Holds the names of the methods used for hydration, indexed by property name + * Holds the names of the methods used for hydration, indexed by class::property name, + * false if the hydration method is not callable/usable for hydration purposes * - * @var string[] + * @var string[]|bool[] */ private $hydrationMethodsCache = array(); @@ -179,14 +180,21 @@ public function hydrate(array $data, $object) )); } + $objectClass = get_class($object); + foreach ($data as $property => $value) { - $this->hydrationMethodsCache[$property] = (isset($this->hydrationMethodsCache[$property])) - ? $this->hydrationMethodsCache[$property] - : 'set' . ucfirst($this->hydrateName($property, $data)); + $propertyFqn = $objectClass . '::$' . $property; + + if (! isset($this->hydrationMethodsCache[$propertyFqn])) { + $setterName = 'set' . ucfirst($this->hydrateName($property, $data)); + + $this->hydrationMethodsCache[$propertyFqn] = is_callable(array($object, $setterName)) + ? $setterName + : false; + } - if (is_callable(array($object, $this->hydrationMethodsCache[$property]))) { - $value = $this->hydrateValue($property, $value, $data); - $object->{$this->hydrationMethodsCache[$property]}($value); + if ($this->hydrationMethodsCache[$propertyFqn]) { + $object->{$this->hydrationMethodsCache[$propertyFqn]}($this->hydrateValue($property, $value, $data)); } } From 750f09de6b2a611bfbdfca8da1c676554817f71f Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Sat, 10 May 2014 01:28:36 +0200 Subject: [PATCH 07/13] zendframework/zf2#6142 - Stubbing out structure of an extraction methods cache --- src/Hydrator/ClassMethods.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Hydrator/ClassMethods.php b/src/Hydrator/ClassMethods.php index 379fab64b..f36c27784 100644 --- a/src/Hydrator/ClassMethods.php +++ b/src/Hydrator/ClassMethods.php @@ -31,6 +31,14 @@ class ClassMethods extends AbstractHydrator implements HydratorOptionsInterface */ private $hydrationMethodsCache = array(); + /** + * A map of extraction methods to property name to be used during extraction, indexed + * by class name and method name + * + * @var string[][] + */ + private $extractionMethodsCache = array(); + /** * Flag defining whether array keys are underscore-separated (true) or camel case (false) * From 767bcac462228f589ad5120405eeb2fbcc2e56c3 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Sat, 10 May 2014 02:07:35 +0200 Subject: [PATCH 08/13] zendframework/zf2#6142 - Implementing more performing extraction via an extraction map cache --- src/Hydrator/ClassMethods.php | 62 +++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/src/Hydrator/ClassMethods.php b/src/Hydrator/ClassMethods.php index f36c27784..ce014e6a2 100644 --- a/src/Hydrator/ClassMethods.php +++ b/src/Hydrator/ClassMethods.php @@ -19,6 +19,7 @@ use Zend\Stdlib\Hydrator\Filter\IsFilter; use Zend\Stdlib\Hydrator\Filter\MethodMatchFilter; use Zend\Stdlib\Hydrator\Filter\OptionalParametersFilter; +use Zend\Stdlib\Hydrator\NamingStrategy\NamingStrategyInterface; use Zend\Stdlib\Hydrator\NamingStrategy\UnderscoreNamingStrategy; class ClassMethods extends AbstractHydrator implements HydratorOptionsInterface @@ -132,41 +133,54 @@ public function extract($object) )); } - $filter = null; + $objectClass = get_class($object); + + // reset the hydrator's hydrator's cache for this object, as the filter may be per-instance if ($object instanceof FilterProviderInterface) { - $filter = new FilterComposite( - array($object->getFilter()), - array(new MethodMatchFilter("getFilter")) - ); - } else { - $filter = $this->filterComposite; + $this->extractionMethodsCache[$objectClass] = null; } - $attributes = array(); - $methods = get_class_methods($object); - - foreach ($methods as $method) { - if (!$filter->filter(get_class($object) . '::' . $method)) { - continue; + // pass 1 - finding out which properties can be extracted, with which methods + if (! isset($this->extractionMethodsCache[$objectClass])) { + $this->extractionMethodsCache[$objectClass] = array(); + $filter = $this->filterComposite; + $methods = get_class_methods($object); + + if ($object instanceof FilterProviderInterface) { + $filter = new FilterComposite( + array($object->getFilter()), + array(new MethodMatchFilter("getFilter")) + ); } - if (!$this->callableMethodFilter->filter(get_class($object) . '::' . $method)) { - continue; - } + foreach ($methods as $method) { + $methodFqn = $objectClass . '::' . $method; - $attribute = $method; - if (preg_match('/^get/', $method)) { - $attribute = substr($method, 3); - if (!property_exists($object, $attribute)) { - $attribute = lcfirst($attribute); + if (! ($filter->filter($methodFqn) && $this->callableMethodFilter->filter($methodFqn))) { + continue; } + + $attribute = $method; + + if (preg_match('/^get/', $method)) { + $attribute = substr($method, 3); + if (!property_exists($object, $attribute)) { + $attribute = lcfirst($attribute); + } + } + + $this->extractionMethodsCache[$objectClass][$method] = $attribute; } + } + + $values = array(); - $attribute = $this->extractName($attribute, $object); - $attributes[$attribute] = $this->extractValue($attribute, $object->$method(), $object); + foreach ($this->extractionMethodsCache[$objectClass] as $methodName => $attributeName) { + $realAttributeName = $this->extractName($attributeName, $object); + $values[$realAttributeName] = $this->extractValue($realAttributeName, $object->$methodName(), $object); } - return $attributes; + return $values; } /** From cbd8fc2766c0fa3851cb1055cedc9f36582e1180 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Sat, 10 May 2014 02:08:34 +0200 Subject: [PATCH 09/13] zendframework/zf2#6142 - Hydration caches should be reset when filters/strategies are changed --- src/Hydrator/ClassMethods.php | 48 +++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/Hydrator/ClassMethods.php b/src/Hydrator/ClassMethods.php index ce014e6a2..7b1b0df78 100644 --- a/src/Hydrator/ClassMethods.php +++ b/src/Hydrator/ClassMethods.php @@ -222,4 +222,52 @@ public function hydrate(array $data, $object) return $object; } + + /** + * {@inheritDoc} + */ + public function addFilter($name, $filter, $condition = FilterComposite::CONDITION_OR) + { + $this->resetCaches(); + + return parent::addFilter($name, $filter, $condition); + } + + /** + * {@inheritDoc} + */ + public function removeFilter($name) + { + $this->resetCaches(); + + return parent::removeFilter($name); + } + + /** + * {@inheritDoc} + */ + public function setNamingStrategy(NamingStrategyInterface $strategy) + { + $this->resetCaches(); + + return parent::setNamingStrategy($strategy); + } + + /** + * {@inheritDoc} + */ + public function removeNamingStrategy() + { + $this->resetCaches(); + + return parent::removeNamingStrategy(); + } + + /** + * Reset all local hydration/extraction caches + */ + private function resetCaches() + { + $this->hydrationMethodsCache = $this->extractionMethodsCache = array(); + } } From 214427c4b904d371bec029425cdeb1d1a6487d3a Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Sat, 10 May 2014 02:51:34 +0200 Subject: [PATCH 10/13] zendframework/zf2#6142 - Adding minor comments to distinguish various portions of the caching logic --- src/Hydrator/ClassMethods.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Hydrator/ClassMethods.php b/src/Hydrator/ClassMethods.php index 7b1b0df78..02586dd0f 100644 --- a/src/Hydrator/ClassMethods.php +++ b/src/Hydrator/ClassMethods.php @@ -140,7 +140,7 @@ public function extract($object) $this->extractionMethodsCache[$objectClass] = null; } - // pass 1 - finding out which properties can be extracted, with which methods + // pass 1 - finding out which properties can be extracted, with which methods (populate hydration cache) if (! isset($this->extractionMethodsCache[$objectClass])) { $this->extractionMethodsCache[$objectClass] = array(); $filter = $this->filterComposite; @@ -175,6 +175,7 @@ public function extract($object) $values = array(); + // pass 2 - actually extract data foreach ($this->extractionMethodsCache[$objectClass] as $methodName => $attributeName) { $realAttributeName = $this->extractName($attributeName, $object); $values[$realAttributeName] = $this->extractValue($realAttributeName, $object->$methodName(), $object); From 95d0a7b6168f51123aabd9ee019997d7a002d54e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 10 May 2014 14:29:12 +0200 Subject: [PATCH 11/13] Use substr instead of regex --- src/Hydrator/ClassMethods.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Hydrator/ClassMethods.php b/src/Hydrator/ClassMethods.php index 02586dd0f..7b9d3ad2b 100644 --- a/src/Hydrator/ClassMethods.php +++ b/src/Hydrator/ClassMethods.php @@ -162,7 +162,8 @@ public function extract($object) $attribute = $method; - if (preg_match('/^get/', $method)) { + $prefix = substr($method, 0, 3); + if ($prefix === 'get') { $attribute = substr($method, 3); if (!property_exists($object, $attribute)) { $attribute = lcfirst($attribute); From 135ae36d6f7f8ae922999efd6445a3aa0fbeb442 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 10 May 2014 14:29:51 +0200 Subject: [PATCH 12/13] Fixed CS --- src/Hydrator/ClassMethods.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Hydrator/ClassMethods.php b/src/Hydrator/ClassMethods.php index 7b9d3ad2b..79a1d4e28 100644 --- a/src/Hydrator/ClassMethods.php +++ b/src/Hydrator/ClassMethods.php @@ -162,7 +162,7 @@ public function extract($object) $attribute = $method; - $prefix = substr($method, 0, 3); + $prefix = substr($method, 0, 3); if ($prefix === 'get') { $attribute = substr($method, 3); if (!property_exists($object, $attribute)) { From 3acfd2790b5140b7bf2e82e2c43b74b7c93ded89 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 10 May 2014 15:29:37 +0200 Subject: [PATCH 13/13] Use strpos instead of substr. --- src/Hydrator/ClassMethods.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Hydrator/ClassMethods.php b/src/Hydrator/ClassMethods.php index 79a1d4e28..4fc56d3f5 100644 --- a/src/Hydrator/ClassMethods.php +++ b/src/Hydrator/ClassMethods.php @@ -162,8 +162,7 @@ public function extract($object) $attribute = $method; - $prefix = substr($method, 0, 3); - if ($prefix === 'get') { + if (strpos($method, 'get') === 0) { $attribute = substr($method, 3); if (!property_exists($object, $attribute)) { $attribute = lcfirst($attribute);