From 6b1a3852928f8104878d435171fdca152907c95c Mon Sep 17 00:00:00 2001 From: icefox Date: Mon, 23 Feb 2026 21:35:08 -0300 Subject: [PATCH] refactor --- src/Config.php | 2 +- ...llectionFactory.php => CustomHandlers.php} | 10 +- src/DataObjectFactory.php | 12 +- src/InputFactory.php | 41 ----- src/ReflectionHelper.php | 1 - src/{Support => }/RuleFactory.php | 3 +- src/{Support => }/ValueFactory.php | 7 +- tests/DataObjectTest.php | 2 +- tests/Rules/RulesTest.php | 2 +- tests/RulesTest.php | 4 +- tests/ValuesTest.php | 165 ++++++++++++++++++ workbench/config/dto.php | 5 + workbench/phpunit.xml | 17 -- 13 files changed, 191 insertions(+), 80 deletions(-) rename src/{Factories/CollectionFactory.php => CustomHandlers.php} (75%) delete mode 100644 src/InputFactory.php rename src/{Support => }/RuleFactory.php (99%) rename src/{Support => }/ValueFactory.php (98%) create mode 100644 tests/ValuesTest.php delete mode 100644 workbench/phpunit.xml diff --git a/src/Config.php b/src/Config.php index f827157..e804397 100644 --- a/src/Config.php +++ b/src/Config.php @@ -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; diff --git a/src/Factories/CollectionFactory.php b/src/CustomHandlers.php similarity index 75% rename from src/Factories/CollectionFactory.php rename to src/CustomHandlers.php index bfd0fe0..075e1ba 100644 --- a/src/Factories/CollectionFactory.php +++ b/src/CustomHandlers.php @@ -1,17 +1,16 @@ */ - 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 ); } } + diff --git a/src/DataObjectFactory.php b/src/DataObjectFactory.php index adbf200..78ece59 100644 --- a/src/DataObjectFactory.php +++ b/src/DataObjectFactory.php @@ -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 ]); } diff --git a/src/InputFactory.php b/src/InputFactory.php deleted file mode 100644 index 7109eee..0000000 --- a/src/InputFactory.php +++ /dev/null @@ -1,41 +0,0 @@ -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; - } -} diff --git a/src/ReflectionHelper.php b/src/ReflectionHelper.php index 5c92a22..1ab7448 100644 --- a/src/ReflectionHelper.php +++ b/src/ReflectionHelper.php @@ -45,4 +45,3 @@ class ReflectionHelper return self::$cache[$class]; } } - diff --git a/src/Support/RuleFactory.php b/src/RuleFactory.php similarity index 99% rename from src/Support/RuleFactory.php rename to src/RuleFactory.php index 9aa1da5..c7a9e05 100644 --- a/src/Support/RuleFactory.php +++ b/src/RuleFactory.php @@ -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; diff --git a/src/Support/ValueFactory.php b/src/ValueFactory.php similarity index 98% rename from src/Support/ValueFactory.php rename to src/ValueFactory.php index 2781ae2..53de322 100644 --- a/src/Support/ValueFactory.php +++ b/src/ValueFactory.php @@ -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 $input + */ public static function make(string $class, array $input): object { $parameters = ReflectionHelper::getParametersMeta($class); diff --git a/tests/DataObjectTest.php b/tests/DataObjectTest.php index b68a4b7..de463e5 100644 --- a/tests/DataObjectTest.php +++ b/tests/DataObjectTest.php @@ -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; diff --git a/tests/Rules/RulesTest.php b/tests/Rules/RulesTest.php index 5f7ed9f..c2a2da2 100644 --- a/tests/Rules/RulesTest.php +++ b/tests/Rules/RulesTest.php @@ -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; diff --git a/tests/RulesTest.php b/tests/RulesTest.php index 8ffeff5..8e06401 100644 --- a/tests/RulesTest.php +++ b/tests/RulesTest.php @@ -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 { diff --git a/tests/ValuesTest.php b/tests/ValuesTest.php new file mode 100644 index 0000000..05d2ac8 --- /dev/null +++ b/tests/ValuesTest.php @@ -0,0 +1,165 @@ + '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 $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 $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 $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 $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'], + ]); +}); diff --git a/workbench/config/dto.php b/workbench/config/dto.php index c89046d..d212857 100644 --- a/workbench/config/dto.php +++ b/workbench/config/dto.php @@ -1,8 +1,13 @@ [ + Collection::class => CustomHandlers::CollectionRules(...), + ], 'logging' => [ 'channel' => 'dto', 'context' => [ diff --git a/workbench/phpunit.xml b/workbench/phpunit.xml deleted file mode 100644 index 0788ab5..0000000 --- a/workbench/phpunit.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - tests - - - - - app - - -