diff --git a/src/DataObjectFactory.php b/src/DataObjectFactory.php index 3a14c7e..2d14088 100644 --- a/src/DataObjectFactory.php +++ b/src/DataObjectFactory.php @@ -13,6 +13,8 @@ use Illuminate\Support\Facades\Log; use Illuminate\Validation\ValidationException; use Illuminate\Validation\Validator; use Psr\Log\LoggerInterface; +use ReflectionNamedType; +use phpDocumentor\Reflection\Types\AbstractList; class DataObjectFactory { @@ -21,7 +23,8 @@ class DataObjectFactory */ public static function fromRequest(string $class, Request $request): ?object { - $routeParameters = $request->route() instanceof Route ? $request->route()->parameters() : []; + $route = $request->route(); + $routeParameters = $route instanceof Route ? $route->parameters() : []; return static::fromArray($class, $request->input(), $routeParameters); } @@ -52,7 +55,6 @@ class DataObjectFactory return ValueFactory::make($class, $validator->validated()); } - /** * @param class-string $class * @param array $rawInput @@ -67,6 +69,7 @@ class DataObjectFactory ): array { $input = []; $parameters = ReflectionHelper::getParametersMeta($class); + foreach ($parameters as $parameter) { $parameterName = $parameter->reflection->getName(); @@ -77,17 +80,51 @@ class DataObjectFactory } } + $reflectionType = $parameter->reflection->getType(); + $namedType = $reflectionType instanceof ReflectionNamedType ? $reflectionType->getName() : null; + $annotatedType = $parameter->tag?->getType(); + + $isListType + = $parameter->reflection->isArray() + || in_array($namedType, config('dto.listTypes', [])) + || in_array($annotatedType?->__toString(), config('dto.listTypes', [])) + || $annotatedType instanceof AbstractList; + foreach ($parameter->reflection->getAttributes(FromInput::class) as $attr) { if ($value = $rawInput[$attr->newInstance()->name] ?? null) { - $input[$parameterName] = $value; + if ($valueType = ReflectionHelper::getListParameterValueType($parameter->tag)) { + $input[$parameterName] = $isListType + ? array_map( + fn($element) => self::mapInput($valueType, $element, $routeParameters, $logger), + $value, + ) + : self::mapInput($valueType, $value, $routeParameters, $logger); + } else { + $input[$parameterName] = $value; + } continue 2; } } - if ($value = $rawInput[$parameterName] ?? null) { - $input[$parameterName] = $value; + if ($valueType = ReflectionHelper::getListParameterValueType($parameter->tag)) { + $input[$parameterName] = $isListType + ? array_map( + fn($element) => self::mapInput($valueType, $element, $routeParameters, $logger), + $rawInput[$parameterName], + ) + : self::mapInput($valueType, $rawInput[$parameterName], $routeParameters, $logger); continue; } + + if ($reflectionType instanceof ReflectionNamedType) { + $input[$parameterName] = $reflectionType->isBuiltin() + ? $rawInput[$parameterName] + : self::mapInput($reflectionType->__toString(), $rawInput[$parameterName], $routeParameters, $logger); + + continue; + } + + $input[$parameterName] = $rawInput[$parameterName]; } $logger->debug('input', $input); return $input; diff --git a/src/ReflectionHelper.php b/src/ReflectionHelper.php index 1ab7448..b6f77a5 100644 --- a/src/ReflectionHelper.php +++ b/src/ReflectionHelper.php @@ -5,6 +5,8 @@ namespace Icefox\DTO; use ReflectionParameter; use phpDocumentor\Reflection\DocBlock\Tag; use phpDocumentor\Reflection\DocBlock\Tags\Param; +use phpDocumentor\Reflection\PseudoTypes\Generic; +use phpDocumentor\Reflection\Types\AbstractList; use phpDocumentor\Reflection\Types\ContextFactory; use phpDocumentor\Reflection\DocBlockFactory; use ReflectionClass; @@ -44,4 +46,21 @@ class ReflectionHelper ); return self::$cache[$class]; } + + public static function getListParameterValueType(?Param $param): ?string + { + $type = $param?->getType(); + + if ($type instanceof AbstractList) { + return $type->getValueType()->__toString(); + } + + if (!$type instanceof Generic) { + return null; + } + + $subtypes = $type->getTypes(); + return count($subtypes) > 1 ? $subtypes[1]->__toString() : $subtypes[0]->__toString(); + } + } diff --git a/tests/Casters/CasterTest.php b/tests/Casters/CasterTest.php deleted file mode 100644 index 93fc5ce..0000000 --- a/tests/Casters/CasterTest.php +++ /dev/null @@ -1,71 +0,0 @@ - []]); - }); - - it('uses CastWith attribute over global config caster', function () { - $globalCaster = function (mixed $data): SimpleValue { - return new SimpleValue($data * 3); - }; - config(['dto.cast.' . SimpleValue::class => $globalCaster]); - - $object = WithSpecificCaster::fromArray([ - 'value' => ['value' => 5], - ]); - - expect($object->value->value)->toBe(10); // 5 * 2 - }); - - it('falls back to global config caster when no CastWith attribute', function () { - $globalCaster = function (mixed $data): SimpleValue { - return new SimpleValue($data['value'] * 3); - }; - config(['dto.cast.' . SimpleValue::class => $globalCaster]); - - $object = WithGlobalCaster::fromArray([ - 'simple' => ['value' => 5], - ]); - - expect($object->simple->value)->toBe(15); // 5 * 3 - }); - - it('falls back to default construction when no caster exists', function () { - $object = WithoutCaster::fromArray([ - 'value' => ['value' => 5], - ]); - expect($object)->toBeInstanceOf(WithoutCaster::class); - }); -}); - -describe('caster with rules', function () { - beforeEach(function () { - config(['dto.cast' => []]); - }); - - it('validates input using caster rules before casting', function () { - expect(fn() => WithSpecificCaster::fromArray([ - 'value' => [], - ]))->toThrow(ValidationException::class); - }); - - it('accepts valid input and casts correctly', function () { - $object = WithSpecificCaster::fromArray([ - 'value' => ['value' => 10], - ]); - - expect($object->value->value)->toBe(20); // 10 * 2 - }); -}); diff --git a/tests/Casters/SimpleValue.php b/tests/Casters/SimpleValue.php deleted file mode 100644 index 1f08683..0000000 --- a/tests/Casters/SimpleValue.php +++ /dev/null @@ -1,10 +0,0 @@ - ['required', 'numeric'], - ]; - } -} diff --git a/tests/Casters/WithGlobalCaster.php b/tests/Casters/WithGlobalCaster.php deleted file mode 100644 index d829310..0000000 --- a/tests/Casters/WithGlobalCaster.php +++ /dev/null @@ -1,16 +0,0 @@ - $values - */ - public function __construct(public array $values) {} -} diff --git a/tests/Classes/CarbonPeriodMapper.php b/tests/Classes/CarbonPeriodMapper.php deleted file mode 100644 index 4edee01..0000000 --- a/tests/Classes/CarbonPeriodMapper.php +++ /dev/null @@ -1,22 +0,0 @@ - ['required', 'date'], - 'end' => ['required', 'date'], - ]; - } -} diff --git a/tests/Classes/CollectionDataObject.php b/tests/Classes/CollectionDataObject.php deleted file mode 100644 index 711dbc7..0000000 --- a/tests/Classes/CollectionDataObject.php +++ /dev/null @@ -1,17 +0,0 @@ - $values - */ - public function __construct(public Collection $values) {} -} diff --git a/tests/Classes/FailsReturnsDefault.php b/tests/Classes/FailsReturnsDefault.php deleted file mode 100644 index 7fdba3a..0000000 --- a/tests/Classes/FailsReturnsDefault.php +++ /dev/null @@ -1,23 +0,0 @@ -json(['errors' => $validator->errors()], 422) - ); - } -} diff --git a/tests/Classes/FromInputObject.php b/tests/Classes/FromInputObject.php deleted file mode 100644 index 575e44c..0000000 --- a/tests/Classes/FromInputObject.php +++ /dev/null @@ -1,19 +0,0 @@ - ['value' => 1 ] ], [], new NullLogger()); + expect($input)->toBe(['element' => ['value' => 1]]); +}); + +readonly class MappedElement +{ + public function __construct(#[FromInput('name')] public int $value) {} +} + +readonly class MappedNode +{ + public function __construct(public MappedElement $element) {} +} + +test('basic nested input map', function () { + $input = DataObjectFactory::mapInput(MappedNode::class, ['element' => ['name' => 1 ] ], [], new NullLogger()); + expect($input)->toBe(['element' => ['value' => 1]]); +}); + +readonly class MappedCollectionItem +{ + public function __construct(#[FromInput('id_item')] public int $idItem) {} +} + +readonly class MappedCollectionRoot +{ + /** + * @param Collection $items + */ + public function __construct(public string $text, #[FromInput('data')] public Collection $items) {} +} + +test('using from input nested', function () { + $mapped = DataObjectFactory::mapInput(MappedCollectionRoot::class, [ + 'text' => 'abc', + 'data' => [ + [ 'id_item' => 1 ], + [ 'id_item' => 2 ], + [ 'id_item' => 4 ], + [ 'id_item' => 8 ], + ], + ], [], new NullLogger()); + + expect($mapped)->toBe([ + 'text' => 'abc', + 'items' => [ + [ 'idItem' => 1 ], + [ 'idItem' => 2 ], + [ 'idItem' => 4 ], + [ 'idItem' => 8 ], + ], + ]); +}); + +readonly class CollectionRoot +{ + /** + * @param Collection $items + */ + public function __construct(public string $text, public Collection $items) {} +} + +test('using from input', function () { + $mapped = DataObjectFactory::mapInput(MappedCollectionRoot::class, [ + 'text' => 'abc', + 'items' => [ + [ 'id_item' => 1 ], + [ 'id_item' => 2 ], + [ 'id_item' => 4 ], + [ 'id_item' => 8 ], + ], + ], [], new NullLogger()); + + expect($mapped)->toBe([ + 'text' => 'abc', + 'items' => [ + [ 'idItem' => 1 ], + [ 'idItem' => 2 ], + [ 'idItem' => 4 ], + [ 'idItem' => 8 ], + ], + ]); +}); + +readonly class AnnotatedArrayItem +{ + public function __construct(#[FromInput('name')] public float $value) {} +} + +readonly class AnnotatedArray +{ + /** + * @param array $items + */ + public function __construct(public array $items) {} +} + +test('annotated array', function () { + $mapped = DataObjectFactory::mapInput( + AnnotatedArray::class, + ['items' => [['name' => 1], ['name' => 2]]], + [], + new NullLogger(), + ); + + expect($mapped)->toBe(['items' => [['value' => 1], ['value' => 2]]]); +}); diff --git a/tests/DataObjectTest.php b/tests/DataObjectTest.php deleted file mode 100644 index 8886ac2..0000000 --- a/tests/DataObjectTest.php +++ /dev/null @@ -1,44 +0,0 @@ - $items - */ - public function __construct(public string $text, #[FromInput('data')] public Collection $items) {} -} - -test('using from input', function () { - $mapped = DataObjectFactory::mapInput(MappedCollectionRoot::class, [ - 'text' => 'abc', - 'data' => [ - [ 'id_item' => 1 ], - [ 'id_item' => 2 ], - [ 'id_item' => 4 ], - [ 'id_item' => 8 ], - ], - ], [], new NullLogger()); - var_dump($mapped); - - expect($mapped)->toBe([ - 'text' => 'abc', - 'items' => [ - [ 'idItem' => 1 ], - [ 'idItem' => 2 ], - [ 'idItem' => 4 ], - [ 'idItem' => 8 ], - ], - ]); -}); diff --git a/tests/FailedValidation/FailsMethodTest.php b/tests/FailedValidation/FailsMethodTest.php deleted file mode 100644 index 11b2ca1..0000000 --- a/tests/FailedValidation/FailsMethodTest.php +++ /dev/null @@ -1,108 +0,0 @@ - 0, - 'float' => 3.14, - 'bool' => true, - ]); - })->toThrow(ValidationException::class); - }); - - it('returns null when fails() returns null', function () { - $result = FailsReturnsNull::fromArray([ - 'int' => 0, - ]); - - expect($result)->toBeNull(); - }); - - it('returns static instance when fails() returns an object', function () { - $result = FailsReturnsDefault::fromArray([ - 'int' => 0, - ]); - - expect($result)->toBeInstanceOf(FailsReturnsDefault::class); - expect($result->string)->toBe('default_value'); - expect($result->int)->toBe(42); - }); -}); - -describe('HTTP request handling', function () { - - beforeEach(function () { - \Illuminate\Support\Facades\Route::post('/test-validation-exception', function () { - PrimitiveData::fromArray([ - 'int' => 0, - 'float' => 3.14, - 'bool' => true, - ]); - return response()->json(['success' => true]); - }); - - \Illuminate\Support\Facades\Route::post('/test-http-response-exception', function () { - FailsWithHttpResponse::fromArray([ - 'int' => 0, - ]); - return response()->json(['success' => true]); - }); - - \Illuminate\Support\Facades\Route::post('/test-validation-exception-html', function () { - PrimitiveData::fromArray([ - 'int' => 0, - 'float' => 3.14, - 'bool' => true, - ]); - return response('success'); - }); - }); - - it('returns 422 with errors when ValidationException is thrown in JSON request', function () { - $response = $this->postJson('/test-validation-exception', [ - 'int' => 0, - 'float' => 3.14, - 'bool' => true, - ]); - - $response->assertStatus(422); - $response->assertJsonValidationErrors(['string']); - }); - - it('returns custom JSON response when HttpResponseException is thrown', function () { - $response = $this->postJson('/test-http-response-exception', [ - 'int' => 0, - ]); - - $response->assertStatus(422); - $response->assertJsonStructure(['errors']); - $response->assertJsonFragment([ - 'errors' => [ - 'string' => ['The string field is required.'], - ], - ]); - }); - - it('redirects back with session errors when ValidationException is thrown in text/html request', function () { - $response = $this->post('/test-validation-exception-html', [ - 'int' => 0, - 'float' => 3.14, - 'bool' => true, - ], [ - 'Accept' => 'text/html', - ]); - - $response->assertRedirect(); - $response->assertSessionHasErrors(['string']); - }); -}); diff --git a/tests/Logging/CustomLogger.php b/tests/Logging/CustomLogger.php deleted file mode 100644 index c4bff3d..0000000 --- a/tests/Logging/CustomLogger.php +++ /dev/null @@ -1,36 +0,0 @@ -logs[] = [ - 'level' => $level, - 'message' => $message, - 'context' => $context, - ]; - } - - public function hasLog(string $level, string $contains): bool - { - foreach ($this->logs as $log) { - if ($log['level'] === $level && str_contains($log['message'], $contains)) { - return true; - } - } - return false; - } - - public function clear(): void - { - $this->logs = []; - } -} diff --git a/tests/Logging/LogTest.php b/tests/Logging/LogTest.php deleted file mode 100644 index 6847938..0000000 --- a/tests/Logging/LogTest.php +++ /dev/null @@ -1,281 +0,0 @@ -set('dto.log.logger', NullLogger::class); - }); - - it('uses NullLogger as fallback when logger config is null', function () { - config()->set('dto.log.logger', null); - $log = new Log(); - expect($log->logger)->toBeInstanceOf(NullLogger::class); - }); - - it('uses NullLogger as fallback when logger config is invalid', function () { - config()->set('dto.log.logger', 'NonExistentLoggerClass'); - $log = new Log(); - expect($log->logger)->toBeInstanceOf(NullLogger::class); - }); - - it('instantiates logger from class name via Laravel container', function () { - config()->set('dto.log.logger', CustomLogger::class); - $log = new Log(); - expect($log->logger)->toBeInstanceOf(CustomLogger::class); - }); - - it('uses logger object directly when provided', function () { - $customLogger = new CustomLogger(); - config()->set('dto.log.logger', $customLogger); - $log = new Log(); - expect($log->logger)->toBe($customLogger); - }); - - it('invokes callable to get logger instance', function () { - config()->set('dto.log.logger', function () { - return new CustomLogger(); - }); - - $log = new Log(); - - expect($log->logger)->toBeInstanceOf(CustomLogger::class); - }); -}); - -describe('log level configuration', function () { - - beforeEach(function () { - $this->customLogger = new CustomLogger(); - config()->set('dto.log.logger', $this->customLogger); - }); - - afterEach(function () { - config()->set('dto.log.logger', NullLogger::class); - config()->set('dto.log.rules', LogLevel::DEBUG); - config()->set('dto.log.input', LogLevel::DEBUG); - config()->set('dto.log.raw_input', LogLevel::DEBUG); - config()->set('dto.log.validation_errors', LogLevel::INFO); - }); - - it('logs rules at configured level', function () { - config()->set('dto.log.rules', LogLevel::INFO); - - $log = new Log(); - $log->rules(['field' => ['required']]); - - expect($this->customLogger->hasLog(LogLevel::INFO, 'field'))->toBeTrue(); - }); - - it('logs input at configured level', function () { - config()->set('dto.log.input', LogLevel::INFO); - - $log = new Log(); - $log->input(['field' => 'value']); - - expect($this->customLogger->hasLog(LogLevel::INFO, 'value'))->toBeTrue(); - }); - - it('logs raw input at configured level', function () { - config()->set('dto.log.raw_input', LogLevel::ERROR); - - $log = new Log(); - $log->inputRaw(['field' => 'raw_value']); - - expect($this->customLogger->hasLog(LogLevel::ERROR, 'raw_value'))->toBeTrue(); - }); - - it('logs validation errors at configured level', function () { - config()->set('dto.log.validation_errors', LogLevel::ERROR); - - $log = new Log(); - $log->validationErrors(['field' => ['The field is required.']]); - - expect($this->customLogger->hasLog(LogLevel::ERROR, 'required'))->toBeTrue(); - }); - - it('allows different log levels for each log type', function () { - config()->set('dto.log.rules', LogLevel::DEBUG); - config()->set('dto.log.input', LogLevel::INFO); - config()->set('dto.log.raw_input', LogLevel::INFO); - - $log = new Log(); - - $log->rules(['rules_field' => ['required']]); - $log->input(['input_field' => 'value']); - $log->inputRaw(['raw_field' => 'raw_value']); - - expect($this->customLogger->hasLog(LogLevel::DEBUG, 'rules_field'))->toBeTrue(); - expect($this->customLogger->hasLog(LogLevel::INFO, 'input_field'))->toBeTrue(); - expect($this->customLogger->hasLog(LogLevel::INFO, 'raw_field'))->toBeTrue(); - }); - - it('defaults to DEBUG level when not configured', function () { - config()->set('dto.log.rules', null); - config()->set('dto.log.input', null); - config()->set('dto.log.raw_input', null); - - $customLogger = new CustomLogger(); - config()->set('dto.log.logger', $customLogger); - - $log = new Log(); - - $log->rules(['field' => ['required']]); - $log->input(['field' => 'value']); - $log->inputRaw(['field' => 'raw_value']); - - expect(count($customLogger->logs))->toBe(3); - expect($customLogger->logs[0]['level'])->toBe(LogLevel::DEBUG); - expect($customLogger->logs[1]['level'])->toBe(LogLevel::DEBUG); - expect($customLogger->logs[2]['level'])->toBe(LogLevel::DEBUG); - }); -}); - -describe('integration with DataObject', function () { - - beforeEach(function () { - $this->customLogger = new CustomLogger(); - config()->set('dto.log.logger', $this->customLogger); - config()->set('dto.log.rules', LogLevel::DEBUG); - config()->set('dto.log.input', LogLevel::DEBUG); - config()->set('dto.log.raw_input', LogLevel::DEBUG); - }); - - afterEach(function () { - config()->set('dto.log.logger', NullLogger::class); - }); - - it('logs raw input during fromArray execution', function () { - PrimitiveData::fromArray([ - 'string' => 'test', - 'int' => 42, - 'float' => 3.14, - 'bool' => true, - ]); - - expect($this->customLogger->hasLog(LogLevel::DEBUG, 'raw_input'))->toBeFalse(); - expect($this->customLogger->hasLog(LogLevel::DEBUG, 'string'))->toBeTrue(); - expect($this->customLogger->hasLog(LogLevel::DEBUG, '42'))->toBeTrue(); - }); - - it('logs rules during fromArray execution', function () { - PrimitiveData::fromArray([ - 'string' => 'test', - 'int' => 42, - 'float' => 3.14, - 'bool' => true, - ]); - - expect($this->customLogger->hasLog(LogLevel::DEBUG, 'required'))->toBeTrue(); - }); - - it('logs processed input during fromArray execution', function () { - PrimitiveData::fromArray([ - 'string' => 'test', - 'int' => 42, - 'float' => 3.14, - 'bool' => true, - ]); - - expect($this->customLogger->hasLog(LogLevel::DEBUG, 'test'))->toBeTrue(); - }); - - it('captures all three log types during successful fromArray', function () { - PrimitiveData::fromArray([ - 'string' => 'integration_test', - 'int' => 123, - 'float' => 9.99, - 'bool' => false, - ]); - - $rawInputLogged = false; - $rulesLogged = false; - $inputLogged = false; - - foreach ($this->customLogger->logs as $log) { - if (str_contains($log['message'], 'string')) { - $rawInputLogged = true; - } - if (str_contains($log['message'], 'required')) { - $rulesLogged = true; - } - if (str_contains($log['message'], 'integration_test')) { - $inputLogged = true; - } - } - - expect($rawInputLogged)->toBeTrue('Raw input should be logged'); - expect($rulesLogged)->toBeTrue('Rules should be logged'); - expect($inputLogged)->toBeTrue('Processed input should be logged'); - }); - - it('logs even when validation fails', function () { - try { - PrimitiveData::fromArray([ - 'int' => 42, - 'float' => 3.14, - 'bool' => true, - ]); - } catch (\Illuminate\Validation\ValidationException $e) { - // Expected - } - - expect($this->customLogger->hasLog(LogLevel::DEBUG, 'required'))->toBeTrue(); - expect($this->customLogger->hasLog(LogLevel::DEBUG, '42'))->toBeTrue(); - }); - - it('logs validation errors when validation fails', function () { - config()->set('dto.log.validation_errors', LogLevel::ERROR); - - try { - PrimitiveData::fromArray([ - 'int' => 42, - 'float' => 3.14, - 'bool' => true, - ]); - } catch (\Illuminate\Validation\ValidationException $e) { - // Expected - } - - expect($this->customLogger->hasLog(LogLevel::ERROR, 'string'))->toBeTrue(); - expect($this->customLogger->hasLog(LogLevel::ERROR, 'required'))->toBeTrue(); - }); -}); - -describe('logging with NullLogger', function () { - - it('does not throw when logging with NullLogger', function () { - config()->set('dto.log.logger', NullLogger::class); - - $log = new Log(); - - expect(function () use ($log) { - $log->rules(['field' => ['required']]); - $log->input(['field' => 'value']); - $log->inputRaw(['field' => 'raw_value']); - })->not->toThrow(\Throwable::class); - }); - - it('does not affect DataObject behavior when using NullLogger', function () { - config()->set('dto.log.logger', NullLogger::class); - - $object = PrimitiveData::fromArray([ - 'string' => 'test', - 'int' => 42, - 'float' => 3.14, - 'bool' => true, - ]); - - expect($object)->toBeInstanceOf(PrimitiveData::class); - expect($object->string)->toBe('test'); - }); -}); diff --git a/tests/RulesTest.php b/tests/Rules/RulesTest.php similarity index 99% rename from tests/RulesTest.php rename to tests/Rules/RulesTest.php index 34c48cf..1d722b5 100644 --- a/tests/RulesTest.php +++ b/tests/Rules/RulesTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Tests; +namespace Tests\Rules; use Icefox\DTO\Attributes\Flat; use Icefox\DTO\Attributes\Overwrite; diff --git a/tests/ValuesTest.php b/tests/Values/ValuesTest.php similarity index 97% rename from tests/ValuesTest.php rename to tests/Values/ValuesTest.php index 381900d..93eb02f 100644 --- a/tests/ValuesTest.php +++ b/tests/Values/ValuesTest.php @@ -2,14 +2,10 @@ declare(strict_types=1); -namespace Tests; +namespace Tests\Values; use Carbon\CarbonPeriod; use Icefox\DTO\Attributes\CastWith; -use Icefox\DTO\Attributes\Flat; -use Icefox\DTO\Attributes\FromInput; -use Icefox\DTO\Attributes\Overwrite; -use Icefox\DTO\RuleFactory; use Icefox\DTO\ValueFactory; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; diff --git a/workbench/config/dto.php b/workbench/config/dto.php index d212857..a06c16b 100644 --- a/workbench/config/dto.php +++ b/workbench/config/dto.php @@ -17,4 +17,7 @@ return [ 'internals' => LogLevel::DEBUG, ], ], + 'listTypes' => [ + Collection::class, + ], ];