laravel-data/src/Support/RuleFactory.php
2026-02-18 21:57:12 -03:00

193 lines
6.6 KiB
PHP

<?php
declare(strict_types=1);
namespace Icefox\DTO\Support;
use Icefox\DTO\Attributes\CastWith;
use Icefox\DTO\Config;
use Icefox\DTO\Attributes\FromMapper;
use Icefox\DTO\ParameterMeta;
use Illuminate\Support\Facades\App;
use Illuminate\Validation\Rule;
use ReflectionClass;
use ReflectionNamedType;
use ReflectionParameter;
use ReflectionUnionType;
use BackedEnum;
use phpDocumentor\Reflection\DocBlockFactory;
use phpDocumentor\Reflection\DocBlock\Tag;
use phpDocumentor\Reflection\DocBlock\Tags\Param;
use phpDocumentor\Reflection\Type;
use phpDocumentor\Reflection\Types\AbstractList;
use phpDocumentor\Reflection\Types\Boolean;
use phpDocumentor\Reflection\Types\ContextFactory;
use phpDocumentor\Reflection\Types\Float_;
use phpDocumentor\Reflection\Types\Integer;
use phpDocumentor\Reflection\Types\Nullable;
use phpDocumentor\Reflection\Types\Object_;
class RuleFactory
{
protected static array $cache = [];
/**
* @return array<string, array<string|Rule>>
*/
public static function getRulesFromDocBlock(
Type $type,
string $prefix,
): array {
$rules = [];
if ($type instanceof Nullable) {
$rules[$prefix] = ['nullable'];
$rules = array_merge($rules, self::getRulesFromDocBlock($type->getActualType(), $prefix));
} else {
$rules[$prefix] = ['required'];
}
if ($type instanceof AbstractList) {
$rules[$prefix][] = 'array';
$valueType = $type->getValueType();
$rules = array_merge($rules, self::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 = self::getParametersMeta($type->getFqsen()->__toString());
$rules = array_merge(
$rules,
self::infer($paramsSub, $prefix . '.'),
);
}
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>>
*/
public static function infer(array $parameters, string $prefix): array
{
$rules = [];
foreach ($parameters as $parameter) {
foreach (self::buildParameterRule($parameter, $prefix) as $key => $newRules) {
$rules[$key] = $newRules;
}
}
return $rules;
}
/**
* @return array<string, array<int, string|Rule>>
*/
public static function buildParameterRule(ParameterMeta $parameter, string $prefix): array
{
$type = $parameter->reflection->getType();
$root = $prefix . $parameter->reflection->getName();
if (empty($type)) {
return [$root => $parameter->reflection->isOptional() ? ['sometimes'] : ['required']];
}
$rules = [$root => []];
if ($parameter->reflection->isOptional()) {
$rules[$root][] = 'sometimes';
} elseif ($type->allowsNull()) {
$rules[$root][] = 'nullable';
} else {
$rules[$root][] = '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->reflection, $parameter->tag->getType()) as $scopedPrefix => $values) {
$realPrefix = $root . $scopedPrefix;
$rules[$realPrefix] = array_values(array_unique(array_merge($rules[$realPrefix] ?? [], $values)));
}
}
if ($name === 'string') {
} elseif ($name === 'bool') {
$rules[$root][] = 'boolean';
} elseif ($name === 'int' || $name === 'float') {
$rules[$root][] = 'numeric';
} elseif ($name === 'array') {
$rules[$root][] = 'array';
} elseif (enum_exists($name)) {
$ref = new ReflectionClass($name);
if ($ref->isSubclassOf(BackedEnum::class)) {
$rules[$root][] = Rule::enum($name);
}
} else {
$paramsSub = self::getParametersMeta($type->getName());
$rules = array_merge(
$rules,
self::infer($paramsSub, $root . '.'),
);
}
}
foreach ($parameter->reflection->getAttributes(CastWith::class) as $attr) {
$mapperClass = $attr->newInstance()->class;
if (method_exists($mapperClass, 'rules')) {
$subRules = App::call("$mapperClass@rules");
foreach ($subRules as $key => &$value) {
$path = empty($key) ? $root : ($root . '.' . $key);
$rules[$path] = array_values(array_unique(array_merge($rules[$path] ?? [], $value)));
}
}
}
if ($parameter->tag instanceof Param) {
$docblockRules = self::getRulesFromDocBlock(
$parameter->tag->getType(),
$prefix . $parameter->reflection->getName(),
);
foreach ($docblockRules as $key => &$values) {
$rules[$key] = array_values(array_unique(array_merge($rules[$key] ?? [], $values)));
}
}
return $rules;
}
}