diff --git a/src/Hydrator/ObjectProperty.php b/src/Hydrator/ObjectProperty.php index 20c97c4a2..33c90c30b 100644 --- a/src/Hydrator/ObjectProperty.php +++ b/src/Hydrator/ObjectProperty.php @@ -10,16 +10,21 @@ namespace Zend\Stdlib\Hydrator; use Zend\Stdlib\Exception; +use ReflectionClass; +use ReflectionProperty; class ObjectProperty extends AbstractHydrator { /** - * Extract values from an object + * @var array[] indexed by class name and then property name + */ + private static $skippedPropertiesCache = array(); + + /** + * {@inheritDoc} * * Extracts the accessible non-static properties of the given $object. * - * @param object $object - * @return array * @throws Exception\BadMethodCallException for a non-object $object */ public function extract($object) @@ -30,21 +35,24 @@ public function extract($object) )); } - $data = get_object_vars($object); - + $data = get_object_vars($object); $filter = $this->getFilter(); + foreach ($data as $name => $value) { // Filter keys, removing any we don't want - if (!$filter->filter($name)) { + if (! $filter->filter($name)) { unset($data[$name]); continue; } + // Replace name if extracted differ $extracted = $this->extractName($name, $object); + if ($extracted !== $name) { unset($data[$name]); $name = $extracted; } + $data[$name] = $this->extractValue($name, $value, $object); } @@ -52,26 +60,51 @@ public function extract($object) } /** + * {@inheritDoc} + * * Hydrate an object by populating public properties * * Hydrates an object by setting public properties of the object. * - * @param array $data - * @param object $object - * @return object * @throws Exception\BadMethodCallException for a non-object $object */ public function hydrate(array $data, $object) { - if (!is_object($object)) { + if (! is_object($object)) { throw new Exception\BadMethodCallException(sprintf( '%s expects the provided $object to be a PHP object)', __METHOD__ )); } + + $properties = & self::$skippedPropertiesCache[get_class($object)]; + + if (! isset($properties)) { + $reflection = new ReflectionClass($object); + $properties = array_fill_keys( + array_map( + function (ReflectionProperty $property) { + return $property->getName(); + }, + $reflection->getProperties( + ReflectionProperty::IS_PRIVATE + + ReflectionProperty::IS_PROTECTED + + ReflectionProperty::IS_STATIC + ) + ), + true + ); + } + foreach ($data as $name => $value) { $property = $this->hydrateName($name, $data); + + if (isset($properties[$property])) { + continue; + } + $object->$property = $this->hydrateValue($property, $value, $data); } + return $object; } } diff --git a/test/Hydrator/ObjectPropertyTest.php b/test/Hydrator/ObjectPropertyTest.php new file mode 100644 index 000000000..0811057ac --- /dev/null +++ b/test/Hydrator/ObjectPropertyTest.php @@ -0,0 +1,166 @@ +hydrator = new ObjectProperty(); + } + + /** + * Verify that we get an exception when trying to extract on a non-object + */ + public function testHydratorExtractThrowsExceptionOnNonObjectParameter() + { + $this->setExpectedException('BadMethodCallException'); + $this->hydrator->extract('thisIsNotAnObject'); + } + + /** + * Verify that we get an exception when trying to hydrate a non-object + */ + public function testHydratorHydrateThrowsExceptionOnNonObjectParameter() + { + $this->setExpectedException('BadMethodCallException'); + $this->hydrator->hydrate(array('some' => 'data'), 'thisIsNotAnObject'); + } + + /** + * Verifies that the hydrator can extract from property of stdClass objects + */ + public function testCanExtractFromStdClass() + { + $object = new \stdClass(); + $object->foo = 'bar'; + + $this->assertSame(array('foo' => 'bar'), $this->hydrator->extract($object)); + } + + /** + * Verifies that the extraction process works on classes that aren't stdClass + */ + public function testCanExtractFromGenericClass() + { + $this->assertSame( + array( + 'foo' => 'bar', + 'bar' => 'foo', + 'blubb' => 'baz', + 'quo' => 'blubb' + ), + $this->hydrator->extract(new ObjectPropertyTestAsset()) + ); + } + + /** + * Verify hydration of {@see \stdClass} + */ + public function testCanHydrateStdClass() + { + $object = new \stdClass(); + $object->foo = 'bar'; + + $object = $this->hydrator->hydrate(array('foo' => 'baz'), $object); + + $this->assertEquals('baz', $object->foo); + } + + /** + * Verify that new properties are created if the object is stdClass + */ + public function testCanHydrateAdditionalPropertiesToStdClass() + { + $object = new \stdClass(); + $object->foo = 'bar'; + + $object = $this->hydrator->hydrate(array('foo' => 'baz', 'bar' => 'baz'), $object); + + $this->assertEquals('baz', $object->foo); + $this->assertObjectHasAttribute('bar', $object); + $this->assertAttributeSame('baz', 'bar', $object); + } + + /** + * Verify that it can hydrate our class public properties + */ + public function testCanHydrateGenericClassPublicProperties() + { + $object = $this->hydrator->hydrate( + array( + 'foo' => 'foo', + 'bar' => 'bar', + 'blubb' => 'blubb', + 'quo' => 'quo', + 'quin' => 'quin' + ), + new ObjectPropertyTestAsset() + ); + + $this->assertAttributeSame('foo', 'foo', $object); + $this->assertAttributeSame('bar', 'bar', $object); + $this->assertAttributeSame('blubb', 'blubb', $object); + $this->assertAttributeSame('quo', 'quo', $object); + $this->assertAttributeNotSame('quin', 'quin', $object); + } + + /** + * Verify that it can hydrate new properties on generic classes + */ + public function testCanHydrateGenericClassNonExistingProperties() + { + $object = $this->hydrator->hydrate(array('newProperty' => 'newPropertyValue'), new ObjectPropertyTestAsset()); + + $this->assertAttributeSame('newPropertyValue', 'newProperty', $object); + } + + /** + * Verify that hydration is skipped for class properties (it is an object hydrator after all) + */ + public function testSkipsPublicStaticClassPropertiesHydration() + { + $this->hydrator->hydrate( + array('foo' => '1', 'bar' => '2', 'baz' => '3'), + new ClassWithPublicStaticProperties() + ); + + $this->assertSame('foo', ClassWithPublicStaticProperties::$foo); + $this->assertSame('bar', ClassWithPublicStaticProperties::$bar); + $this->assertSame('baz', ClassWithPublicStaticProperties::$baz); + } + + /** + * Verify that extraction is skipped for class properties (it is an object hydrator after all) + */ + public function testSkipsPublicStaticClassPropertiesExtraction() + { + $this->assertEmpty($this->hydrator->extract(new ClassWithPublicStaticProperties())); + } +} diff --git a/test/TestAsset/ClassWithPublicStaticProperties.php b/test/TestAsset/ClassWithPublicStaticProperties.php new file mode 100644 index 000000000..9c53b0e8e --- /dev/null +++ b/test/TestAsset/ClassWithPublicStaticProperties.php @@ -0,0 +1,28 @@ +bar = "foo"; $this->blubb = "baz"; $this->quo = "blubb"; + $this->quin = 'five'; } }