refactor rules out of DataObject
This commit is contained in:
parent
77d1aebc0a
commit
74f151df07
5 changed files with 115 additions and 93 deletions
|
|
@ -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<string,array<int, string|Rule>>
|
||||
*/
|
||||
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<string,array<int, string|Rule>> $inferredRules
|
||||
* @param array<string,array<int, string|Rule>> $customRules
|
||||
* @return array<string,array<int, string|Rule>>
|
||||
*/
|
||||
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<string,mixed> $data
|
||||
* @param array<string,array<int, string|Rule>> $rules
|
||||
|
|
|
|||
48
src/ReflectionHelper.php
Normal file
48
src/ReflectionHelper.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace Icefox\DTO;
|
||||
|
||||
use ReflectionParameter;
|
||||
use phpDocumentor\Reflection\DocBlock\Tag;
|
||||
use phpDocumentor\Reflection\DocBlock\Tags\Param;
|
||||
use phpDocumentor\Reflection\Types\ContextFactory;
|
||||
use phpDocumentor\Reflection\DocBlockFactory;
|
||||
use ReflectionClass;
|
||||
|
||||
class ReflectionHelper
|
||||
{
|
||||
protected static array $cache = [];
|
||||
/**
|
||||
* @param class-string $class
|
||||
* @return array<ParameterMeta>
|
||||
*/
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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<string, array<string|Rule>>
|
||||
*/
|
||||
|
|
@ -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<ParameterMeta>
|
||||
*/
|
||||
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<ParameterMeta> $parameters
|
||||
* @return array<string,array<int,string|Rule>>
|
||||
|
|
@ -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<string,array<int, string>>
|
||||
*/
|
||||
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<string,array<int, string>> $inferredRules
|
||||
* @param array<string,array<int, string>> $customRules
|
||||
* @return array<string,array<int, string>>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
|
|
|
|||
|
|
@ -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([]);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue