From ad8317965a36db1612c94d8c942ea303a5ef997c Mon Sep 17 00:00:00 2001 From: Sergey Boltonosov Date: Sun, 30 Sep 2018 00:51:43 +0700 Subject: [PATCH 1/5] Don't cast data to object (fixes #2) --- src/MetaCast.php | 20 ++++++++++---------- tests/MetaCastTest.php | 36 ++++++++++++++++-------------------- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/src/MetaCast.php b/src/MetaCast.php index 150fdf6..9a13e22 100644 --- a/src/MetaCast.php +++ b/src/MetaCast.php @@ -52,10 +52,6 @@ public function cast(string $class, $data) throw new InvalidArgumentException("Can not cast '$type' to '$class': expected object or array"); } - if (is_array($data)) { - $data = (object)$data; - } - if (is_a($data, $class)) { return clone $data; } @@ -63,7 +59,7 @@ public function cast(string $class, $data) $meta = $this->metaFactory->forClass($class); $data = $this->castProperties($meta, $data); - return $this->typeCast->to($class)->cast($data); + return $data; } /** @@ -75,18 +71,22 @@ public function cast(string $class, $data) */ protected function castProperties(MetaClass $meta, $data) { + $isArray = is_array($data); + if ($isArray) { + $data = (object)$data; + } + $data = clone $data; $properties = $meta->getProperties(); foreach ($properties as $name => $item) { $toType = $item->get('type'); - if (!$toType || !isset($data->$name)) { - continue; - } - $data->$name = $this->typeCast->to($toType)->cast($data->$name); + if ($toType && isset($data->$name)) { + $data->$name = $this->typeCast->to($toType)->cast($data->$name); + } } - return $data; + return $isArray ? (array)$data : $data; } } diff --git a/tests/MetaCastTest.php b/tests/MetaCastTest.php index d2a52c4..ebb1cd0 100644 --- a/tests/MetaCastTest.php +++ b/tests/MetaCastTest.php @@ -83,9 +83,15 @@ public function castProvider() 'baz' => 'value3' ]; + $expected = [ + 'foo' => 'casted_value1', + 'bar' => 'value2', + 'baz' => 'casted_value3' + ]; + return [ - [$data], - [(object)$data], + [$data, $expected], + [(object)$data, (object)$expected], ]; } @@ -94,7 +100,7 @@ public function castProvider() * * @dataProvider castProvider */ - public function testCast($data) + public function testCast($data, $expected) { $class = 'Foo'; $meta = $this->createMock(MetaClass::class); @@ -120,24 +126,16 @@ public function testCast($data) 'pir' => $property5 ]; - $castedProperties = (object)[ - 'foo' => 'casted_value1', - 'bar' => 'value2', - 'baz' => 'casted_value3' - ]; - - $expected = (object)$castedProperties; - $this->metaFactory->expects($this->once())->method('forClass')->with($class)->willReturn($meta); $meta->expects($this->once())->method('getProperties')->willReturn($properties); - $this->typeCast->expects($this->exactly(3))->method('to') - ->withConsecutive(['type1'], ['type3'], [$class]) - ->willReturnOnConsecutiveCalls($castHandler, $castHandler, $castHandler); + $this->typeCast->expects($this->exactly(2))->method('to') + ->withConsecutive(['type1'], ['type3']) + ->willReturnOnConsecutiveCalls($castHandler, $castHandler); - $castHandler->expects($this->exactly(3))->method('cast') - ->withConsecutive(['value1'], ['value3'], [$castedProperties]) - ->willReturnOnConsecutiveCalls('casted_value1', 'casted_value3', $expected); + $castHandler->expects($this->exactly(2))->method('cast') + ->withConsecutive(['value1'], ['value3']) + ->willReturnOnConsecutiveCalls('casted_value1', 'casted_value3'); $metaCast = new MetaCast($this->metaFactory, $this->typeCast); $result = $metaCast->cast('Foo', $data); @@ -155,12 +153,10 @@ public function testCastNoProperties() $meta = $this->createMock(MetaClass::class); $castHandler = $this->createMock(HandlerInterface::class); - $expected = (object)$data; + $expected = $data; $this->metaFactory->expects($this->once())->method('forClass')->with($class)->willReturn($meta); $meta->expects($this->once())->method('getProperties')->willReturn([]); - $this->typeCast->expects($this->once())->method('to')->with($class)->willReturn($castHandler); - $castHandler->expects($this->once())->method('cast')->with((object)$data)->willReturn($expected); $metaCast = new MetaCast($this->metaFactory, $this->typeCast); $result = $metaCast->cast('Foo', $data); From 284d3401295977cd5450f467eaa31454ef47fc01 Mon Sep 17 00:00:00 2001 From: Sergey Boltonosov Date: Sun, 30 Sep 2018 02:21:21 +0700 Subject: [PATCH 2/5] Make MetaCast invokable (fixes #3) --- src/MetaCast.php | 12 ++++++++++++ tests/MetaCastTest.php | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/MetaCast.php b/src/MetaCast.php index 150fdf6..3f4f885 100644 --- a/src/MetaCast.php +++ b/src/MetaCast.php @@ -38,6 +38,18 @@ public function __construct(FactoryInterface $metaFactory, TypeCastInterface $ty $this->typeCast = $typeCast; } + /** + * Use object as callable + * + * @param string $class + * @param array|object $data + * @return object + */ + final public function __invoke(string $class, $data) + { + return $this->cast($class, $data); + } + /** * Cast data to given class * diff --git a/tests/MetaCastTest.php b/tests/MetaCastTest.php index d2a52c4..113bdf6 100644 --- a/tests/MetaCastTest.php +++ b/tests/MetaCastTest.php @@ -167,4 +167,19 @@ public function testCastNoProperties() $this->assertEquals($expected, $result); } + + /** + * Test '__invoke' method + */ + public function testInvoke() + { + $class = 'Foo'; + $data = []; + $expected = (object)[]; + + $metaCast = $this->createMock(MetaCast::class); + $metaCast->expects($this->once())->method('cast')->with($class, $data)->willReturn($expected); + + $result = $metaCast($class, $data); + } } From 4eb508ef74b8a03a12b42cbd23c61a8b292d1bac Mon Sep 17 00:00:00 2001 From: Sergey Boltonosov Date: Sun, 30 Sep 2018 02:47:26 +0700 Subject: [PATCH 3/5] Allow object as class parameter (fixes #4) --- src/MetaCast.php | 17 ++++++++++------- tests/MetaCastTest.php | 43 +++++++++++++++++++++++++++++++++++------- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/MetaCast.php b/src/MetaCast.php index 5e45fcb..586a2b8 100644 --- a/src/MetaCast.php +++ b/src/MetaCast.php @@ -8,6 +8,7 @@ use Jasny\Meta\MetaClass; use Jasny\TypeCastInterface; use \InvalidArgumentException; +use function Jasny\expect_type; /** * Cast data to class @@ -41,11 +42,11 @@ public function __construct(FactoryInterface $metaFactory, TypeCastInterface $ty /** * Use object as callable * - * @param string $class + * @param string|object $class * @param array|object $data * @return object */ - final public function __invoke(string $class, $data) + final public function __invoke($class, $data) { return $this->cast($class, $data); } @@ -53,15 +54,17 @@ final public function __invoke(string $class, $data) /** * Cast data to given class * - * @param string $class + * @param string|object $class * @param array|object $data * @return object */ - public function cast(string $class, $data) + public function cast($class, $data) { - if (!is_array($data) && !is_object($data)) { - $type = gettype($data); - throw new InvalidArgumentException("Can not cast '$type' to '$class': expected object or array"); + expect_type($class, ['string', 'object']); + expect_type($data, ['array', 'object']); + + if (is_object($class)) { + $class = get_class($class); } if (is_a($data, $class)) { diff --git a/tests/MetaCastTest.php b/tests/MetaCastTest.php index 83f6539..cc80e4f 100644 --- a/tests/MetaCastTest.php +++ b/tests/MetaCastTest.php @@ -47,13 +47,42 @@ public function castPrimitiveProvider() */ public function testCastPrimitive($data, $type) { - $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage("Can not cast '$type' to 'Foo': expected object or array"); + $this->expectException(\TypeError::class); + $this->expectExceptionMessage("Expected array or object, $type given"); $metaCast = new MetaCast($this->metaFactory, $this->typeCast); $result = $metaCast->cast('Foo', $data); } + /** + * Provide data for testing 'cast' method, if $class parameter is of wrong type + * + * @return array + */ + public function castClassWrongTypeProvider() + { + return [ + [12, 'integer'], + [true, 'boolean'], + [[], 'array'], + [null, 'NULL'], + ]; + } + + /** + * Test 'cast' method, if $class parameter is of wrong type + * + * @dataProvider castClassWrongTypeProvider + */ + public function testCastClassWrongType($class, $type) + { + $this->expectException(\TypeError::class); + $this->expectExceptionMessage("Expected string or object, $type given"); + + $metaCast = new MetaCast($this->metaFactory, $this->typeCast); + $result = $metaCast->cast($class, []); + } + /** * Test 'cast' method, if data is an instance of the same class */ @@ -90,8 +119,9 @@ public function castProvider() ]; return [ - [$data, $expected], - [(object)$data, (object)$expected], + ['Foo', 'Foo', $data, $expected], + ['Foo', 'Foo', (object)$data, (object)$expected], + [new \stdClass(), \stdClass::class, $data, $expected], ]; } @@ -100,9 +130,8 @@ public function castProvider() * * @dataProvider castProvider */ - public function testCast($data, $expected) + public function testCast($classParam, $class, $data, $expected) { - $class = 'Foo'; $meta = $this->createMock(MetaClass::class); $castHandler = $this->createMock(HandlerInterface::class); @@ -138,7 +167,7 @@ public function testCast($data, $expected) ->willReturnOnConsecutiveCalls('casted_value1', 'casted_value3'); $metaCast = new MetaCast($this->metaFactory, $this->typeCast); - $result = $metaCast->cast('Foo', $data); + $result = $metaCast->cast($classParam, $data); $this->assertEquals($expected, $result); } From a9cba806c65eadc16d8ad377aa91f213b2d4384f Mon Sep 17 00:00:00 2001 From: Sergey Boltonosov Date: Sun, 30 Sep 2018 15:04:52 +0700 Subject: [PATCH 4/5] Added DataCast (fixes #5) --- src/DataCast.php | 54 ++++++++++++++++++++++++ src/MetaCast.php | 42 +++++++++++-------- tests/DataCastTest.php | 93 ++++++++++++++++++++++++++++++++++++++++++ tests/MetaCastTest.php | 41 ++++++++++++++----- 4 files changed, 202 insertions(+), 28 deletions(-) create mode 100644 src/DataCast.php create mode 100644 tests/DataCastTest.php diff --git a/src/DataCast.php b/src/DataCast.php new file mode 100644 index 0000000..b8977d0 --- /dev/null +++ b/src/DataCast.php @@ -0,0 +1,54 @@ +handlers = $handlers; + } + + /** + * Cast data using handlers + * + * @param array|object $data + * @return array|object + */ + public function cast($data) + { + expect_type($data, ['array', 'object']); + + if ($isArray = is_array($data)) { + $data = (object)$data; + } + + $data = clone $data; + + foreach ($this->handlers as $name => $handler) { + if (isset($data->$name)) { + $data->$name = $handler->cast($data->$name); + } + } + + return $isArray ? (array)$data : $data; + } +} diff --git a/src/MetaCast.php b/src/MetaCast.php index 586a2b8..acb0154 100644 --- a/src/MetaCast.php +++ b/src/MetaCast.php @@ -11,7 +11,7 @@ use function Jasny\expect_type; /** - * Cast data to class + * Cast data to class using class metadata */ class MetaCast { @@ -44,7 +44,7 @@ public function __construct(FactoryInterface $metaFactory, TypeCastInterface $ty * * @param string|object $class * @param array|object $data - * @return object + * @return array|object */ final public function __invoke($class, $data) { @@ -56,7 +56,7 @@ final public function __invoke($class, $data) * * @param string|object $class * @param array|object $data - * @return object + * @return array|object */ public function cast($class, $data) { @@ -72,36 +72,44 @@ public function cast($class, $data) } $meta = $this->metaFactory->forClass($class); - $data = $this->castProperties($meta, $data); + $handlers = $this->getHandlers($meta, $data); + $caster = $this->getDataCaster($handlers); - return $data; + return $caster->cast($data); } /** - * Cast class properties + * Get cast handlers * * @param MetaClass $meta * @param object $data - * @return object + * @return array */ - protected function castProperties(MetaClass $meta, $data) + protected function getHandlers(MetaClass $meta, $data) { - $isArray = is_array($data); - if ($isArray) { - $data = (object)$data; - } - - $data = clone $data; + $handlers = []; $properties = $meta->getProperties(); foreach ($properties as $name => $item) { $toType = $item->get('type'); - if ($toType && isset($data->$name)) { - $data->$name = $this->typeCast->to($toType)->cast($data->$name); + if ($toType) { + $handlers[$name] = $this->typeCast->to($toType); } } - return $isArray ? (array)$data : $data; + return $handlers; + } + + /** + * Get instance of data caster + * + * @codeCoverageIgnore + * @param array $handlers + * @return DataCast + */ + protected function getDataCaster(array $handlers) + { + return new DataCast($handlers); } } diff --git a/tests/DataCastTest.php b/tests/DataCastTest.php new file mode 100644 index 0000000..2731329 --- /dev/null +++ b/tests/DataCastTest.php @@ -0,0 +1,93 @@ + 'foo_value', + 'baz' => 'baz_value' + ]; + + $expected = [ + 'foo' => 'foo_casted', + 'baz' => 'baz_casted' + ]; + + return [ + [$data, $expected], + [(object)$data, (object)$expected] + ]; + } + + /** + * Test 'cast' method + * + * @dataProvider castProvider + */ + public function testCast($data, $expected) + { + $handlers = [ + 'foo' => $this->createMock(HandlerInterface::class), + 'bar' => $this->createMock(HandlerInterface::class), + 'baz' => $this->createMock(HandlerInterface::class), + ]; + + $handlers['foo']->expects($this->once())->method('cast')->with('foo_value')->willReturn('foo_casted'); + $handlers['baz']->expects($this->once())->method('cast')->with('baz_value')->willReturn('baz_casted'); + + $dataCast = new DataCast($handlers); + $result = $dataCast->cast($data); + + $this->assertEquals($expected, $result); + } + + /** + * Provide data for testing 'cast' method for promitive values + * + * @return array + */ + public function castPrimitiveProvider() + { + return [ + [12, 'integer'], + ['foo', 'string'], + [true, 'boolean'], + [null, 'NULL'], + ]; + } + + /** + * Test 'cast' method for primitive value + * + * @dataProvider castPrimitiveProvider + */ + public function testCastPrimitive($data, $type) + { + $this->expectException(\TypeError::class); + $this->expectExceptionMessage("Expected array or object, $type given"); + + $dataCast = new DataCast([]); + $dataCast->cast($data); + } +} diff --git a/tests/MetaCastTest.php b/tests/MetaCastTest.php index cc80e4f..4231c66 100644 --- a/tests/MetaCastTest.php +++ b/tests/MetaCastTest.php @@ -6,6 +6,7 @@ use Jasny\Meta\MetaClass; use Jasny\Meta\MetaProperty; use Jasny\MetaCast\MetaCast; +use Jasny\MetaCast\DataCast; use Jasny\TypeCastInterface; use Jasny\TypeCast\HandlerInterface; use PHPUnit\Framework\TestCase; @@ -16,6 +17,8 @@ */ class MetaCastTest extends TestCase { + use \Jasny\TestHelper; + /** * Set up dependencies before each test case */ @@ -133,7 +136,9 @@ public function castProvider() public function testCast($classParam, $class, $data, $expected) { $meta = $this->createMock(MetaClass::class); - $castHandler = $this->createMock(HandlerInterface::class); + $castHandler1 = $this->createMock(HandlerInterface::class); + $castHandler2 = $this->createMock(HandlerInterface::class); + $castHandler3 = $this->createMock(HandlerInterface::class); $property1 = $this->createMock(MetaProperty::class); $property2 = $this->createMock(MetaProperty::class); @@ -155,18 +160,27 @@ public function testCast($classParam, $class, $data, $expected) 'pir' => $property5 ]; + $handlers = [ + 'foo' => $castHandler1, + 'zoo' => $castHandler2, + 'baz' => $castHandler3 + ]; + $this->metaFactory->expects($this->once())->method('forClass')->with($class)->willReturn($meta); $meta->expects($this->once())->method('getProperties')->willReturn($properties); - $this->typeCast->expects($this->exactly(2))->method('to') - ->withConsecutive(['type1'], ['type3']) - ->willReturnOnConsecutiveCalls($castHandler, $castHandler); + $this->typeCast->expects($this->exactly(3))->method('to') + ->withConsecutive(['type1'], ['type2'], ['type3']) + ->willReturnOnConsecutiveCalls($castHandler1, $castHandler2, $castHandler3); - $castHandler->expects($this->exactly(2))->method('cast') - ->withConsecutive(['value1'], ['value3']) - ->willReturnOnConsecutiveCalls('casted_value1', 'casted_value3'); + $metaCast = $this->createPartialMock(MetaCast::class, ['getDataCaster']); + $this->setPrivateProperty($metaCast, 'metaFactory', $this->metaFactory); + $this->setPrivateProperty($metaCast, 'typeCast', $this->typeCast); + + $dataCast = $this->createMock(DataCast::class); + $metaCast->expects($this->once())->method('getDataCaster')->with($handlers)->willReturn($dataCast); + $dataCast->expects($this->once())->method('cast')->with($data)->willReturn($expected); - $metaCast = new MetaCast($this->metaFactory, $this->typeCast); $result = $metaCast->cast($classParam, $data); $this->assertEquals($expected, $result); @@ -180,14 +194,19 @@ public function testCastNoProperties() $class = 'Foo'; $data = ['foo' => 'bar']; $meta = $this->createMock(MetaClass::class); - $castHandler = $this->createMock(HandlerInterface::class); - $expected = $data; $this->metaFactory->expects($this->once())->method('forClass')->with($class)->willReturn($meta); $meta->expects($this->once())->method('getProperties')->willReturn([]); - $metaCast = new MetaCast($this->metaFactory, $this->typeCast); + $metaCast = $this->createPartialMock(MetaCast::class, ['getDataCaster']); + $this->setPrivateProperty($metaCast, 'metaFactory', $this->metaFactory); + $this->setPrivateProperty($metaCast, 'typeCast', $this->typeCast); + + $dataCast = $this->createMock(DataCast::class); + $metaCast->expects($this->once())->method('getDataCaster')->with([])->willReturn($dataCast); + $dataCast->expects($this->once())->method('cast')->with($data)->willReturn($expected); + $result = $metaCast->cast('Foo', $data); $this->assertEquals($expected, $result); From 1cc6cb2c2a720c0f009797871eaf7bfcc7015fe4 Mon Sep 17 00:00:00 2001 From: Sergey Boltonosov Date: Mon, 1 Oct 2018 21:11:07 +0700 Subject: [PATCH 5/5] Improve type-hints --- src/MetaCast.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/MetaCast.php b/src/MetaCast.php index acb0154..fdd43c6 100644 --- a/src/MetaCast.php +++ b/src/MetaCast.php @@ -82,10 +82,10 @@ public function cast($class, $data) * Get cast handlers * * @param MetaClass $meta - * @param object $data + * @param array|object $data * @return array */ - protected function getHandlers(MetaClass $meta, $data) + protected function getHandlers(MetaClass $meta, $data): array { $handlers = []; $properties = $meta->getProperties(); @@ -108,7 +108,7 @@ protected function getHandlers(MetaClass $meta, $data) * @param array $handlers * @return DataCast */ - protected function getDataCaster(array $handlers) + protected function getDataCaster(array $handlers): DataCast { return new DataCast($handlers); }