Skip to content

Commit 793adc6

Browse files
committed
Expect::from() works with class names
1 parent 72a91bd commit 793adc6

File tree

3 files changed

+137
-13
lines changed

3 files changed

+137
-13
lines changed

src/Schema/Expect.php

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,27 +63,42 @@ public static function structure(array $shape): Structure
6363
}
6464

6565

66-
public static function from(object $object, array $items = []): Structure
66+
public static function from(object|string $object, array $items = []): Structure
6767
{
68-
$ro = new \ReflectionObject($object);
68+
$ro = new \ReflectionClass($object);
6969
$props = $ro->hasMethod('__construct')
7070
? $ro->getMethod('__construct')->getParameters()
7171
: $ro->getProperties();
7272

7373
foreach ($props as $prop) {
74-
$item = &$items[$prop->getName()];
75-
if (!$item) {
76-
$item = new Type((string) (Nette\Utils\Type::fromReflection($prop) ?? 'mixed'));
77-
if ($prop instanceof \ReflectionProperty ? $prop->isInitialized($object) : $prop->isOptional()) {
78-
$def = ($prop instanceof \ReflectionProperty ? $prop->getValue($object) : $prop->getDefaultValue());
79-
if (is_object($def)) {
80-
$item = static::from($def);
81-
} else {
82-
$item->default($def);
83-
}
74+
\assert($prop instanceof \ReflectionProperty || $prop instanceof \ReflectionParameter);
75+
if ($item = &$items[$prop->getName()]) {
76+
continue;
77+
}
78+
79+
$item = new Type($propType = (string) (Nette\Utils\Type::fromReflection($prop) ?? 'mixed'));
80+
if (class_exists($propType)) {
81+
$item = static::from($propType);
82+
}
83+
84+
$hasDefault = match (true) {
85+
$prop instanceof \ReflectionParameter => $prop->isOptional(),
86+
is_object($object) => $prop->isInitialized($object),
87+
default => $prop->hasDefaultValue(),
88+
};
89+
if ($hasDefault) {
90+
$default = match (true) {
91+
$prop instanceof \ReflectionParameter => $prop->getDefaultValue(),
92+
is_object($object) => $prop->getValue($object),
93+
default => $prop->getDefaultValue(),
94+
};
95+
if (is_object($default)) {
96+
$item = static::from($default);
8497
} else {
85-
$item->required();
98+
$item->default($default);
8699
}
100+
} else {
101+
$item->required();
87102
}
88103
}
89104

File renamed without changes.
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Nette\Schema\Elements\Structure;
6+
use Nette\Schema\Expect;
7+
use Nette\Schema\Processor;
8+
use Tester\Assert;
9+
10+
11+
require __DIR__ . '/../bootstrap.php';
12+
13+
14+
Assert::with(Structure::class, function () {
15+
$schema = Expect::from(stdClass::class);
16+
17+
Assert::type(Structure::class, $schema);
18+
Assert::same([], $schema->items);
19+
Assert::type(stdClass::class, (new Processor)->process($schema, []));
20+
});
21+
22+
23+
Assert::with(Structure::class, function () {
24+
class Data1
25+
{
26+
public string $dsn = 'mysql';
27+
public ?string $user;
28+
public ?string $password = null;
29+
public array|int $options = [];
30+
public bool $debugger = true;
31+
public mixed $mixed;
32+
public array $arr = [1];
33+
}
34+
35+
$schema = Expect::from(Data1::class);
36+
37+
Assert::type(Structure::class, $schema);
38+
Assert::equal([
39+
'dsn' => Expect::string('mysql'),
40+
'user' => Expect::type('?string')->required(),
41+
'password' => Expect::type('?string'),
42+
'options' => Expect::type('array|int')->default([]),
43+
'debugger' => Expect::bool(true),
44+
'mixed' => Expect::mixed()->required(),
45+
'arr' => Expect::type('array')->default([1]),
46+
], $schema->items);
47+
Assert::type(Data1::class, (new Processor)->process($schema, ['user' => '', 'mixed' => '']));
48+
});
49+
50+
51+
Assert::with(Structure::class, function () { // constructor injection
52+
class Data2
53+
{
54+
public function __construct(
55+
public ?string $user,
56+
public ?string $password = null,
57+
) {
58+
}
59+
}
60+
61+
$schema = Expect::from(Data2::class);
62+
63+
Assert::type(Structure::class, $schema);
64+
Assert::equal([
65+
'user' => Expect::type('?string')->required(),
66+
'password' => Expect::type('?string'),
67+
], $schema->items);
68+
Assert::equal(
69+
new Data2('foo', 'bar'),
70+
(new Processor)->process($schema, ['user' => 'foo', 'password' => 'bar']),
71+
);
72+
});
73+
74+
75+
Assert::with(Structure::class, function () { // overwritten item
76+
class Data3
77+
{
78+
public string $dsn = 'mysql';
79+
public ?string $user;
80+
}
81+
82+
$schema = Expect::from(Data3::class, ['dsn' => Expect::int(123)]);
83+
84+
Assert::equal([
85+
'dsn' => Expect::int(123),
86+
'user' => Expect::type('?string')->required(),
87+
], $schema->items);
88+
});
89+
90+
91+
Assert::with(Structure::class, function () { // nested object
92+
class Data4
93+
{
94+
public Data5 $inner;
95+
}
96+
97+
class Data5
98+
{
99+
public string $name;
100+
}
101+
102+
$schema = Expect::from(Data4::class);
103+
104+
Assert::equal([
105+
'inner' => Expect::structure([
106+
'name' => Expect::string()->required(),
107+
])->castTo(Data5::class),
108+
], $schema->items);
109+
});

0 commit comments

Comments
 (0)