diff --git a/src/Hydrator/ClassMethods.php b/src/Hydrator/ClassMethods.php index 7ea4a248b..4fc56d3f5 100644 --- a/src/Hydrator/ClassMethods.php +++ b/src/Hydrator/ClassMethods.php @@ -19,12 +19,30 @@ 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 { + /** + * 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[]|bool[] + */ + 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) + * * @var bool */ protected $underscoreSeparatedKeys = true; @@ -110,49 +128,60 @@ public function extract($object) { if (!is_object($object)) { throw new Exception\BadMethodCallException(sprintf( - '%s expects the provided $object to be a PHP object)', __METHOD__ + '%s expects the provided $object to be a PHP object)', + __METHOD__ )); } - $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); + // 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; + $methods = get_class_methods($object); - foreach ($methods as $method) { - if ( - !$filter->filter( - get_class($object) . '::' . $method - ) - ) { - continue; + 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; + + if (! ($filter->filter($methodFqn) && $this->callableMethodFilter->filter($methodFqn))) { + continue; + } + + $attribute = $method; - $attribute = $method; - if (preg_match('/^get/', $method)) { - $attribute = substr($method, 3); - if (!property_exists($object, $attribute)) { - $attribute = lcfirst($attribute); + if (strpos($method, 'get') === 0) { + $attribute = substr($method, 3); + if (!property_exists($object, $attribute)) { + $attribute = lcfirst($attribute); + } } + + $this->extractionMethodsCache[$objectClass][$method] = $attribute; } + } - $attribute = $this->extractName($attribute, $object); - $attributes[$attribute] = $this->extractValue($attribute, $object->$method(), $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); } - return $attributes; + return $values; } /** @@ -169,18 +198,77 @@ public function hydrate(array $data, $object) { if (!is_object($object)) { throw new Exception\BadMethodCallException(sprintf( - '%s expects the provided $object to be a PHP object)', __METHOD__ + '%s expects the provided $object to be a PHP object)', + __METHOD__ )); } + $objectClass = get_class($object); + foreach ($data as $property => $value) { - $method = 'set' . ucfirst($this->hydrateName($property, $data)); - if (is_callable(array($object, $method))) { - $value = $this->hydrateValue($property, $value, $data); - $object->$method($value); + $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 ($this->hydrationMethodsCache[$propertyFqn]) { + $object->{$this->hydrationMethodsCache[$propertyFqn]}($this->hydrateValue($property, $value, $data)); } } 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(); + } } 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() + ); + } }