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 150fdf6..fdd43c6 100644 --- a/src/MetaCast.php +++ b/src/MetaCast.php @@ -8,9 +8,10 @@ use Jasny\Meta\MetaClass; use Jasny\TypeCastInterface; use \InvalidArgumentException; +use function Jasny\expect_type; /** - * Cast data to class + * Cast data to class using class metadata */ class MetaCast { @@ -38,22 +39,32 @@ public function __construct(FactoryInterface $metaFactory, TypeCastInterface $ty $this->typeCast = $typeCast; } + /** + * Use object as callable + * + * @param string|object $class + * @param array|object $data + * @return array|object + */ + final public function __invoke($class, $data) + { + return $this->cast($class, $data); + } + /** * Cast data to given class * - * @param string $class + * @param string|object $class * @param array|object $data - * @return object + * @return array|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_array($data)) { - $data = (object)$data; + if (is_object($class)) { + $class = get_class($class); } if (is_a($data, $class)) { @@ -61,32 +72,44 @@ public function cast(string $class, $data) } $meta = $this->metaFactory->forClass($class); - $data = $this->castProperties($meta, $data); + $handlers = $this->getHandlers($meta, $data); + $caster = $this->getDataCaster($handlers); - return $this->typeCast->to($class)->cast($data); + return $caster->cast($data); } /** - * Cast class properties + * Get cast handlers * * @param MetaClass $meta - * @param object $data - * @return object + * @param array|object $data + * @return array */ - protected function castProperties(MetaClass $meta, $data) + protected function getHandlers(MetaClass $meta, $data): array { - $data = clone $data; + $handlers = []; $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) { + $handlers[$name] = $this->typeCast->to($toType); + } } - return $data; + return $handlers; + } + + /** + * Get instance of data caster + * + * @codeCoverageIgnore + * @param array $handlers + * @return DataCast + */ + protected function getDataCaster(array $handlers): DataCast + { + 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 d2a52c4..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 */ @@ -47,13 +50,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 */ @@ -83,9 +115,16 @@ public function castProvider() 'baz' => 'value3' ]; + $expected = [ + 'foo' => 'casted_value1', + 'bar' => 'value2', + 'baz' => 'casted_value3' + ]; + return [ - [$data], - [(object)$data], + ['Foo', 'Foo', $data, $expected], + ['Foo', 'Foo', (object)$data, (object)$expected], + [new \stdClass(), \stdClass::class, $data, $expected], ]; } @@ -94,11 +133,12 @@ public function castProvider() * * @dataProvider castProvider */ - public function testCast($data) + public function testCast($classParam, $class, $data, $expected) { - $class = 'Foo'; $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); @@ -120,27 +160,28 @@ public function testCast($data) 'pir' => $property5 ]; - $castedProperties = (object)[ - 'foo' => 'casted_value1', - 'bar' => 'value2', - 'baz' => 'casted_value3' + $handlers = [ + 'foo' => $castHandler1, + 'zoo' => $castHandler2, + 'baz' => $castHandler3 ]; - $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); + ->withConsecutive(['type1'], ['type2'], ['type3']) + ->willReturnOnConsecutiveCalls($castHandler1, $castHandler2, $castHandler3); - $castHandler->expects($this->exactly(3))->method('cast') - ->withConsecutive(['value1'], ['value3'], [$castedProperties]) - ->willReturnOnConsecutiveCalls('casted_value1', 'casted_value3', $expected); + $metaCast = $this->createPartialMock(MetaCast::class, ['getDataCaster']); + $this->setPrivateProperty($metaCast, 'metaFactory', $this->metaFactory); + $this->setPrivateProperty($metaCast, 'typeCast', $this->typeCast); - $metaCast = new MetaCast($this->metaFactory, $this->typeCast); - $result = $metaCast->cast('Foo', $data); + $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); + + $result = $metaCast->cast($classParam, $data); $this->assertEquals($expected, $result); } @@ -153,18 +194,36 @@ public function testCastNoProperties() $class = 'Foo'; $data = ['foo' => 'bar']; $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); + $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); } + + /** + * 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); + } }