From 5e5aed6abab0bbd06539aae3063770c2eee63a07 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 28 Apr 2024 23:11:29 +0200 Subject: [PATCH] Expect::from() works with class names --- src/Schema/Expect.php | 41 ++++--- ...ect.from.phpt => Expect.from.dynamic.phpt} | 0 tests/Schema/Expect.from.static.phpt | 109 ++++++++++++++++++ 3 files changed, 137 insertions(+), 13 deletions(-) rename tests/Schema/{Expect.from.phpt => Expect.from.dynamic.phpt} (100%) create mode 100644 tests/Schema/Expect.from.static.phpt diff --git a/src/Schema/Expect.php b/src/Schema/Expect.php index 0769bad..784caab 100644 --- a/src/Schema/Expect.php +++ b/src/Schema/Expect.php @@ -64,27 +64,42 @@ public static function structure(array $items): Structure } - public static function from(object $object, array $items = []): Structure + public static function from(object|string $object, array $items = []): Structure { - $ro = new \ReflectionObject($object); + $ro = new \ReflectionClass($object); $props = $ro->hasMethod('__construct') ? $ro->getMethod('__construct')->getParameters() : $ro->getProperties(); foreach ($props as $prop) { - $item = &$items[$prop->getName()]; - if (!$item) { - $item = new Type((string) (Nette\Utils\Type::fromReflection($prop) ?? 'mixed')); - if ($prop instanceof \ReflectionProperty ? $prop->isInitialized($object) : $prop->isOptional()) { - $def = ($prop instanceof \ReflectionProperty ? $prop->getValue($object) : $prop->getDefaultValue()); - if (is_object($def)) { - $item = static::from($def); - } else { - $item->default($def); - } + \assert($prop instanceof \ReflectionProperty || $prop instanceof \ReflectionParameter); + if ($item = &$items[$prop->getName()]) { + continue; + } + + $item = new Type($propType = (string) (Nette\Utils\Type::fromReflection($prop) ?? 'mixed')); + if (class_exists($propType)) { + $item = static::from($propType); + } + + $hasDefault = match (true) { + $prop instanceof \ReflectionParameter => $prop->isOptional(), + is_object($object) => $prop->isInitialized($object), + default => $prop->hasDefaultValue(), + }; + if ($hasDefault) { + $default = match (true) { + $prop instanceof \ReflectionParameter => $prop->getDefaultValue(), + is_object($object) => $prop->getValue($object), + default => $prop->getDefaultValue(), + }; + if (is_object($default)) { + $item = static::from($default); } else { - $item->required(); + $item->default($default); } + } else { + $item->required(); } } diff --git a/tests/Schema/Expect.from.phpt b/tests/Schema/Expect.from.dynamic.phpt similarity index 100% rename from tests/Schema/Expect.from.phpt rename to tests/Schema/Expect.from.dynamic.phpt diff --git a/tests/Schema/Expect.from.static.phpt b/tests/Schema/Expect.from.static.phpt new file mode 100644 index 0000000..581a4f3 --- /dev/null +++ b/tests/Schema/Expect.from.static.phpt @@ -0,0 +1,109 @@ +items); + Assert::type(stdClass::class, (new Processor)->process($schema, [])); +}); + + +Assert::with(Structure::class, function () { + class Data1 + { + public string $dsn = 'mysql'; + public ?string $user; + public ?string $password = null; + public array|int $options = []; + public bool $debugger = true; + public mixed $mixed; + public array $arr = [1]; + } + + $schema = Expect::from(Data1::class); + + Assert::type(Structure::class, $schema); + Assert::equal([ + 'dsn' => Expect::string('mysql'), + 'user' => Expect::type('?string')->required(), + 'password' => Expect::type('?string'), + 'options' => Expect::type('array|int')->default([]), + 'debugger' => Expect::bool(true), + 'mixed' => Expect::mixed()->required(), + 'arr' => Expect::type('array')->default([1]), + ], $schema->items); + Assert::type(Data1::class, (new Processor)->process($schema, ['user' => '', 'mixed' => ''])); +}); + + +Assert::with(Structure::class, function () { // constructor injection + class Data2 + { + public function __construct( + public ?string $user, + public ?string $password = null, + ) { + } + } + + $schema = Expect::from(Data2::class); + + Assert::type(Structure::class, $schema); + Assert::equal([ + 'user' => Expect::type('?string')->required(), + 'password' => Expect::type('?string'), + ], $schema->items); + Assert::equal( + new Data2('foo', 'bar'), + (new Processor)->process($schema, ['user' => 'foo', 'password' => 'bar']), + ); +}); + + +Assert::with(Structure::class, function () { // overwritten item + class Data3 + { + public string $dsn = 'mysql'; + public ?string $user; + } + + $schema = Expect::from(Data3::class, ['dsn' => Expect::int(123)]); + + Assert::equal([ + 'dsn' => Expect::int(123), + 'user' => Expect::type('?string')->required(), + ], $schema->items); +}); + + +Assert::with(Structure::class, function () { // nested object + class Data4 + { + public Data5 $inner; + } + + class Data5 + { + public string $name; + } + + $schema = Expect::from(Data4::class); + + Assert::equal([ + 'inner' => Expect::structure([ + 'name' => Expect::string()->required(), + ])->castTo(Data5::class), + ], $schema->items); +});