wip flattening

This commit is contained in:
icefox 2026-02-19 16:38:35 -03:00
parent 74f151df07
commit d83a324eb0
No known key found for this signature in database
5 changed files with 64 additions and 23 deletions

View file

@ -5,9 +5,9 @@ declare(strict_types=1);
namespace Icefox\DTO\Support; namespace Icefox\DTO\Support;
use Icefox\DTO\Attributes\CastWith; use Icefox\DTO\Attributes\CastWith;
use Icefox\DTO\Attributes\Flat;
use Icefox\DTO\Attributes\OverwriteRules; use Icefox\DTO\Attributes\OverwriteRules;
use Icefox\DTO\Config; use Icefox\DTO\Config;
use Icefox\DTO\Attributes\FromMapper;
use Icefox\DTO\Log; use Icefox\DTO\Log;
use Icefox\DTO\ParameterMeta; use Icefox\DTO\ParameterMeta;
use Icefox\DTO\ReflectionHelper; use Icefox\DTO\ReflectionHelper;
@ -15,16 +15,12 @@ use Illuminate\Support\Facades\App;
use Illuminate\Validation\Rule; use Illuminate\Validation\Rule;
use ReflectionClass; use ReflectionClass;
use ReflectionNamedType; use ReflectionNamedType;
use ReflectionParameter;
use ReflectionUnionType; use ReflectionUnionType;
use BackedEnum; use BackedEnum;
use phpDocumentor\Reflection\DocBlockFactory;
use phpDocumentor\Reflection\DocBlock\Tag;
use phpDocumentor\Reflection\DocBlock\Tags\Param; use phpDocumentor\Reflection\DocBlock\Tags\Param;
use phpDocumentor\Reflection\Type; use phpDocumentor\Reflection\Type;
use phpDocumentor\Reflection\Types\AbstractList; use phpDocumentor\Reflection\Types\AbstractList;
use phpDocumentor\Reflection\Types\Boolean; use phpDocumentor\Reflection\Types\Boolean;
use phpDocumentor\Reflection\Types\ContextFactory;
use phpDocumentor\Reflection\Types\Float_; use phpDocumentor\Reflection\Types\Float_;
use phpDocumentor\Reflection\Types\Integer; use phpDocumentor\Reflection\Types\Integer;
use phpDocumentor\Reflection\Types\Nullable; use phpDocumentor\Reflection\Types\Nullable;
@ -61,7 +57,7 @@ class RuleFactory
$paramsSub = ReflectionHelper::getParametersMeta($type->getFqsen()->__toString()); $paramsSub = ReflectionHelper::getParametersMeta($type->getFqsen()->__toString());
$rules = array_merge( $rules = array_merge(
$rules, $rules,
self::infer($paramsSub, $prefix . '.'), self::infer($paramsSub, $prefix),
); );
} }
return $rules; return $rules;
@ -71,10 +67,13 @@ class RuleFactory
* @param array<ParameterMeta> $parameters * @param array<ParameterMeta> $parameters
* @return array<string,array<int,string|Rule>> * @return array<string,array<int,string|Rule>>
*/ */
public static function infer(array $parameters, string $prefix): array public static function infer(array $parameters, string $basePrefix): array
{ {
$rules = []; $rules = [];
foreach ($parameters as $parameter) { foreach ($parameters as $parameter) {
$prefix = $basePrefix
. (empty($basePrefix) ? '' : '.')
. (empty($parameter->reflection->getAttributes(Flat::class)) ? $parameter->reflection->getName() : '');
foreach (self::buildParameterRule($parameter, $prefix) as $key => $newRules) { foreach (self::buildParameterRule($parameter, $prefix) as $key => $newRules) {
$rules[$key] = $newRules; $rules[$key] = $newRules;
} }
@ -89,18 +88,17 @@ class RuleFactory
{ {
$type = $parameter->reflection->getType(); $type = $parameter->reflection->getType();
$root = $prefix . $parameter->reflection->getName();
if (empty($type)) { if (empty($type)) {
return [$root => $parameter->reflection->isOptional() ? ['sometimes'] : ['required']]; return [$prefix => $parameter->reflection->isOptional() ? ['sometimes'] : ['required']];
} }
$rules = [$root => []]; $rules = [$prefix => []];
if ($parameter->reflection->isOptional()) { if ($parameter->reflection->isOptional()) {
$rules[$root][] = 'sometimes'; $rules[$prefix][] = 'sometimes';
} elseif ($type->allowsNull()) { } elseif ($type->allowsNull()) {
$rules[$root][] = 'nullable'; $rules[$prefix][] = 'nullable';
} else { } else {
$rules[$root][] = 'required'; $rules[$prefix][] = 'required';
} }
if ($type instanceof ReflectionUnionType) { if ($type instanceof ReflectionUnionType) {
@ -111,27 +109,27 @@ class RuleFactory
if ($type instanceof ReflectionNamedType && $name = $type->getName()) { if ($type instanceof ReflectionNamedType && $name = $type->getName()) {
if ($globalRules = Config::getRules($name)) { if ($globalRules = Config::getRules($name)) {
foreach ($globalRules($parameter->reflection, $parameter->tag->getType()) as $scopedPrefix => $values) { foreach ($globalRules($parameter->reflection, $parameter->tag->getType()) as $scopedPrefix => $values) {
$realPrefix = $root . $scopedPrefix; $realPrefix = $prefix . $scopedPrefix;
$rules[$realPrefix] = array_values(array_unique(array_merge($rules[$realPrefix] ?? [], $values))); $rules[$realPrefix] = array_values(array_unique(array_merge($rules[$realPrefix] ?? [], $values)));
} }
} }
if ($name === 'string') { if ($name === 'string') {
} elseif ($name === 'bool') { } elseif ($name === 'bool') {
$rules[$root][] = 'boolean'; $rules[$prefix][] = 'boolean';
} elseif ($name === 'int' || $name === 'float') { } elseif ($name === 'int' || $name === 'float') {
$rules[$root][] = 'numeric'; $rules[$prefix][] = 'numeric';
} elseif ($name === 'array') { } elseif ($name === 'array') {
$rules[$root][] = 'array'; $rules[$prefix][] = 'array';
} elseif (enum_exists($name)) { } elseif (enum_exists($name)) {
$ref = new ReflectionClass($name); $ref = new ReflectionClass($name);
if ($ref->isSubclassOf(BackedEnum::class)) { if ($ref->isSubclassOf(BackedEnum::class)) {
$rules[$root][] = Rule::enum($name); $rules[$prefix][] = Rule::enum($name);
} }
} else { } else {
$paramsSub = ReflectionHelper::getParametersMeta($type->getName()); $paramsSub = ReflectionHelper::getParametersMeta($type->getName());
$rules = array_merge( $rules = array_merge(
$rules, $rules,
self::infer($paramsSub, $root . '.'), self::infer($paramsSub, $prefix),
); );
} }
} }
@ -141,15 +139,16 @@ class RuleFactory
if (method_exists($mapperClass, 'rules')) { if (method_exists($mapperClass, 'rules')) {
$subRules = App::call("$mapperClass@rules"); $subRules = App::call("$mapperClass@rules");
foreach ($subRules as $key => &$value) { foreach ($subRules as $key => &$value) {
$path = empty($key) ? $root : ($root . '.' . $key); $path = empty($key) ? $prefix : ($prefix . '.' . $key);
$rules[$path] = array_values(array_unique(array_merge($rules[$path] ?? [], $value))); $rules[$path] = array_values(array_unique(array_merge($rules[$path] ?? [], $value)));
} }
} }
} }
if ($parameter->tag instanceof Param) { if ($parameter->tag instanceof Param) {
$docblockRules = self::getRulesFromDocBlock( $docblockRules = self::getRulesFromDocBlock(
$parameter->tag->getType(), $parameter->tag->getType(),
$prefix . $parameter->reflection->getName(), $prefix,
); );
foreach ($docblockRules as $key => &$values) { foreach ($docblockRules as $key => &$values) {
$rules[$key] = array_values(array_unique(array_merge($rules[$key] ?? [], $values))); $rules[$key] = array_values(array_unique(array_merge($rules[$key] ?? [], $values)));

View file

@ -0,0 +1,12 @@
<?php
namespace Tests\Flattening\Classes;
use Icefox\DTO\Attributes\Flat;
use Icefox\DTO\DataObject;
class BasicRoot
{
use DataObject;
public function __construct(public string $text, #[Flat] public RequiredLeaf $leaf) {}
}

View file

@ -0,0 +1,12 @@
<?php
namespace Tests\Flattening\Classes;
use Icefox\DTO\DataObject;
class RequiredLeaf
{
use DataObject;
public function __construct(public int $value) {}
}

View file

@ -0,0 +1,18 @@
<?php
namespace Tests\Flattening;
use Icefox\DTO\Log;
use Icefox\DTO\Support\RuleFactory;
use Tests\Flattening\Classes\BasicRoot;
describe('flattens required parameters', function () {
it('generates correct rules', function () {
$rules = (new RuleFactory(new Log()))->make(BasicRoot::class);
expect($rules)->toMatchArray([
'text' => ['required'],
'value' => ['required', 'numeric'],
]);
});
});

View file

@ -15,7 +15,7 @@ use Tests\Rules\WithOverwriteRules;
describe('rules array shape', function () { describe('rules array shape', function () {
it('returns inferred rules shape from RuleFactory::infer (inferred only)', function () { it('returns inferred rules shape from RuleFactory::infer (inferred only)', function () {
$parameters = ReflectionHelper::getParametersMeta(WithMergedRules::class); $parameters = ReflectionHelper::getParametersMeta(WithMergedRules::class);
$rules = RuleFactory::infer($parameters, ''); $rules = RuleFactory::infer($parameters, '', '');
expect($rules)->toBe([ expect($rules)->toBe([
'value' => ['required', 'numeric'], 'value' => ['required', 'numeric'],
@ -24,7 +24,7 @@ describe('rules array shape', function () {
it('returns inferred rules shape regardless of OverwriteRules attribute', function () { it('returns inferred rules shape regardless of OverwriteRules attribute', function () {
$parameters = ReflectionHelper::getParametersMeta(WithOverwriteRules::class); $parameters = ReflectionHelper::getParametersMeta(WithOverwriteRules::class);
$rules = RuleFactory::infer($parameters, ''); $rules = RuleFactory::infer($parameters, '', '');
expect($rules)->toBe([ expect($rules)->toBe([
'value' => ['required', 'numeric'], 'value' => ['required', 'numeric'],