refactor, tests
This commit is contained in:
parent
fc46fe20ee
commit
b827038df3
11 changed files with 304 additions and 103 deletions
|
|
@ -1,202 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Icefox\DTO;
|
||||
|
||||
use Icefox\DTO\Attributes\Flat;
|
||||
use Icefox\DTO\Attributes\Overwrite;
|
||||
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;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use ReflectionClass;
|
||||
use ReflectionNamedType;
|
||||
use ReflectionUnionType;
|
||||
use BackedEnum;
|
||||
use phpDocumentor\Reflection\DocBlock\Tags\Param;
|
||||
use phpDocumentor\Reflection\Type;
|
||||
use phpDocumentor\Reflection\Types\AbstractList;
|
||||
use phpDocumentor\Reflection\Types\Boolean;
|
||||
use phpDocumentor\Reflection\Types\Float_;
|
||||
use phpDocumentor\Reflection\Types\Integer;
|
||||
use phpDocumentor\Reflection\Types\Nullable;
|
||||
use phpDocumentor\Reflection\Types\Object_;
|
||||
|
||||
final class RuleFactory
|
||||
{
|
||||
/**
|
||||
* @return array<string, array<int, mixed>>
|
||||
*/
|
||||
public function getRulesFromDocBlock(
|
||||
Type $type,
|
||||
string $prefix,
|
||||
): array {
|
||||
$rules = [];
|
||||
if ($type instanceof Nullable) {
|
||||
$rules[$prefix] = ['nullable'];
|
||||
$type = $type->getActualType();
|
||||
} else {
|
||||
$rules[$prefix] = ['required'];
|
||||
}
|
||||
|
||||
if ($type instanceof AbstractList) {
|
||||
$rules[$prefix][] = 'array';
|
||||
|
||||
$valueType = $type->getValueType();
|
||||
$rules = $this->mergeRules($rules, $this->getRulesFromDocBlock($valueType, $prefix . '.*'));
|
||||
}
|
||||
if ($type instanceof Boolean) {
|
||||
$rules[$prefix][] = 'boolean';
|
||||
} elseif ($type instanceof Float_ || $type instanceof Integer) {
|
||||
$rules[$prefix][] = 'numeric';
|
||||
} elseif ($type instanceof Object_) {
|
||||
$paramsSub = ReflectionHelper::getParametersMeta($type->getFqsen()->__toString());
|
||||
$rules = $this->mergeRules($rules, $this->infer($paramsSub, $prefix));
|
||||
}
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<ParameterMeta> $parameters
|
||||
* @return array<string,array<int,mixed>>
|
||||
*/
|
||||
public function infer(array $parameters, string $basePrefix): array
|
||||
{
|
||||
$rules = [];
|
||||
foreach ($parameters as $parameter) {
|
||||
$prefix = $basePrefix
|
||||
. (empty($basePrefix) ? '' : '.')
|
||||
. (empty($parameter->reflection->getAttributes(Flat::class)) ? $parameter->reflection->getName() : '');
|
||||
foreach ($this->buildParameterRule($parameter, $prefix) as $key => $newRules) {
|
||||
$rules[$key] = $newRules;
|
||||
}
|
||||
}
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array<int, mixed>>
|
||||
*/
|
||||
public function buildParameterRule(ParameterMeta $parameter, string $prefix): array
|
||||
{
|
||||
$type = $parameter->reflection->getType();
|
||||
|
||||
if (empty($type)) {
|
||||
return [$prefix => $parameter->reflection->isOptional() ? ['sometimes'] : ['required']];
|
||||
}
|
||||
|
||||
$rules = [$prefix => []];
|
||||
if (!empty($prefix)) {
|
||||
if ($parameter->reflection->isOptional()) {
|
||||
$rules[$prefix][] = 'sometimes';
|
||||
} elseif ($type->allowsNull()) {
|
||||
$rules[$prefix][] = 'nullable';
|
||||
} else {
|
||||
$rules[$prefix][] = 'required';
|
||||
}
|
||||
}
|
||||
|
||||
if ($type instanceof ReflectionUnionType) {
|
||||
//TODO: handle ReflectionUnionType
|
||||
return $rules;
|
||||
}
|
||||
|
||||
if ($type instanceof ReflectionNamedType && $name = $type->getName()) {
|
||||
if ($globalRules = Config::getRules($name)) {
|
||||
foreach ($globalRules($parameter, $this) as $scopedPrefix => $values) {
|
||||
$realPrefix = $prefix . $scopedPrefix;
|
||||
$rules[$realPrefix] = array_merge($rules[$realPrefix] ?? [], $values);
|
||||
}
|
||||
return $rules;
|
||||
}
|
||||
|
||||
if ($name === 'string') {
|
||||
} elseif ($name === 'bool') {
|
||||
$rules[$prefix][] = 'boolean';
|
||||
} elseif ($name === 'int' || $name === 'float') {
|
||||
$rules[$prefix][] = 'numeric';
|
||||
} elseif ($name === 'array') {
|
||||
$rules[$prefix][] = 'array';
|
||||
} elseif (enum_exists($name)) {
|
||||
$ref = new ReflectionClass($name);
|
||||
if ($ref->isSubclassOf(BackedEnum::class)) {
|
||||
$rules[$prefix][] = Rule::enum($name);
|
||||
}
|
||||
} else {
|
||||
$paramsSub = ReflectionHelper::getParametersMeta($type->getName());
|
||||
$rules = $this->mergeRules($rules, $this->infer($paramsSub, $prefix));
|
||||
}
|
||||
}
|
||||
|
||||
if ($parameter->tag instanceof Param) {
|
||||
$docblockRules = $this->getRulesFromDocBlock(
|
||||
$parameter->tag->getType(),
|
||||
$prefix,
|
||||
);
|
||||
$rules = $this->mergeRules($rules, $docblockRules);
|
||||
}
|
||||
if (empty($rules[$prefix])) {
|
||||
unset($rules[$prefix]);
|
||||
}
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function __construct(public LoggerInterface $log) {}
|
||||
|
||||
/**
|
||||
* @param class-string $class
|
||||
* @return array<string,array<int, mixed>>
|
||||
*/
|
||||
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(Overwrite::class))) {
|
||||
$rules = $customRules;
|
||||
} else {
|
||||
$inferredRules = RuleFactory::infer($parameters, '');
|
||||
$rules = $this->mergeRules($inferredRules, $customRules);
|
||||
}
|
||||
$this->log->info('Constructed rules for class ' . $class, $rules);
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,array<int, mixed>> $first
|
||||
* @param array<string,array<int, mixed>> $second
|
||||
* @return array<string,array<int, mixed>>
|
||||
*/
|
||||
public function mergeRules(array $first, array $second): array
|
||||
{
|
||||
$merged = $first;
|
||||
foreach ($second as $key => $rules) {
|
||||
if (isset($merged[$key])) {
|
||||
$merged[$key] = array_values(array_unique(array_merge($merged[$key], $rules)));
|
||||
} else {
|
||||
$merged[$key] = $rules;
|
||||
}
|
||||
}
|
||||
return $merged;
|
||||
}
|
||||
|
||||
private static self $_instance;
|
||||
|
||||
public static function instance(?LoggerInterface $log = null): static
|
||||
{
|
||||
if (empty(self::$_instance)) {
|
||||
static::$_instance = new self($log ?? Log::channel(config('dto.logging.channel')));
|
||||
}
|
||||
return static::$_instance;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue