> */ 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 $parameters * @return array> */ 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> */ 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('dto.rules.' . $name, null)) { 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> */ 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> $first * @param array> $second * @return array> */ 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; } }