From d3e12785bb0c4f8eb06ee1b2022ffe4f4db77bf1 Mon Sep 17 00:00:00 2001 From: icefox Date: Wed, 18 Feb 2026 19:19:07 -0300 Subject: [PATCH] include missing --- phpstan.neon.dist | 4 +- src/Attributes/FromInput.php | 13 ++ src/Attributes/FromMapper.php | 16 +++ src/Attributes/FromRouteParameter.php | 13 ++ src/DataObject.php | 5 +- src/Factories/CollectionFactory.php | 40 ++++++ src/Support/RuleFactory.php | 191 +++++++++++++++++++++++++ src/Support/ValueFactory.php | 101 +++++++++++++ src/config/dto.php | 14 ++ tests/Classes/ArrayDataObject.php | 16 +++ tests/Classes/CarbonPeriodMapper.php | 22 +++ tests/Classes/CollectionDataObject.php | 17 +++ tests/Classes/FromInputObject.php | 19 +++ tests/Classes/ObjectWithoutMapper.php | 17 +++ tests/Classes/OptionalData.php | 19 +++ tests/Classes/OptionalNullableData.php | 19 +++ tests/Classes/PrimitiveData.php | 19 +++ tests/Classes/RecursiveDataObject.php | 17 +++ tests/Classes/WithMapperObject.php | 19 +++ workbench/bootstrap/app.php | 7 + 20 files changed, 584 insertions(+), 4 deletions(-) create mode 100644 src/Attributes/FromInput.php create mode 100644 src/Attributes/FromMapper.php create mode 100644 src/Attributes/FromRouteParameter.php create mode 100644 src/Factories/CollectionFactory.php create mode 100644 src/Support/RuleFactory.php create mode 100644 src/Support/ValueFactory.php create mode 100644 src/config/dto.php create mode 100644 tests/Classes/ArrayDataObject.php create mode 100644 tests/Classes/CarbonPeriodMapper.php create mode 100644 tests/Classes/CollectionDataObject.php create mode 100644 tests/Classes/FromInputObject.php create mode 100644 tests/Classes/ObjectWithoutMapper.php create mode 100644 tests/Classes/OptionalData.php create mode 100644 tests/Classes/OptionalNullableData.php create mode 100644 tests/Classes/PrimitiveData.php create mode 100644 tests/Classes/RecursiveDataObject.php create mode 100644 tests/Classes/WithMapperObject.php create mode 100644 workbench/bootstrap/app.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 18f3e1c..79b90ad 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ parameters: paths: - - app + - src - tests - level: 10 + level: 5 diff --git a/src/Attributes/FromInput.php b/src/Attributes/FromInput.php new file mode 100644 index 0000000..0a536bc --- /dev/null +++ b/src/Attributes/FromInput.php @@ -0,0 +1,13 @@ + $input + * @param array $input */ public static function fromArray(array $input): static { @@ -86,7 +87,7 @@ trait DataObject continue; } - $mappedInput[$parameterName] = RuleFactory::resolveValue( + $mappedInput[$parameterName] = ValueFactory::resolveValue( $validator->getValue($parameterName), $parameter->tag instanceof Param ? $parameter->tag->getType() : null, $parameter->reflection, diff --git a/src/Factories/CollectionFactory.php b/src/Factories/CollectionFactory.php new file mode 100644 index 0000000..5696dad --- /dev/null +++ b/src/Factories/CollectionFactory.php @@ -0,0 +1,40 @@ + + */ + public static function rules(ReflectionParameter $parameter, ?Type $type): array + { + if (is_null($type)) { + return []; + } + + if (!$type instanceof Generic) { + return []; + } + + $subtypes = $type->getTypes(); + + if (count($subtypes) == 0) { + return []; + } + + $subtype = count($subtypes) == 1 ? $subtypes[0] : $subtypes[1]; + + return array_merge( + ['' => ['array']], + RuleFactory::getRulesFromDocBlock($subtype, '.*'), + ); + } +} diff --git a/src/Support/RuleFactory.php b/src/Support/RuleFactory.php new file mode 100644 index 0000000..623ebc9 --- /dev/null +++ b/src/Support/RuleFactory.php @@ -0,0 +1,191 @@ +> + */ + 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::buildRules($paramsSub, $prefix . '.'), + ); + } + 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> + */ + public static function buildRules(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> + */ + 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('dto.rules.' . $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::buildRules($paramsSub, $root . '.'), + ); + } + } + + foreach ($parameter->reflection->getAttributes(FromMapper::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; + } +} diff --git a/src/Support/ValueFactory.php b/src/Support/ValueFactory.php new file mode 100644 index 0000000..8229b35 --- /dev/null +++ b/src/Support/ValueFactory.php @@ -0,0 +1,101 @@ +getActualType(); + } + + if ($type instanceof Generic) { + $types = $type->getTypes(); + $innerType = count($types) === 2 ? $types[1] : $types[0]; + $result = []; + foreach ($rawValue as $key => $value) { + $result[$key] = self::resolveTypedValue($value, $innerType); + } + return new ($type->getFqsen()->__toString())($result); + } + + if ($type instanceof AbstractList) { + $innerType = $type->getValueType(); + $result = []; + foreach ($rawValue as $key => $value) { + $result[$key] = self::resolveTypedValue($value, $innerType); + } + return $result; + } + + if ($type instanceof Boolean) { + return boolval($rawValue); + } + + if ($type instanceof Float_) { + return floatval($rawValue); + } + + if ($type instanceof Integer) { + return intval($rawValue); + } + + if ($type instanceof Object_) { + return self::constructObject($type->getFqsen()->__toString(), $rawValue); + } + + return $rawValue; + } + + public static function resolveValue(mixed $rawValue, ?Type $type, ReflectionParameter $reflection): mixed + { + if ($reflection->allowsNull() && is_null($rawValue)) { + return null; + } + + if (is_null($type)) { + $reflectedType = $reflection->getType(); + if ($reflectedType instanceof ReflectionNamedType && $name = $reflectedType->getName()) { + return match ($name) { + 'string' => $rawValue, + 'bool' => boolval($rawValue), + 'int' => intval($rawValue), + 'float' => floatval($rawValue), + 'array' => $rawValue, + default => self::constructObject($name, $rawValue), + }; + } + return $rawValue; + } + + return self::resolveTypedValue($rawValue, $type); + } +} diff --git a/src/config/dto.php b/src/config/dto.php new file mode 100644 index 0000000..634b846 --- /dev/null +++ b/src/config/dto.php @@ -0,0 +1,14 @@ + [ + Collection::class => CollectionFactory::rules(...), + ], + 'default' => [ + 'factories' => [ + ], + ], +]; diff --git a/tests/Classes/ArrayDataObject.php b/tests/Classes/ArrayDataObject.php new file mode 100644 index 0000000..6da4819 --- /dev/null +++ b/tests/Classes/ArrayDataObject.php @@ -0,0 +1,16 @@ + $values + */ + public function __construct(public array $values) {} +} diff --git a/tests/Classes/CarbonPeriodMapper.php b/tests/Classes/CarbonPeriodMapper.php new file mode 100644 index 0000000..1003370 --- /dev/null +++ b/tests/Classes/CarbonPeriodMapper.php @@ -0,0 +1,22 @@ + ['required', 'date'], + 'end' => ['required', 'date'], + ]; + } +} diff --git a/tests/Classes/CollectionDataObject.php b/tests/Classes/CollectionDataObject.php new file mode 100644 index 0000000..711dbc7 --- /dev/null +++ b/tests/Classes/CollectionDataObject.php @@ -0,0 +1,17 @@ + $values + */ + public function __construct(public Collection $values) {} +} diff --git a/tests/Classes/FromInputObject.php b/tests/Classes/FromInputObject.php new file mode 100644 index 0000000..575e44c --- /dev/null +++ b/tests/Classes/FromInputObject.php @@ -0,0 +1,19 @@ +create();