diff --git a/src/DataObject.php b/src/DataObject.php index 26f35ad..1c1fc1e 100644 --- a/src/DataObject.php +++ b/src/DataObject.php @@ -46,7 +46,7 @@ trait DataObject public static function fromArray(array $input): ?static { $logger = new Log(); - $parameters = RuleFactory::getParametersMeta(static::class); + $parameters = ReflectionHelper::getParametersMeta(static::class); foreach ($parameters as $parameter) { $parameterName = $parameter->reflection->getName(); @@ -69,8 +69,7 @@ trait DataObject } $logger->inputRaw($input); - $rules = static::getRules(); - $logger->rules($rules); + $rules = (new RuleFactory($logger))->make(static::class); $validator = static::withValidator($input, $rules); @@ -102,52 +101,11 @@ trait DataObject return App::make(static::class, $mappedInput); } - public static function rules(): array - { - return []; - } - public static function fails(Validator $validator): ?static { throw new ValidationException($validator); } - /** - * @return array> - */ - public static function getRules(): array - { - $parameters = RuleFactory::getParametersMeta(static::class); - $customRules = static::rules(); - $classReflection = new ReflectionClass(static::class); - $rulesMethod = $classReflection->getMethod('rules'); - - if (!empty($rulesMethod->getAttributes(OverwriteRules::class))) { - return $customRules; - } - - $inferredRules = RuleFactory::infer($parameters, ''); - return self::mergeRules($inferredRules, $customRules); - } - - /** - * @param array> $inferredRules - * @param array> $customRules - * @return array> - */ - protected static function mergeRules(array $inferredRules, array $customRules): array - { - $merged = $inferredRules; - foreach ($customRules as $key => $rules) { - if (isset($merged[$key])) { - $merged[$key] = array_values(array_unique(array_merge($merged[$key], $rules))); - } else { - $merged[$key] = $rules; - } - } - return $merged; - } - /** * @param array $data * @param array> $rules diff --git a/src/ReflectionHelper.php b/src/ReflectionHelper.php new file mode 100644 index 0000000..5c92a22 --- /dev/null +++ b/src/ReflectionHelper.php @@ -0,0 +1,48 @@ + + */ + public static function getParametersMeta(string $class): array + { + if (array_key_exists($class, self::$cache)) { + return self::$cache[$class]; + } + + $reflection = new ReflectionClass($class); + $constructor = $reflection->getConstructor(); + try { + $docblockParams = (DocBlockFactory::createInstance())->create( + $constructor->getDocComment(), + (new ContextFactory())->createFromReflector($constructor), + )->getTagsByName('param'); + } catch (\Exception) { + $docblockParams = []; + } + self::$cache[$class] = array_map( + fn(ReflectionParameter $p) => new ParameterMeta( + $p, + array_find( + $docblockParams, + fn(Tag $tag) => $tag instanceof Param ? $tag->getVariableName() == $p->getName() : false, + ), + ), + $constructor->getParameters(), + ); + return self::$cache[$class]; + } +} + diff --git a/src/Support/RuleFactory.php b/src/Support/RuleFactory.php index db07382..bd48324 100644 --- a/src/Support/RuleFactory.php +++ b/src/Support/RuleFactory.php @@ -5,9 +5,12 @@ declare(strict_types=1); namespace Icefox\DTO\Support; use Icefox\DTO\Attributes\CastWith; +use Icefox\DTO\Attributes\OverwriteRules; use Icefox\DTO\Config; use Icefox\DTO\Attributes\FromMapper; +use Icefox\DTO\Log; use Icefox\DTO\ParameterMeta; +use Icefox\DTO\ReflectionHelper; use Illuminate\Support\Facades\App; use Illuminate\Validation\Rule; use ReflectionClass; @@ -29,8 +32,6 @@ use phpDocumentor\Reflection\Types\Object_; class RuleFactory { - protected static array $cache = []; - /** * @return array> */ @@ -57,7 +58,7 @@ class RuleFactory } elseif ($type instanceof Float_ || $type instanceof Integer) { $rules[$prefix][] = 'numeric'; } elseif ($type instanceof Object_) { - $paramsSub = self::getParametersMeta($type->getFqsen()->__toString()); + $paramsSub = ReflectionHelper::getParametersMeta($type->getFqsen()->__toString()); $rules = array_merge( $rules, self::infer($paramsSub, $prefix . '.'), @@ -66,39 +67,6 @@ class RuleFactory return $rules; } - /** - * @param class-string $class - * @return array - */ - public static function getParametersMeta(string $class): array - { - if (array_key_exists($class, self::$cache)) { - return self::$cache[$class]; - } - - $reflection = new ReflectionClass($class); - $constructor = $reflection->getConstructor(); - try { - $docblockParams = (DocBlockFactory::createInstance())->create( - $constructor->getDocComment(), - (new ContextFactory())->createFromReflector($constructor), - )->getTagsByName('param'); - } catch (\Exception) { - $docblockParams = []; - } - self::$cache[$class] = array_map( - fn(ReflectionParameter $p) => new ParameterMeta( - $p, - array_find( - $docblockParams, - fn(Tag $tag) => $tag instanceof Param ? $tag->getVariableName() == $p->getName() : false, - ), - ), - $constructor->getParameters(), - ); - return self::$cache[$class]; - } - /** * @param array $parameters * @return array> @@ -160,7 +128,7 @@ class RuleFactory $rules[$root][] = Rule::enum($name); } } else { - $paramsSub = self::getParametersMeta($type->getName()); + $paramsSub = ReflectionHelper::getParametersMeta($type->getName()); $rules = array_merge( $rules, self::infer($paramsSub, $root . '.'), @@ -190,4 +158,48 @@ class RuleFactory return $rules; } + + public function __construct(public Log $log) {} + + /** + * @param class-string $class + * @return array> + */ + public function make(string $class): array + { + $parameters = ReflectionHelper::getParametersMeta($class); + + $classReflection = new ReflectionClass($class); + $hasRulesMethod = $classReflection->hasMethod('rules'); + + $customRules = $hasRulesMethod ? App::call("$class::rules", []) : []; + + + if ($hasRulesMethod && !empty($classReflection->getMethod('rules')->getAttributes(OverwriteRules::class))) { + $rules = $customRules; + } else { + $inferredRules = RuleFactory::infer($parameters, ''); + $rules = self::mergeRules($inferredRules, $customRules); + } + $this->log->rules($rules); + return $rules; + } + + /** + * @param array> $inferredRules + * @param array> $customRules + * @return array> + */ + protected function mergeRules(array $inferredRules, array $customRules): array + { + $merged = $inferredRules; + foreach ($customRules as $key => $rules) { + if (isset($merged[$key])) { + $merged[$key] = array_values(array_unique(array_merge($merged[$key], $rules))); + } else { + $merged[$key] = $rules; + } + } + return $merged; + } } diff --git a/tests/DataObjectTest.php b/tests/DataObjectTest.php index 4177c8b..b68a4b7 100644 --- a/tests/DataObjectTest.php +++ b/tests/DataObjectTest.php @@ -2,6 +2,8 @@ namespace Tests; +use Icefox\DTO\Log; +use Icefox\DTO\Support\RuleFactory; use Illuminate\Validation\ValidationException; use Tests\Classes\ArrayDataObject; use Tests\Classes\CollectionDataObject; @@ -14,7 +16,7 @@ use Tests\Classes\WithMapperObject; describe('primitive data test', function () { it('creates required rules', function () { - $rules = PrimitiveData::getRules(); + $rules = (new RuleFactory(new Log()))->make(PrimitiveData::class); expect($rules)->toMatchArray([ 'string' => ['required'], 'int' => ['required', 'numeric'], @@ -40,7 +42,7 @@ describe('primitive data test', function () { describe('optional data', function () { it('creates optional rules', function () { - $rules = OptionalData::getRules(); + $rules = (new RuleFactory(new Log()))->make(OptionalData::class); expect($rules)->toMatchArray([ 'string' => ['sometimes'], 'int' => ['sometimes', 'numeric'], @@ -61,7 +63,7 @@ describe('optional data', function () { describe('nullable data', function () { it('creates nullable rules', function () { - $rules = OptionalNullableData::getRules(); + $rules = (new RuleFactory(new Log()))->make(OptionalNullableData::class); expect($rules)->toMatchArray([ 'string' => ['required'], 'int' => ['nullable', 'numeric'], @@ -91,7 +93,7 @@ describe('nullable data', function () { describe('reference other DataObject', function () { it('creates recursive rules', function () { - $rules = RecursiveDataObject::getRules(); + $rules = (new RuleFactory(new Log()))->make(RecursiveDataObject::class); expect($rules)->toMatchArray([ 'string' => ['required'], 'extra.string' => ['required'], @@ -104,7 +106,7 @@ describe('reference other DataObject', function () { describe('primitive array', function () { it('creates array rules', function () { - $rules = ArrayDataObject::getRules(); + $rules = (new RuleFactory(new Log()))->make(ArrayDataObject::class); expect($rules)->toMatchArray([ 'values' => ['required', 'array'], 'values.*' => ['required', 'numeric'], @@ -115,7 +117,7 @@ describe('primitive array', function () { describe('object array', function () { it('creates array rules', function () { - $rules = CollectionDataObject::getRules(); + $rules = (new RuleFactory(new Log()))->make(CollectionDataObject::class); expect($rules)->toMatchArray([ 'values' => ['required', 'array'], 'values.*' => ['required'], @@ -131,7 +133,7 @@ describe('can map input names', function () { it('creates rules with property names', function () { - $rules = FromInputObject::getRules(); + $rules = (new RuleFactory(new Log()))->make(FromInputObject::class); expect($rules)->toMatchArray([ 'text' => ['required' ], 'standard' => ['required', 'numeric'], diff --git a/tests/Rules/RulesTest.php b/tests/Rules/RulesTest.php index 19ad404..c769acd 100644 --- a/tests/Rules/RulesTest.php +++ b/tests/Rules/RulesTest.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace Tests\Rules; +use Icefox\DTO\Log; +use Icefox\DTO\ReflectionHelper; use Icefox\DTO\Support\RuleFactory; use Illuminate\Validation\ValidationException; use Tests\Rules\WithEmptyOverwriteRules; @@ -12,7 +14,7 @@ use Tests\Rules\WithOverwriteRules; describe('rules array shape', function () { it('returns inferred rules shape from RuleFactory::infer (inferred only)', function () { - $parameters = RuleFactory::getParametersMeta(WithMergedRules::class); + $parameters = ReflectionHelper::getParametersMeta(WithMergedRules::class); $rules = RuleFactory::infer($parameters, ''); expect($rules)->toBe([ @@ -21,7 +23,7 @@ describe('rules array shape', function () { }); it('returns inferred rules shape regardless of OverwriteRules attribute', function () { - $parameters = RuleFactory::getParametersMeta(WithOverwriteRules::class); + $parameters = ReflectionHelper::getParametersMeta(WithOverwriteRules::class); $rules = RuleFactory::infer($parameters, ''); expect($rules)->toBe([ @@ -32,7 +34,7 @@ describe('rules array shape', function () { describe('getRules method', function () { it('returns merged rules from DataObject::getRules()', function () { - $rules = WithMergedRules::getRules(); + $rules = (new RuleFactory(new Log()))->make(WithMergedRules::class); expect($rules)->toBe([ 'value' => ['required', 'numeric', 'max:20'], @@ -40,7 +42,7 @@ describe('getRules method', function () { }); it('returns only custom rules from DataObject::getRules() with OverwriteRules', function () { - $rules = WithOverwriteRules::getRules(); + $rules = (new RuleFactory(new Log()))->make(WithOverwriteRules::class); expect($rules)->toBe([ 'value' => ['numeric', 'max:20'], @@ -48,7 +50,7 @@ describe('getRules method', function () { }); it('returns empty rules from DataObject::getRules() with OverwriteRules and no custom rules', function () { - $rules = WithEmptyOverwriteRules::getRules(); + $rules = (new RuleFactory(new Log()))->make(WithEmptyOverwriteRules::class); expect($rules)->toBe([]); });