Skip to content

Commit

Permalink
Expect::from() works with class names
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Apr 28, 2024
1 parent 1abd2c1 commit 5e5aed6
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 13 deletions.
41 changes: 28 additions & 13 deletions src/Schema/Expect.php
Expand Up @@ -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();
}
}

Expand Down
File renamed without changes.
109 changes: 109 additions & 0 deletions tests/Schema/Expect.from.static.phpt
@@ -0,0 +1,109 @@
<?php

declare(strict_types=1);

use Nette\Schema\Elements\Structure;
use Nette\Schema\Expect;
use Nette\Schema\Processor;
use Tester\Assert;


require __DIR__ . '/../bootstrap.php';


Assert::with(Structure::class, function () {
$schema = Expect::from(stdClass::class);

Assert::type(Structure::class, $schema);
Assert::same([], $schema->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);
});

0 comments on commit 5e5aed6

Please sign in to comment.