Skip to content

Commit f5c9ef2

Browse files
committed
Expect::from() works with class names
1 parent eb3a5c3 commit f5c9ef2

3 files changed

Lines changed: 139 additions & 15 deletions

File tree

src/Schema/Expect.php

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -71,32 +71,49 @@ public static function structure(array $shape): Structure
7171

7272

7373
/**
74-
* Generates a structure schema from a class instance by reflecting its properties or constructor parameters.
75-
* @param array<string, Schema> $items Optional overrides for specific properties.
74+
* Generates a structure schema from a class by reflecting its properties or constructor parameters.
75+
* @param class-string|object $object
76+
* @param array<string, Schema> $items
7677
*/
77-
public static function from(object $object, array $items = []): Structure
78+
public static function from(object|string $object, array $items = []): Structure
7879
{
79-
$ro = new \ReflectionObject($object);
80+
$ro = new \ReflectionClass($object);
8081
$props = $ro->hasMethod('__construct')
8182
? $ro->getMethod('__construct')->getParameters()
8283
: $ro->getProperties();
8384

8485
foreach ($props as $prop) {
8586
$name = $prop->getName();
86-
if (!isset($items[$name])) {
87-
$item = new Type((string) (Nette\Utils\Type::fromReflection($prop) ?? 'mixed'));
88-
if ($prop instanceof \ReflectionProperty ? $prop->isInitialized($object) : $prop->isOptional()) {
89-
$def = ($prop instanceof \ReflectionProperty ? $prop->getValue($object) : $prop->getDefaultValue());
90-
if (is_object($def)) {
91-
$item = static::from($def);
92-
} else {
93-
$item->default($def);
94-
}
87+
if (isset($items[$name])) {
88+
continue;
89+
}
90+
91+
$item = new Type($propType = (string) (Nette\Utils\Type::fromReflection($prop) ?? 'mixed'));
92+
if (class_exists($propType)) {
93+
$item = static::from($propType);
94+
}
95+
96+
$hasDefault = match (true) {
97+
$prop instanceof \ReflectionParameter => $prop->isOptional(),
98+
is_object($object) => $prop->isInitialized($object),
99+
default => $prop->hasDefaultValue(),
100+
};
101+
if ($hasDefault) {
102+
$default = match (true) {
103+
$prop instanceof \ReflectionParameter => $prop->getDefaultValue(),
104+
is_object($object) => $prop->getValue($object),
105+
default => $prop->getDefaultValue(),
106+
};
107+
if (is_object($default)) {
108+
$item = static::from($default);
95109
} else {
96-
$item->required();
110+
$item->default($default);
97111
}
98-
$items[$name] = $item;
112+
} else {
113+
$item->required();
99114
}
115+
116+
$items[$name] = $item;
100117
}
101118

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

0 commit comments

Comments
 (0)