workbench, tests
This commit is contained in:
parent
d83a324eb0
commit
367858c97c
27 changed files with 568 additions and 410 deletions
|
|
@ -12,7 +12,8 @@
|
|||
"phpstan/phpstan": "^2.1",
|
||||
"friendsofphp/php-cs-fixer": "^3.94",
|
||||
"orchestra/testbench": "^9.16",
|
||||
"pestphp/pest-plugin-laravel": "^4.0"
|
||||
"pestphp/pest-plugin-laravel": "^4.0",
|
||||
"laravel/pail": "^1.2"
|
||||
},
|
||||
"license": "GPL-2.0-only",
|
||||
"autoload": {
|
||||
|
|
@ -22,7 +23,10 @@
|
|||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Tests\\": "tests/"
|
||||
"Tests\\": "tests/",
|
||||
"Workbench\\App\\": "workbench/app/",
|
||||
"Workbench\\Database\\Factories\\": "workbench/database/factories/",
|
||||
"Workbench\\Database\\Seeders\\": "workbench/database/seeders/"
|
||||
}
|
||||
},
|
||||
"authors": [
|
||||
|
|
@ -35,5 +39,26 @@
|
|||
"allow-plugins": {
|
||||
"pestphp/pest-plugin": true
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"post-autoload-dump": [
|
||||
"@clear",
|
||||
"@prepare"
|
||||
],
|
||||
"clear": "@php vendor/bin/testbench package:purge-skeleton --ansi",
|
||||
"prepare": "@php vendor/bin/testbench package:discover --ansi",
|
||||
"build": "@php vendor/bin/testbench workbench:build --ansi",
|
||||
"serve": [
|
||||
"Composer\\Config::disableProcessTimeout",
|
||||
"@build",
|
||||
"@php vendor/bin/testbench serve --ansi"
|
||||
],
|
||||
"lint": [
|
||||
"@php vendor/bin/phpstan analyse --verbose --ansi"
|
||||
],
|
||||
"test": [
|
||||
"@clear",
|
||||
"@php vendor/bin/pest"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
2
composer.lock
generated
2
composer.lock
generated
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "336ed1e898bc39b0e9990becc327415c",
|
||||
"content-hash": "0ecb4cd71aa6ab475ed1db205f47b632",
|
||||
"packages": [
|
||||
{
|
||||
"name": "brick/math",
|
||||
|
|
|
|||
12
flake.nix
12
flake.nix
|
|
@ -16,10 +16,20 @@
|
|||
system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
php = (
|
||||
pkgs.php.withExtensions (
|
||||
{ enabled, all }:
|
||||
enabled
|
||||
++ [
|
||||
all.pcntl
|
||||
all.xdebug
|
||||
]
|
||||
)
|
||||
);
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
packages = [
|
||||
php
|
||||
php.packages.composer
|
||||
];
|
||||
|
|
|
|||
|
|
@ -4,15 +4,60 @@ declare(strict_types=1);
|
|||
|
||||
namespace Icefox\DTO;
|
||||
|
||||
use Icefox\DTO\Support\RuleFactory;
|
||||
use Illuminate\Support\Collection;
|
||||
use phpDocumentor\Reflection\PseudoTypes\Generic;
|
||||
|
||||
class Config
|
||||
{
|
||||
public static function getCaster(string $className): ?callable
|
||||
/**
|
||||
* @param class-string $class
|
||||
**/
|
||||
public static function getCaster(string $class): ?callable
|
||||
{
|
||||
return config('dto.cast.' . $className, null);
|
||||
return config('dto.cast.' . $class, null);
|
||||
}
|
||||
|
||||
public static function getRules(string $className): ?callable
|
||||
/**
|
||||
* @param class-string $class
|
||||
**/
|
||||
public static function getRules(string $class): ?callable
|
||||
{
|
||||
return config('dto.rules.' . $className, null);
|
||||
if ($userDefined = config('dto.rules.' . $class, null)) {
|
||||
return $userDefined;
|
||||
}
|
||||
return match ($class) {
|
||||
Collection::class => static::rulesIlluminateCollection(...),
|
||||
default => null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return array<string,string[]>
|
||||
*/
|
||||
private static function rulesIlluminateCollection(ParameterMeta $parameter, RuleFactory $factory): array
|
||||
{
|
||||
if (is_null($parameter->tag)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$type = $parameter->tag->getType();
|
||||
if (!$type instanceof Generic) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$subtypes = $type->getTypes();
|
||||
|
||||
if (count($subtypes) == 0) {
|
||||
return ['' => ['array']];
|
||||
}
|
||||
|
||||
$subtype = count($subtypes) == 1 ? $subtypes[0] : $subtypes[1];
|
||||
|
||||
return $factory->mergeRules(
|
||||
['' => ['array']],
|
||||
$factory->getRulesFromDocBlock($subtype, '.*'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,40 +4,13 @@ declare(strict_types=1);
|
|||
|
||||
namespace Icefox\DTO;
|
||||
|
||||
use Icefox\DTO\Attributes\FromInput;
|
||||
use Icefox\DTO\Attributes\CastWith;
|
||||
use Icefox\DTO\Attributes\FromRouteParameter;
|
||||
use Icefox\DTO\Attributes\OverwriteRules;
|
||||
use Icefox\DTO\Support\RuleFactory;
|
||||
use Icefox\DTO\Support\ValueFactory;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\Validation\Validator;
|
||||
use ReflectionClass;
|
||||
use phpDocumentor\Reflection\DocBlock\Tags\Param;
|
||||
|
||||
trait DataObject
|
||||
{
|
||||
public static function fromRequest(Request $request): mixed
|
||||
{
|
||||
$reflection = new ReflectionClass(static::class);
|
||||
$constructor = $reflection->getConstructor();
|
||||
|
||||
$input = [];
|
||||
foreach ($constructor->getParameters() as $parameter) {
|
||||
$parameterName = $parameter->getName();
|
||||
|
||||
foreach ($parameter->getAttributes(FromRouteParameter::class) as $attr) {
|
||||
$name = $attr->newInstance()->name;
|
||||
if ($value = $request->input($name, null)) {
|
||||
$input[$parameterName] = $value;
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
return static::fromArray($input);
|
||||
return DataObjectFactory::fromRequest(static::class, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -45,73 +18,6 @@ trait DataObject
|
|||
*/
|
||||
public static function fromArray(array $input): ?static
|
||||
{
|
||||
$logger = new Log();
|
||||
$parameters = ReflectionHelper::getParametersMeta(static::class);
|
||||
foreach ($parameters as $parameter) {
|
||||
$parameterName = $parameter->reflection->getName();
|
||||
|
||||
foreach ($parameter->reflection->getAttributes(FromInput::class) as $attr) {
|
||||
if ($value = $input[$attr->newInstance()->name] ?? null) {
|
||||
$input[$parameterName] = $value;
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
if ($value = $input[$parameterName] ?? null) {
|
||||
$input[$parameterName] = $value;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($parameter->reflection->isDefaultValueAvailable()) {
|
||||
$input[$parameterName] = $parameter->reflection->getDefaultValue();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
$logger->inputRaw($input);
|
||||
|
||||
$rules = (new RuleFactory($logger))->make(static::class);
|
||||
|
||||
$validator = static::withValidator($input, $rules);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$logger->validationErrors($validator->errors()->toArray());
|
||||
return static::fails($validator);
|
||||
}
|
||||
|
||||
$mappedInput = [];
|
||||
foreach ($parameters as $parameter) {
|
||||
$parameterName = $parameter->reflection->getName();
|
||||
|
||||
if ($castWith = array_first($parameter->reflection->getAttributes(CastWith::class))) {
|
||||
$value = App::call(
|
||||
[App::make($castWith->newInstance()->class), 'cast'],
|
||||
['value' => $validator->getValue($parameterName)],
|
||||
);
|
||||
$mappedInput[$parameterName] = $value;
|
||||
continue;
|
||||
}
|
||||
|
||||
$mappedInput[$parameterName] = ValueFactory::resolveValue(
|
||||
$validator->getValue($parameterName),
|
||||
$parameter->tag instanceof Param ? $parameter->tag->getType() : null,
|
||||
$parameter->reflection,
|
||||
);
|
||||
}
|
||||
$logger->input($mappedInput);
|
||||
return App::make(static::class, $mappedInput);
|
||||
}
|
||||
|
||||
public static function fails(Validator $validator): ?static
|
||||
{
|
||||
throw new ValidationException($validator);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $data
|
||||
* @param array<string,array<int, string|Rule>> $rules
|
||||
*/
|
||||
public static function withValidator(array $data, array $rules): Validator
|
||||
{
|
||||
return App::makeWith(Validator::class, ['data' => $data, 'rules' => $rules]);
|
||||
return DataObjectFactory::fromArray(static::class, $input);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
81
src/DataObjectFactory.php
Normal file
81
src/DataObjectFactory.php
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
namespace Icefox\DTO;
|
||||
|
||||
use Icefox\DTO\Attributes\FromInput;
|
||||
use Icefox\DTO\Attributes\FromRouteParameter;
|
||||
use Icefox\DTO\Support\RuleFactory;
|
||||
use Icefox\DTO\Support\ValueFactory;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Route;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\Validation\Validator;
|
||||
use ReflectionClass;
|
||||
|
||||
class DataObjectFactory
|
||||
{
|
||||
/**
|
||||
* @param class-string $class
|
||||
*/
|
||||
public static function fromRequest(string $class, Request $request): ?object
|
||||
{
|
||||
$routeParameters = $request->route() instanceof Route ? $request->route()->parameters() : [];
|
||||
return static::fromArray($class, $request->input(), $routeParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string $class
|
||||
* @param array<string,mixed> $input
|
||||
* @param array<string,mixed> $routeParameters
|
||||
*/
|
||||
public static function fromArray(string $class, array $input, array $routeParameters): ?object
|
||||
{
|
||||
$logger = new Log();
|
||||
$parameters = ReflectionHelper::getParametersMeta($class);
|
||||
foreach ($parameters as $parameter) {
|
||||
$parameterName = $parameter->reflection->getName();
|
||||
|
||||
foreach ($parameter->reflection->getAttributes(FromRouteParameter::class) as $fromRouteParameter) {
|
||||
if ($value = $routeParameters[$fromRouteParameter->newInstance()->name] ?? null) {
|
||||
$input[$parameterName] = $value;
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($parameter->reflection->getAttributes(FromInput::class) as $attr) {
|
||||
if ($value = $input[$attr->newInstance()->name] ?? null) {
|
||||
$input[$parameterName] = $value;
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
|
||||
if ($value = $input[$parameterName] ?? null) {
|
||||
$input[$parameterName] = $value;
|
||||
continue;
|
||||
}
|
||||
|
||||
// if ($parameter->reflection->isDefaultValueAvailable()) {
|
||||
// $input[$parameterName] = $parameter->reflection->getDefaultValue();
|
||||
// continue;
|
||||
// }
|
||||
}
|
||||
$logger->inputRaw($input);
|
||||
|
||||
$rules = (new RuleFactory($logger))->make($class);
|
||||
|
||||
$validator = method_exists($class, 'withValidator')
|
||||
? App::call("$class::withValidator", ['data' => $input, 'rules' => $rules])
|
||||
: App::makeWith(Validator::class, ['data' => $input, 'rules' => $rules]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
$logger->validationErrors($validator->errors()->toArray());
|
||||
if (method_exists($class, 'fails')) {
|
||||
return App::call("$class::fails", ['validator' => $validator ]);
|
||||
}
|
||||
throw new ValidationException($validator);
|
||||
}
|
||||
|
||||
return ValueFactory::make($class, $validator->validated());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Icefox\DTO;
|
||||
|
||||
use Icefox\DTO\Support\DataObjectRegistrar;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class DataObjectServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
$this->mergeConfigFrom(__DIR__ . '/config/dto.php', 'dto');
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
|
|
@ -2,24 +2,22 @@
|
|||
|
||||
namespace Icefox\DTO\Factories;
|
||||
|
||||
use Icefox\DTO\ParameterMeta;
|
||||
use Icefox\DTO\Support\RuleFactory;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Validation\Rule;
|
||||
use ReflectionParameter;
|
||||
use phpDocumentor\Reflection\PseudoTypes\Generic;
|
||||
use phpDocumentor\Reflection\Type;
|
||||
|
||||
class CollectionFactory
|
||||
{
|
||||
/**
|
||||
* @return array<string,string|Rule[]>
|
||||
* @return array<string,string[]>
|
||||
*/
|
||||
public static function rules(ReflectionParameter $parameter, ?Type $type): array
|
||||
public static function rules(ParameterMeta $parameter, RuleFactory $factory): array
|
||||
{
|
||||
if (is_null($type)) {
|
||||
if (is_null($parameter->tag)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$type = $parameter->tag->getType();
|
||||
if (!$type instanceof Generic) {
|
||||
return [];
|
||||
}
|
||||
|
|
@ -27,14 +25,14 @@ class CollectionFactory
|
|||
$subtypes = $type->getTypes();
|
||||
|
||||
if (count($subtypes) == 0) {
|
||||
return [];
|
||||
return ['' => ['array']];
|
||||
}
|
||||
|
||||
$subtype = count($subtypes) == 1 ? $subtypes[0] : $subtypes[1];
|
||||
|
||||
return array_merge(
|
||||
return $factory->mergeRules(
|
||||
['' => ['array']],
|
||||
RuleFactory::getRulesFromDocBlock($subtype, '.*'),
|
||||
$factory->getRulesFromDocBlock($subtype, '.*'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
41
src/InputFactory.php
Normal file
41
src/InputFactory.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace Icefox\DTO;
|
||||
|
||||
use Icefox\DTO\Attributes\FromInput;
|
||||
use Icefox\DTO\Attributes\FromRouteParameter;
|
||||
|
||||
class InputFactory
|
||||
{
|
||||
public function __construct(public readonly Log $log) {}
|
||||
|
||||
public function make(string $class): array
|
||||
{
|
||||
$map = [];
|
||||
$parameters = ReflectionHelper::getParametersMeta($class);
|
||||
foreach ($parameters as $parameter) {
|
||||
$name = $parameter->reflection->getName();
|
||||
|
||||
foreach ($parameter->reflection->getAttributes(FromRouteParameter::class) as $attr) {
|
||||
$map[$name][] = 'route_' . $attr->newInstance()->name;
|
||||
}
|
||||
|
||||
foreach ($parameter->reflection->getAttributes(FromInput::class) as $attr) {
|
||||
$map[$name][] = $attr->newInstance()->name;
|
||||
}
|
||||
|
||||
$map[$name][] = $name;
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
private static self $_instance;
|
||||
|
||||
public static function instance(): self
|
||||
{
|
||||
if (empty(self::$_instance)) {
|
||||
self::$_instance = new self(new Log());
|
||||
}
|
||||
return self::$_instance;
|
||||
}
|
||||
}
|
||||
67
src/Log.php
67
src/Log.php
|
|
@ -1,67 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Icefox\DTO;
|
||||
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Psr\Log\LogLevel;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
readonly class Log
|
||||
{
|
||||
public LoggerInterface $logger;
|
||||
public function __construct()
|
||||
{
|
||||
$raw = config('dto.log.logger');
|
||||
if (is_callable($raw)) {
|
||||
$this->logger = App::call($raw);
|
||||
return;
|
||||
}
|
||||
if (is_object($raw)) {
|
||||
$this->logger = $raw;
|
||||
return;
|
||||
}
|
||||
if (is_string($raw) && class_exists($raw)) {
|
||||
$this->logger = App::make($raw);
|
||||
return;
|
||||
}
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,array<int, string|Rule>> $rules
|
||||
*/
|
||||
public function rules(array $rules): void
|
||||
{
|
||||
$level = config('dto.log.rules') ?? LogLevel::DEBUG;
|
||||
$this->logger->log($level, print_r($rules, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $input
|
||||
*/
|
||||
public function input(array $input): void
|
||||
{
|
||||
$level = config('dto.log.input') ?? LogLevel::DEBUG;
|
||||
$this->logger->log($level, print_r($input, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, null|int|float|string|array> $input
|
||||
*/
|
||||
public function inputRaw(array $input): void
|
||||
{
|
||||
$level = config('dto.log.raw_input') ?? LogLevel::DEBUG;
|
||||
$this->logger->log($level, print_r($input, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<int, string>> $errors
|
||||
*/
|
||||
public function validationErrors(array $errors): void
|
||||
{
|
||||
$level = config('dto.log.validation_errors') ?? LogLevel::INFO;
|
||||
$this->logger->log($level, print_r($errors, true));
|
||||
}
|
||||
}
|
||||
|
|
@ -4,15 +4,15 @@ declare(strict_types=1);
|
|||
|
||||
namespace Icefox\DTO\Support;
|
||||
|
||||
use Icefox\DTO\Attributes\CastWith;
|
||||
use Icefox\DTO\Attributes\Flat;
|
||||
use Icefox\DTO\Attributes\OverwriteRules;
|
||||
use Icefox\DTO\Config;
|
||||
use Icefox\DTO\Log;
|
||||
use Icefox\DTO\ParameterMeta;
|
||||
use Icefox\DTO\ReflectionHelper;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use ReflectionClass;
|
||||
use ReflectionNamedType;
|
||||
use ReflectionUnionType;
|
||||
|
|
@ -26,19 +26,19 @@ use phpDocumentor\Reflection\Types\Integer;
|
|||
use phpDocumentor\Reflection\Types\Nullable;
|
||||
use phpDocumentor\Reflection\Types\Object_;
|
||||
|
||||
class RuleFactory
|
||||
final class RuleFactory
|
||||
{
|
||||
/**
|
||||
* @return array<string, array<string|Rule>>
|
||||
*/
|
||||
public static function getRulesFromDocBlock(
|
||||
public function getRulesFromDocBlock(
|
||||
Type $type,
|
||||
string $prefix,
|
||||
): array {
|
||||
$rules = [];
|
||||
if ($type instanceof Nullable) {
|
||||
$rules[$prefix] = ['nullable'];
|
||||
$rules = array_merge($rules, self::getRulesFromDocBlock($type->getActualType(), $prefix));
|
||||
$type = $type->getActualType();
|
||||
} else {
|
||||
$rules[$prefix] = ['required'];
|
||||
}
|
||||
|
|
@ -47,7 +47,7 @@ class RuleFactory
|
|||
$rules[$prefix][] = 'array';
|
||||
|
||||
$valueType = $type->getValueType();
|
||||
$rules = array_merge($rules, self::getRulesFromDocBlock($valueType, $prefix . '.*'));
|
||||
$rules = $this->mergeRules($rules, $this->getRulesFromDocBlock($valueType, $prefix . '.*'));
|
||||
}
|
||||
if ($type instanceof Boolean) {
|
||||
$rules[$prefix][] = 'boolean';
|
||||
|
|
@ -55,10 +55,7 @@ class RuleFactory
|
|||
$rules[$prefix][] = 'numeric';
|
||||
} elseif ($type instanceof Object_) {
|
||||
$paramsSub = ReflectionHelper::getParametersMeta($type->getFqsen()->__toString());
|
||||
$rules = array_merge(
|
||||
$rules,
|
||||
self::infer($paramsSub, $prefix),
|
||||
);
|
||||
$rules = $this->mergeRules($rules, $this->infer($paramsSub, $prefix));
|
||||
}
|
||||
return $rules;
|
||||
}
|
||||
|
|
@ -67,14 +64,14 @@ class RuleFactory
|
|||
* @param array<ParameterMeta> $parameters
|
||||
* @return array<string,array<int,string|Rule>>
|
||||
*/
|
||||
public static function infer(array $parameters, string $basePrefix): 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 (self::buildParameterRule($parameter, $prefix) as $key => $newRules) {
|
||||
foreach ($this->buildParameterRule($parameter, $prefix) as $key => $newRules) {
|
||||
$rules[$key] = $newRules;
|
||||
}
|
||||
}
|
||||
|
|
@ -84,7 +81,7 @@ class RuleFactory
|
|||
/**
|
||||
* @return array<string, array<int, string|Rule>>
|
||||
*/
|
||||
public static function buildParameterRule(ParameterMeta $parameter, string $prefix): array
|
||||
public function buildParameterRule(ParameterMeta $parameter, string $prefix): array
|
||||
{
|
||||
$type = $parameter->reflection->getType();
|
||||
|
||||
|
|
@ -93,12 +90,14 @@ class RuleFactory
|
|||
}
|
||||
|
||||
$rules = [$prefix => []];
|
||||
if ($parameter->reflection->isOptional()) {
|
||||
$rules[$prefix][] = 'sometimes';
|
||||
} elseif ($type->allowsNull()) {
|
||||
$rules[$prefix][] = 'nullable';
|
||||
} else {
|
||||
$rules[$prefix][] = 'required';
|
||||
if (!empty($prefix)) {
|
||||
if ($parameter->reflection->isOptional()) {
|
||||
$rules[$prefix][] = 'sometimes';
|
||||
} elseif ($type->allowsNull()) {
|
||||
$rules[$prefix][] = 'nullable';
|
||||
} else {
|
||||
$rules[$prefix][] = 'required';
|
||||
}
|
||||
}
|
||||
|
||||
if ($type instanceof ReflectionUnionType) {
|
||||
|
|
@ -108,11 +107,13 @@ class RuleFactory
|
|||
|
||||
if ($type instanceof ReflectionNamedType && $name = $type->getName()) {
|
||||
if ($globalRules = Config::getRules($name)) {
|
||||
foreach ($globalRules($parameter->reflection, $parameter->tag->getType()) as $scopedPrefix => $values) {
|
||||
foreach ($globalRules($parameter, $this) as $scopedPrefix => $values) {
|
||||
$realPrefix = $prefix . $scopedPrefix;
|
||||
$rules[$realPrefix] = array_values(array_unique(array_merge($rules[$realPrefix] ?? [], $values)));
|
||||
$rules[$realPrefix] = array_merge($rules[$realPrefix] ?? [], $values);
|
||||
}
|
||||
return $rules;
|
||||
}
|
||||
|
||||
if ($name === 'string') {
|
||||
} elseif ($name === 'bool') {
|
||||
$rules[$prefix][] = 'boolean';
|
||||
|
|
@ -127,38 +128,24 @@ class RuleFactory
|
|||
}
|
||||
} else {
|
||||
$paramsSub = ReflectionHelper::getParametersMeta($type->getName());
|
||||
$rules = array_merge(
|
||||
$rules,
|
||||
self::infer($paramsSub, $prefix),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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) ? $prefix : ($prefix . '.' . $key);
|
||||
$rules[$path] = array_values(array_unique(array_merge($rules[$path] ?? [], $value)));
|
||||
}
|
||||
$rules = $this->mergeRules($rules, $this->infer($paramsSub, $prefix));
|
||||
}
|
||||
}
|
||||
|
||||
if ($parameter->tag instanceof Param) {
|
||||
$docblockRules = self::getRulesFromDocBlock(
|
||||
$docblockRules = $this->getRulesFromDocBlock(
|
||||
$parameter->tag->getType(),
|
||||
$prefix,
|
||||
);
|
||||
foreach ($docblockRules as $key => &$values) {
|
||||
$rules[$key] = array_values(array_unique(array_merge($rules[$key] ?? [], $values)));
|
||||
}
|
||||
$rules = $this->mergeRules($rules, $docblockRules);
|
||||
}
|
||||
if (empty($rules[$prefix])) {
|
||||
unset($rules[$prefix]);
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function __construct(public Log $log) {}
|
||||
public function __construct(public LoggerInterface $log) {}
|
||||
|
||||
/**
|
||||
* @param class-string $class
|
||||
|
|
@ -178,21 +165,21 @@ class RuleFactory
|
|||
$rules = $customRules;
|
||||
} else {
|
||||
$inferredRules = RuleFactory::infer($parameters, '');
|
||||
$rules = self::mergeRules($inferredRules, $customRules);
|
||||
$rules = $this->mergeRules($inferredRules, $customRules);
|
||||
}
|
||||
$this->log->rules($rules);
|
||||
$this->log->info('Constructed rules for class ' . $class, $rules);
|
||||
return $rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,array<int, string>> $inferredRules
|
||||
* @param array<string,array<int, string>> $customRules
|
||||
* @param array<string,array<int, string>> $first
|
||||
* @param array<string,array<int, string>> $second
|
||||
* @return array<string,array<int, string>>
|
||||
*/
|
||||
protected function mergeRules(array $inferredRules, array $customRules): array
|
||||
public function mergeRules(array $first, array $second): array
|
||||
{
|
||||
$merged = $inferredRules;
|
||||
foreach ($customRules as $key => $rules) {
|
||||
$merged = $first;
|
||||
foreach ($second as $key => $rules) {
|
||||
if (isset($merged[$key])) {
|
||||
$merged[$key] = array_values(array_unique(array_merge($merged[$key], $rules)));
|
||||
} else {
|
||||
|
|
@ -201,4 +188,14 @@ class RuleFactory
|
|||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,14 @@ declare(strict_types=1);
|
|||
namespace Icefox\DTO\Support;
|
||||
|
||||
use Icefox\DTO\Attributes\CastWith;
|
||||
use Icefox\DTO\Attributes\Flat;
|
||||
use Icefox\DTO\Config;
|
||||
use Icefox\DTO\ParameterMeta;
|
||||
use Icefox\DTO\ReflectionHelper;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use ReflectionNamedType;
|
||||
use ReflectionParameter;
|
||||
use phpDocumentor\Reflection\DocBlock\Tags\Param;
|
||||
use phpDocumentor\Reflection\PseudoTypes\Generic;
|
||||
use phpDocumentor\Reflection\Type;
|
||||
use phpDocumentor\Reflection\Types\AbstractList;
|
||||
|
|
@ -35,31 +39,37 @@ class ValueFactory
|
|||
return App::makeWith($className, $rawValue);
|
||||
}
|
||||
|
||||
public static function resolveTypedValue(mixed $rawValue, Type $type): mixed
|
||||
public static function resolveAnnotatedValue(Type $type, mixed $rawValue): mixed
|
||||
{
|
||||
if ($type instanceof Nullable) {
|
||||
$type = $type->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);
|
||||
$result[$key] = self::resolveAnnotatedValue($innerType, $value);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($type instanceof Generic) {
|
||||
$types = $type->getTypes();
|
||||
$innerType = count($types) === 2 ? $types[1] : $types[0];
|
||||
if (is_array($rawValue)) {
|
||||
$innerValues = [];
|
||||
foreach ($rawValue as $key => $value) {
|
||||
$innerValues[$key] = self::resolveAnnotatedValue($innerType, $value);
|
||||
}
|
||||
return array_key_exists(0, $rawValue)
|
||||
? new ($type->getFqsen()->__toString())($innerValues)
|
||||
: App::makeWith($type->getFqsen()->__toString(), $innerValues);
|
||||
}
|
||||
$value = self::resolveAnnotatedValue($innerType, $rawValue);
|
||||
return new ($type->getFqsen()->__toString())($value);
|
||||
}
|
||||
|
||||
if ($type instanceof Boolean) {
|
||||
return boolval($rawValue);
|
||||
}
|
||||
|
|
@ -73,44 +83,70 @@ class ValueFactory
|
|||
}
|
||||
|
||||
if ($type instanceof Object_) {
|
||||
return self::constructObject($type->getFqsen()->__toString(), $rawValue);
|
||||
return self::make($type->getFqsen()->__toString(), $rawValue);
|
||||
}
|
||||
|
||||
return $rawValue;
|
||||
}
|
||||
|
||||
public static function resolveValue(mixed $rawValue, ?Type $type, ReflectionParameter $reflection): mixed
|
||||
public static function resolveDeclaredTypeValue(ReflectionNamedType $parameter, mixed $rawValue): mixed
|
||||
{
|
||||
if ($reflection->allowsNull() && is_null($rawValue)) {
|
||||
return null;
|
||||
}
|
||||
return match ($parameter->getName()) {
|
||||
'string' => $rawValue,
|
||||
'bool' => boolval($rawValue),
|
||||
'int' => intval($rawValue),
|
||||
'float' => floatval($rawValue),
|
||||
'array' => $rawValue,
|
||||
default => self::make($parameter->getName(), $rawValue),
|
||||
|
||||
$castWithAttrs = $reflection->getAttributes(CastWith::class);
|
||||
if ($withCast = $reflection->getAttributes(CastWith::class)[0] ?? null) {
|
||||
$caster = $withCast->newInstance()->class;
|
||||
return App::call("$caster@cast", ['value' => $rawValue]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!is_null($type)) {
|
||||
return self::resolveTypedValue($rawValue, $type);
|
||||
}
|
||||
public static function make(string $class, array $input): object
|
||||
{
|
||||
$parameters = ReflectionHelper::getParametersMeta($class);
|
||||
$arguments = [];
|
||||
foreach ($parameters as $parameter) {
|
||||
$name = $parameter->reflection->getName();
|
||||
|
||||
$reflectedType = $reflection->getType();
|
||||
if ($reflectedType instanceof ReflectionNamedType && $name = $reflectedType->getName()) {
|
||||
if ($caster = Config::getCaster($name)) {
|
||||
return App::call($caster, ['value' => $rawValue]);
|
||||
$parameterArgs = empty($parameter->reflection->getAttributes(Flat::class)) ? ($input[$name] ?? null) : $input;
|
||||
|
||||
if (is_null($parameterArgs)) {
|
||||
if ($parameter->reflection->allowsNull()) {
|
||||
$arguments[$name] = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
return match ($name) {
|
||||
'string' => $rawValue,
|
||||
'bool' => boolval($rawValue),
|
||||
'int' => intval($rawValue),
|
||||
'float' => floatval($rawValue),
|
||||
'array' => $rawValue,
|
||||
default => self::constructObject($name, $rawValue),
|
||||
};
|
||||
}
|
||||
if ($caster = $parameter->reflection->getAttributes(CastWith::class)[0] ?? null) {
|
||||
$caster = $caster->newInstance()->class;
|
||||
$arguments[$name] = App::call("$caster@cast", ['data' => $parameterArgs]);
|
||||
continue;
|
||||
}
|
||||
|
||||
return $rawValue;
|
||||
$parameterClass = $parameter->reflection->getClass()?->getName();
|
||||
|
||||
$type = $parameter->tag?->getType();
|
||||
if (empty($parameterClass) && $type instanceof Object_) {
|
||||
$parameterClass = $type->getFqsen();
|
||||
}
|
||||
if (!empty($parameterClass) && $caster = config('dto.cast.' . $parameterClass, null)) {
|
||||
$arguments[$name] = App::call($caster, ['data' => $parameterArgs]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($parameter->tag instanceof Param) {
|
||||
$arguments[$name] = self::resolveAnnotatedValue($type, $parameterArgs);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($parameter->reflection->getType() instanceof ReflectionNamedType) {
|
||||
$arguments[$name] = self::resolveDeclaredTypeValue($parameter->reflection->getType(), $parameterArgs);
|
||||
continue;
|
||||
}
|
||||
|
||||
$arguments[$name] = $parameterArgs;
|
||||
}
|
||||
return App::makeWith($class, $arguments);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Icefox\DTO\Factories\CollectionFactory;
|
||||
use Illuminate\Support\Collection;
|
||||
use Psr\Log\LogLevel;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
return [
|
||||
'cast' => [],
|
||||
'rules' => [
|
||||
Collection::class => CollectionFactory::rules(...),
|
||||
],
|
||||
'log' => [
|
||||
'logger' => NullLogger::class,
|
||||
'internal' => LogLevel::WARNING,
|
||||
'rules' => LogLevel::DEBUG,
|
||||
'input' => LogLevel::DEBUG,
|
||||
'raw_input' => LogLevel::DEBUG,
|
||||
'validation_errors' => LogLevel::INFO,
|
||||
],
|
||||
];
|
||||
|
|
@ -17,8 +17,8 @@ describe('caster priority', function () {
|
|||
});
|
||||
|
||||
it('uses CastWith attribute over global config caster', function () {
|
||||
$globalCaster = function (mixed $value): SimpleValue {
|
||||
return new SimpleValue($value['value'] * 3);
|
||||
$globalCaster = function (mixed $data): SimpleValue {
|
||||
return new SimpleValue($data * 3);
|
||||
};
|
||||
config(['dto.cast.' . SimpleValue::class => $globalCaster]);
|
||||
|
||||
|
|
@ -30,16 +30,16 @@ describe('caster priority', function () {
|
|||
});
|
||||
|
||||
it('falls back to global config caster when no CastWith attribute', function () {
|
||||
$globalCaster = function (mixed $value): SimpleValue {
|
||||
return new SimpleValue($value['value'] * 3);
|
||||
$globalCaster = function (mixed $data): SimpleValue {
|
||||
return new SimpleValue($data['value'] * 3);
|
||||
};
|
||||
config(['dto.cast.' . SimpleValue::class => $globalCaster]);
|
||||
|
||||
$object = WithGlobalCaster::fromArray([
|
||||
'value' => ['value' => 5],
|
||||
'simple' => ['value' => 5],
|
||||
]);
|
||||
|
||||
expect($object->value->value)->toBe(15); // 5 * 3
|
||||
expect($object->simple->value)->toBe(15); // 5 * 3
|
||||
});
|
||||
|
||||
it('falls back to default construction when no caster exists', function () {
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@ namespace Tests\Casters;
|
|||
|
||||
class SimpleValueCaster
|
||||
{
|
||||
public function cast(mixed $value): SimpleValue
|
||||
public function cast(mixed $data): SimpleValue
|
||||
{
|
||||
return new SimpleValue($value['value'] * 2);
|
||||
return new SimpleValue($data['value'] * 2);
|
||||
}
|
||||
|
||||
public static function rules(): array
|
||||
|
|
|
|||
|
|
@ -11,6 +11,6 @@ readonly class WithGlobalCaster
|
|||
use DataObject;
|
||||
|
||||
public function __construct(
|
||||
public SimpleValue $value,
|
||||
public SimpleValue $simple,
|
||||
) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,9 +7,9 @@ use Illuminate\Support\Carbon;
|
|||
|
||||
class CarbonPeriodMapper
|
||||
{
|
||||
public function cast(mixed $value): CarbonPeriodImmutable
|
||||
public function cast(mixed $data): CarbonPeriodImmutable
|
||||
{
|
||||
return new CarbonPeriodImmutable(Carbon::parse($value['start']), Carbon::parse($value['end']));
|
||||
return new CarbonPeriodImmutable(Carbon::parse($data['start']), Carbon::parse($data['end']));
|
||||
}
|
||||
|
||||
public static function rules(): array
|
||||
|
|
|
|||
|
|
@ -1,12 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Flattening\Classes;
|
||||
|
||||
use Icefox\DTO\Attributes\Flat;
|
||||
use Icefox\DTO\DataObject;
|
||||
|
||||
class BasicRoot
|
||||
{
|
||||
use DataObject;
|
||||
public function __construct(public string $text, #[Flat] public RequiredLeaf $leaf) {}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Flattening\Classes;
|
||||
|
||||
use Icefox\DTO\DataObject;
|
||||
|
||||
class RequiredLeaf
|
||||
{
|
||||
use DataObject;
|
||||
|
||||
public function __construct(public int $value) {}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Flattening;
|
||||
|
||||
use Icefox\DTO\Log;
|
||||
use Icefox\DTO\Support\RuleFactory;
|
||||
use Tests\Flattening\Classes\BasicRoot;
|
||||
|
||||
describe('flattens required parameters', function () {
|
||||
it('generates correct rules', function () {
|
||||
|
||||
$rules = (new RuleFactory(new Log()))->make(BasicRoot::class);
|
||||
expect($rules)->toMatchArray([
|
||||
'text' => ['required'],
|
||||
'value' => ['required', 'numeric'],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
@ -15,7 +15,7 @@ use Tests\Rules\WithOverwriteRules;
|
|||
describe('rules array shape', function () {
|
||||
it('returns inferred rules shape from RuleFactory::infer (inferred only)', function () {
|
||||
$parameters = ReflectionHelper::getParametersMeta(WithMergedRules::class);
|
||||
$rules = RuleFactory::infer($parameters, '', '');
|
||||
$rules = RuleFactory::infer($parameters, '');
|
||||
|
||||
expect($rules)->toBe([
|
||||
'value' => ['required', 'numeric'],
|
||||
|
|
@ -24,7 +24,7 @@ describe('rules array shape', function () {
|
|||
|
||||
it('returns inferred rules shape regardless of OverwriteRules attribute', function () {
|
||||
$parameters = ReflectionHelper::getParametersMeta(WithOverwriteRules::class);
|
||||
$rules = RuleFactory::infer($parameters, '', '');
|
||||
$rules = RuleFactory::infer($parameters, '');
|
||||
|
||||
expect($rules)->toBe([
|
||||
'value' => ['required', 'numeric'],
|
||||
|
|
@ -93,30 +93,8 @@ describe('rules overwrite', function () {
|
|||
});
|
||||
|
||||
it('does not enforce inferred required rule when overwritten', function () {
|
||||
$object = WithOverwriteRules::fromArray([]);
|
||||
|
||||
expect($object)->toBeInstanceOf(WithOverwriteRules::class);
|
||||
});
|
||||
|
||||
it('does not enforce inferred numeric rule when overwritten', function () {
|
||||
$rules = WithOverwriteRules::rules();
|
||||
expect($rules)->toHaveKey('value');
|
||||
expect($rules['value'])->toBe(['numeric', 'max:20']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty rules overwrite', function () {
|
||||
it('allows any value when rules are empty with OverwriteRules', function () {
|
||||
$object = WithEmptyOverwriteRules::fromArray([
|
||||
'value' => 999,
|
||||
]);
|
||||
|
||||
expect($object->value)->toBe(999);
|
||||
});
|
||||
|
||||
it('allows missing value when rules are empty with OverwriteRules', function () {
|
||||
$object = WithEmptyOverwriteRules::fromArray([]);
|
||||
|
||||
expect($object)->toBeInstanceOf(WithEmptyOverwriteRules::class);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
161
tests/RulesTest.php
Normal file
161
tests/RulesTest.php
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Icefox\DTO\Attributes\Flat;
|
||||
use Icefox\DTO\Support\RuleFactory;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
readonly class BasicPrimitives
|
||||
{
|
||||
public function __construct(
|
||||
public string $text,
|
||||
public int $number,
|
||||
public bool $flag,
|
||||
public ?array $items,
|
||||
public float $floating = 0.0,
|
||||
) {}
|
||||
}
|
||||
|
||||
test('required rules', function () {
|
||||
expect(RuleFactory::instance()->make(BasicPrimitives::class))->toBe([
|
||||
'text' => ['required'],
|
||||
'number' => ['required', 'numeric'],
|
||||
'flag' => ['required', 'boolean'],
|
||||
'items' => ['nullable', 'array'],
|
||||
'floating' => ['sometimes', 'numeric'],
|
||||
]);
|
||||
});
|
||||
|
||||
readonly class AnnotatedArray
|
||||
{
|
||||
/**
|
||||
* @param array<int,float> $items
|
||||
*/
|
||||
public function __construct(public array $items) {}
|
||||
}
|
||||
test('annotated array', function () {
|
||||
expect(RuleFactory::instance()->make(AnnotatedArray::class))->toBe([
|
||||
'items' => ['required', 'array'],
|
||||
'items.*' => ['required', 'numeric'],
|
||||
]);
|
||||
});
|
||||
|
||||
readonly class AnnotatedArrayNullableValue
|
||||
{
|
||||
/**
|
||||
* @param array<?float> $items
|
||||
*/
|
||||
public function __construct(public array $items) {}
|
||||
}
|
||||
test('annotated array with nullable items', function () {
|
||||
expect(RuleFactory::instance()->make(AnnotatedArrayNullableValue::class))->toBe([
|
||||
'items' => ['required', 'array'],
|
||||
'items.*' => ['nullable', 'numeric'],
|
||||
]);
|
||||
});
|
||||
|
||||
readonly class PlainLeaf
|
||||
{
|
||||
public function __construct(public string $name) {}
|
||||
}
|
||||
readonly class PlainRoot
|
||||
{
|
||||
public function __construct(public int $value, public PlainLeaf $leaf) {}
|
||||
}
|
||||
|
||||
test('plain nesting', function () {
|
||||
expect(RuleFactory::instance()->make(PlainRoot::class))->toBe([
|
||||
'value' => ['required', 'numeric'],
|
||||
'leaf' => ['required'],
|
||||
'leaf.name' => ['required'],
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
readonly class AnnotatedArrayItem
|
||||
{
|
||||
public function __construct(public int $value) {}
|
||||
}
|
||||
|
||||
readonly class AnnotatedArrayObject
|
||||
{
|
||||
/**
|
||||
* @param ?array<AnnotatedArrayItem> $items
|
||||
*/
|
||||
public function __construct(public ?array $items) {}
|
||||
}
|
||||
|
||||
test('annotated array with object', function () {
|
||||
expect(RuleFactory::instance()->make(AnnotatedArrayObject::class))->toBe([
|
||||
'items' => ['nullable', 'array'],
|
||||
'items.*' => ['required'],
|
||||
'items.*.value' => ['required', 'numeric'],
|
||||
]);
|
||||
});
|
||||
|
||||
readonly class FlattenedLeaf
|
||||
{
|
||||
public function __construct(public ?bool $flag) {}
|
||||
}
|
||||
|
||||
readonly class NotFlattenedLeaf
|
||||
{
|
||||
public function __construct(public string $description) {}
|
||||
}
|
||||
|
||||
readonly class FlattenedNode
|
||||
{
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public NotFlattenedLeaf $leaf,
|
||||
#[Flat]
|
||||
public FlattenedLeaf $squish,
|
||||
public int $level = 1,
|
||||
) {}
|
||||
}
|
||||
|
||||
readonly class FlattenedRoot
|
||||
{
|
||||
public function __construct(
|
||||
public int $value,
|
||||
#[Flat]
|
||||
public FlattenedNode $node,
|
||||
) {}
|
||||
}
|
||||
|
||||
test('flattened basic', function () {
|
||||
expect(RuleFactory::instance()->make(FlattenedRoot::class))->toBe([
|
||||
'value' => ['required', 'numeric'],
|
||||
'id' => ['required' ],
|
||||
'leaf' => ['required'],
|
||||
'leaf.description' => ['required'],
|
||||
'flag' => ['nullable', 'boolean'],
|
||||
'level' => ['sometimes', 'numeric'],
|
||||
]);
|
||||
});
|
||||
|
||||
readonly class AnnotatedCollectionItem
|
||||
{
|
||||
public function __construct(public int $value) {}
|
||||
}
|
||||
|
||||
readonly class AnnotatedCollection
|
||||
{
|
||||
/**
|
||||
* @param Collection<AnnotatedCollectionItem> $group
|
||||
*/
|
||||
public function __construct(public Collection $group) {}
|
||||
}
|
||||
|
||||
test('annotated collection', function () {
|
||||
expect(RuleFactory::instance()->make(AnnotatedCollection::class))->toBe([
|
||||
'group' => ['required', 'array'],
|
||||
'group.*' => ['required'],
|
||||
'group.*.value' => ['required', 'numeric'],
|
||||
]);
|
||||
});
|
||||
|
|
@ -2,15 +2,11 @@
|
|||
|
||||
namespace Tests;
|
||||
|
||||
use Icefox\DTO\DataObjectServiceProvider;
|
||||
use Orchestra\Testbench\TestCase as BaseTestCase;
|
||||
use Illuminate\Contracts\Config\Repository;
|
||||
use Monolog\Formatter\JsonFormatter;
|
||||
use Orchestra\Testbench\Concerns\WithWorkbench;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
abstract class TestCase extends \Orchestra\Testbench\TestCase
|
||||
{
|
||||
protected function getPackageProviders($app)
|
||||
{
|
||||
return [
|
||||
DataObjectServiceProvider::class,
|
||||
];
|
||||
}
|
||||
use WithWorkbench;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
|
||||
use function Orchestra\Testbench\default_skeleton_path;
|
||||
|
||||
return Application::configure(basePath: $APP_BASE_PATH ?? default_skeleton_path())->create();
|
||||
15
workbench/config/dto.php
Normal file
15
workbench/config/dto.php
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
return [
|
||||
'logging' => [
|
||||
'channel' => 'dto',
|
||||
'context' => [
|
||||
'rules' => LogLevel::NOTICE,
|
||||
'input' => LogLevel::INFO,
|
||||
'casts' => LogLevel::INFO,
|
||||
'internals' => LogLevel::DEBUG,
|
||||
],
|
||||
],
|
||||
];
|
||||
16
workbench/config/logging.php
Normal file
16
workbench/config/logging.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
use Monolog\Formatter\JsonFormatter;
|
||||
|
||||
return [
|
||||
'default' => env('LOG_CHANNEL', 'single'),
|
||||
'channels' => [
|
||||
'dto' => [
|
||||
'driver' => 'single',
|
||||
'path' => getcwd() . '/logs/dto.log',
|
||||
'level' => 'debug',
|
||||
'replace_placeholders' => true,
|
||||
'formatter' => JsonFormatter::class,
|
||||
],
|
||||
],
|
||||
];
|
||||
17
workbench/phpunit.xml
Normal file
17
workbench/phpunit.xml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="root">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
<include>
|
||||
<directory>app</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
||||
Loading…
Add table
Add a link
Reference in a new issue