This commit is contained in:
icefox 2026-02-23 21:35:08 -03:00
parent 367858c97c
commit 6b1a385292
No known key found for this signature in database
13 changed files with 191 additions and 80 deletions

View file

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Icefox\DTO;
use Icefox\DTO\Support\RuleFactory;
use Icefox\DTO\RuleFactory;
use Illuminate\Support\Collection;
use phpDocumentor\Reflection\PseudoTypes\Generic;

View file

@ -1,17 +1,16 @@
<?php
namespace Icefox\DTO\Factories;
namespace Icefox\DTO;
use Icefox\DTO\ParameterMeta;
use Icefox\DTO\Support\RuleFactory;
use Icefox\DTO\RuleFactory;
use phpDocumentor\Reflection\PseudoTypes\Generic;
class CollectionFactory
class CustomHandlers
{
/**
* @return array<string,string[]>
*/
public static function rules(ParameterMeta $parameter, RuleFactory $factory): array
public static function CollectionRules(ParameterMeta $parameter, RuleFactory $factory): array
{
if (is_null($parameter->tag)) {
return [];
@ -36,3 +35,4 @@ class CollectionFactory
);
}
}

View file

@ -4,14 +4,14 @@ namespace Icefox\DTO;
use Icefox\DTO\Attributes\FromInput;
use Icefox\DTO\Attributes\FromRouteParameter;
use Icefox\DTO\Support\RuleFactory;
use Icefox\DTO\Support\ValueFactory;
use Icefox\DTO\RuleFactory;
use Icefox\DTO\ValueFactory;
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
use Illuminate\Validation\Validator;
use ReflectionClass;
class DataObjectFactory
{
@ -31,7 +31,7 @@ class DataObjectFactory
*/
public static function fromArray(string $class, array $input, array $routeParameters): ?object
{
$logger = new Log();
$logger = Log::channel('dto');
$parameters = ReflectionHelper::getParametersMeta($class);
foreach ($parameters as $parameter) {
$parameterName = $parameter->reflection->getName();
@ -60,7 +60,7 @@ class DataObjectFactory
// continue;
// }
}
$logger->inputRaw($input);
$logger->debug('input', $input);
$rules = (new RuleFactory($logger))->make($class);
@ -69,7 +69,7 @@ class DataObjectFactory
: App::makeWith(Validator::class, ['data' => $input, 'rules' => $rules]);
if ($validator->fails()) {
$logger->validationErrors($validator->errors()->toArray());
$logger->warning('validation error', $validator->errors()->toArray());
if (method_exists($class, 'fails')) {
return App::call("$class::fails", ['validator' => $validator ]);
}

View file

@ -1,41 +0,0 @@
<?php
namespace Icefox\DTO;
use Icefox\DTO\Attributes\FromInput;
use Icefox\DTO\Attributes\FromRouteParameter;
class InputFactory
{
public function __construct(public readonly Log $log) {}
public function make(string $class): array
{
$map = [];
$parameters = ReflectionHelper::getParametersMeta($class);
foreach ($parameters as $parameter) {
$name = $parameter->reflection->getName();
foreach ($parameter->reflection->getAttributes(FromRouteParameter::class) as $attr) {
$map[$name][] = 'route_' . $attr->newInstance()->name;
}
foreach ($parameter->reflection->getAttributes(FromInput::class) as $attr) {
$map[$name][] = $attr->newInstance()->name;
}
$map[$name][] = $name;
}
return $map;
}
private static self $_instance;
public static function instance(): self
{
if (empty(self::$_instance)) {
self::$_instance = new self(new Log());
}
return self::$_instance;
}
}

View file

@ -45,4 +45,3 @@ class ReflectionHelper
return self::$cache[$class];
}
}

View file

@ -2,13 +2,14 @@
declare(strict_types=1);
namespace Icefox\DTO\Support;
namespace Icefox\DTO;
use Icefox\DTO\Attributes\Flat;
use Icefox\DTO\Attributes\OverwriteRules;
use Icefox\DTO\Config;
use Icefox\DTO\ParameterMeta;
use Icefox\DTO\ReflectionHelper;
use Icefox\DTO\RuleFactory;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\Rule;

View file

@ -2,16 +2,14 @@
declare(strict_types=1);
namespace Icefox\DTO\Support;
namespace Icefox\DTO;
use Icefox\DTO\Attributes\CastWith;
use Icefox\DTO\Attributes\Flat;
use Icefox\DTO\Config;
use Icefox\DTO\ParameterMeta;
use Icefox\DTO\ReflectionHelper;
use Illuminate\Support\Facades\App;
use ReflectionNamedType;
use ReflectionParameter;
use phpDocumentor\Reflection\DocBlock\Tags\Param;
use phpDocumentor\Reflection\PseudoTypes\Generic;
use phpDocumentor\Reflection\Type;
@ -102,6 +100,9 @@ class ValueFactory
};
}
/**
* @param array<string,mixed> $input
*/
public static function make(string $class, array $input): object
{
$parameters = ReflectionHelper::getParametersMeta($class);

View file

@ -3,7 +3,7 @@
namespace Tests;
use Icefox\DTO\Log;
use Icefox\DTO\Support\RuleFactory;
use Icefox\DTO\RuleFactory;
use Illuminate\Validation\ValidationException;
use Tests\Classes\ArrayDataObject;
use Tests\Classes\CollectionDataObject;

View file

@ -6,7 +6,7 @@ namespace Tests\Rules;
use Icefox\DTO\Log;
use Icefox\DTO\ReflectionHelper;
use Icefox\DTO\Support\RuleFactory;
use Icefox\DTO\RuleFactory;
use Illuminate\Validation\ValidationException;
use Tests\Rules\WithEmptyOverwriteRules;
use Tests\Rules\WithMergedRules;

View file

@ -5,10 +5,8 @@ declare(strict_types=1);
namespace Tests;
use Icefox\DTO\Attributes\Flat;
use Icefox\DTO\Support\RuleFactory;
use Icefox\DTO\RuleFactory;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
readonly class BasicPrimitives
{

165
tests/ValuesTest.php Normal file
View file

@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
namespace Tests;
use Icefox\DTO\Attributes\Flat;
use Icefox\DTO\RuleFactory;
use Icefox\DTO\ValueFactory;
use Illuminate\Support\Collection;
readonly class BasicPrimitives
{
public function __construct(
public string $text,
public int $number,
public bool $flag,
public ?array $items,
public float $floating = 0.0,
) {}
}
test('required rules', function () {
$object = ValueFactory::make(BasicPrimitives::class, [
'text' => 'abc',
'number' => 42,
'flag' => false,
'items' => ['a', 2, true],
'floating' => 4.2,
]);
expect($object->text)->toBe('abc');
expect($object->number)->toBe(42);
expect($object->flag)->toBe(false);
expect($object->items)->toBe(['a', 2, true]);
expect($object->floating)->toEqualWithDelta(4.2, 0.000001);
});
readonly class AnnotatedArray
{
/**
* @param array<int,float> $items
*/
public function __construct(public array $items) {}
}
test('annotated array', function () {
expect(RuleFactory::instance()->make(AnnotatedArray::class))->toBe([
'items' => ['required', 'array'],
'items.*' => ['required', 'numeric'],
]);
});
readonly class AnnotatedArrayNullableValue
{
/**
* @param array<?float> $items
*/
public function __construct(public array $items) {}
}
test('annotated array with nullable items', function () {
expect(RuleFactory::instance()->make(AnnotatedArrayNullableValue::class))->toBe([
'items' => ['required', 'array'],
'items.*' => ['nullable', 'numeric'],
]);
});
readonly class PlainLeaf
{
public function __construct(public string $name) {}
}
readonly class PlainRoot
{
public function __construct(public int $value, public PlainLeaf $leaf) {}
}
test('plain nesting', function () {
expect(RuleFactory::instance()->make(PlainRoot::class))->toBe([
'value' => ['required', 'numeric'],
'leaf' => ['required'],
'leaf.name' => ['required'],
]);
});
readonly class AnnotatedArrayItem
{
public function __construct(public int $value) {}
}
readonly class AnnotatedArrayObject
{
/**
* @param ?array<AnnotatedArrayItem> $items
*/
public function __construct(public ?array $items) {}
}
test('annotated array with object', function () {
expect(RuleFactory::instance()->make(AnnotatedArrayObject::class))->toBe([
'items' => ['nullable', 'array'],
'items.*' => ['required'],
'items.*.value' => ['required', 'numeric'],
]);
});
readonly class FlattenedLeaf
{
public function __construct(public ?bool $flag) {}
}
readonly class NotFlattenedLeaf
{
public function __construct(public string $description) {}
}
readonly class FlattenedNode
{
public function __construct(
public string $id,
public NotFlattenedLeaf $leaf,
#[Flat]
public FlattenedLeaf $squish,
public int $level = 1,
) {}
}
readonly class FlattenedRoot
{
public function __construct(
public int $value,
#[Flat]
public FlattenedNode $node,
) {}
}
test('flattened basic', function () {
expect(RuleFactory::instance()->make(FlattenedRoot::class))->toBe([
'value' => ['required', 'numeric'],
'id' => ['required' ],
'leaf' => ['required'],
'leaf.description' => ['required'],
'flag' => ['nullable', 'boolean'],
'level' => ['sometimes', 'numeric'],
]);
});
readonly class AnnotatedCollectionItem
{
public function __construct(public int $value) {}
}
readonly class AnnotatedCollection
{
/**
* @param Collection<AnnotatedCollectionItem> $group
*/
public function __construct(public Collection $group) {}
}
test('annotated collection', function () {
expect(RuleFactory::instance()->make(AnnotatedCollection::class))->toBe([
'group' => ['required', 'array'],
'group.*' => ['required'],
'group.*.value' => ['required', 'numeric'],
]);
});

View file

@ -1,8 +1,13 @@
<?php
use Icefox\DTO\CustomHandlers;
use Illuminate\Support\Collection;
use Psr\Log\LogLevel;
return [
'rules' => [
Collection::class => CustomHandlers::CollectionRules(...),
],
'logging' => [
'channel' => 'dto',
'context' => [

View file

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
>
<testsuites>
<testsuite name="root">
<directory>tests</directory>
</testsuite>
</testsuites>
<source>
<include>
<directory>app</directory>
</include>
</source>
</phpunit>