input mapping
This commit is contained in:
parent
bba10b455f
commit
fc46fe20ee
29 changed files with 193 additions and 887 deletions
|
|
@ -13,6 +13,8 @@ use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
use Illuminate\Validation\Validator;
|
use Illuminate\Validation\Validator;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
use ReflectionNamedType;
|
||||||
|
use phpDocumentor\Reflection\Types\AbstractList;
|
||||||
|
|
||||||
class DataObjectFactory
|
class DataObjectFactory
|
||||||
{
|
{
|
||||||
|
|
@ -21,7 +23,8 @@ class DataObjectFactory
|
||||||
*/
|
*/
|
||||||
public static function fromRequest(string $class, Request $request): ?object
|
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);
|
return static::fromArray($class, $request->input(), $routeParameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,7 +55,6 @@ class DataObjectFactory
|
||||||
return ValueFactory::make($class, $validator->validated());
|
return ValueFactory::make($class, $validator->validated());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param class-string $class
|
* @param class-string $class
|
||||||
* @param array<string,mixed> $rawInput
|
* @param array<string,mixed> $rawInput
|
||||||
|
|
@ -67,6 +69,7 @@ class DataObjectFactory
|
||||||
): array {
|
): array {
|
||||||
$input = [];
|
$input = [];
|
||||||
$parameters = ReflectionHelper::getParametersMeta($class);
|
$parameters = ReflectionHelper::getParametersMeta($class);
|
||||||
|
|
||||||
foreach ($parameters as $parameter) {
|
foreach ($parameters as $parameter) {
|
||||||
$parameterName = $parameter->reflection->getName();
|
$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) {
|
foreach ($parameter->reflection->getAttributes(FromInput::class) as $attr) {
|
||||||
if ($value = $rawInput[$attr->newInstance()->name] ?? null) {
|
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;
|
continue 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($value = $rawInput[$parameterName] ?? null) {
|
if ($valueType = ReflectionHelper::getListParameterValueType($parameter->tag)) {
|
||||||
$input[$parameterName] = $value;
|
$input[$parameterName] = $isListType
|
||||||
|
? array_map(
|
||||||
|
fn($element) => self::mapInput($valueType, $element, $routeParameters, $logger),
|
||||||
|
$rawInput[$parameterName],
|
||||||
|
)
|
||||||
|
: self::mapInput($valueType, $rawInput[$parameterName], $routeParameters, $logger);
|
||||||
continue;
|
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);
|
$logger->debug('input', $input);
|
||||||
return $input;
|
return $input;
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ namespace Icefox\DTO;
|
||||||
use ReflectionParameter;
|
use ReflectionParameter;
|
||||||
use phpDocumentor\Reflection\DocBlock\Tag;
|
use phpDocumentor\Reflection\DocBlock\Tag;
|
||||||
use phpDocumentor\Reflection\DocBlock\Tags\Param;
|
use phpDocumentor\Reflection\DocBlock\Tags\Param;
|
||||||
|
use phpDocumentor\Reflection\PseudoTypes\Generic;
|
||||||
|
use phpDocumentor\Reflection\Types\AbstractList;
|
||||||
use phpDocumentor\Reflection\Types\ContextFactory;
|
use phpDocumentor\Reflection\Types\ContextFactory;
|
||||||
use phpDocumentor\Reflection\DocBlockFactory;
|
use phpDocumentor\Reflection\DocBlockFactory;
|
||||||
use ReflectionClass;
|
use ReflectionClass;
|
||||||
|
|
@ -44,4 +46,21 @@ class ReflectionHelper
|
||||||
);
|
);
|
||||||
return self::$cache[$class];
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests;
|
|
||||||
|
|
||||||
use Illuminate\Validation\ValidationException;
|
|
||||||
use Tests\Casters\SimpleValue;
|
|
||||||
use Tests\Casters\SimpleValueCaster;
|
|
||||||
use Tests\Casters\WithGlobalCaster;
|
|
||||||
use Tests\Casters\WithSpecificCaster;
|
|
||||||
use Tests\Casters\WithoutCaster;
|
|
||||||
|
|
||||||
describe('caster priority', function () {
|
|
||||||
beforeEach(function () {
|
|
||||||
config(['dto.cast' => []]);
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Casters;
|
|
||||||
|
|
||||||
class SimpleValue
|
|
||||||
{
|
|
||||||
public function __construct(public readonly int $value) {}
|
|
||||||
}
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Casters;
|
|
||||||
|
|
||||||
class SimpleValueCaster
|
|
||||||
{
|
|
||||||
public function cast(mixed $data): SimpleValue
|
|
||||||
{
|
|
||||||
return new SimpleValue($data['value'] * 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'value' => ['required', 'numeric'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Casters;
|
|
||||||
|
|
||||||
use Icefox\DTO\DataObject;
|
|
||||||
|
|
||||||
readonly class WithGlobalCaster
|
|
||||||
{
|
|
||||||
use DataObject;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
public SimpleValue $simple,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Casters;
|
|
||||||
|
|
||||||
use Icefox\DTO\Attributes\CastWith;
|
|
||||||
use Icefox\DTO\DataObject;
|
|
||||||
|
|
||||||
readonly class WithSpecificCaster
|
|
||||||
{
|
|
||||||
use DataObject;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
#[CastWith(SimpleValueCaster::class)]
|
|
||||||
public SimpleValue $value,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Casters;
|
|
||||||
|
|
||||||
use Icefox\DTO\DataObject;
|
|
||||||
|
|
||||||
readonly class WithoutCaster
|
|
||||||
{
|
|
||||||
use DataObject;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
public SimpleValue $value,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Classes;
|
|
||||||
|
|
||||||
use Icefox\DTO\DataObject;
|
|
||||||
|
|
||||||
readonly class ArrayDataObject
|
|
||||||
{
|
|
||||||
use DataObject;
|
|
||||||
/**
|
|
||||||
* @param array<int,int> $values
|
|
||||||
*/
|
|
||||||
public function __construct(public array $values) {}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\Classes;
|
|
||||||
|
|
||||||
use Carbon\CarbonPeriodImmutable;
|
|
||||||
use Illuminate\Support\Carbon;
|
|
||||||
|
|
||||||
class CarbonPeriodMapper
|
|
||||||
{
|
|
||||||
public function cast(mixed $data): CarbonPeriodImmutable
|
|
||||||
{
|
|
||||||
return new CarbonPeriodImmutable(Carbon::parse($data['start']), Carbon::parse($data['end']));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function rules(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'start' => ['required', 'date'],
|
|
||||||
'end' => ['required', 'date'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Classes;
|
|
||||||
|
|
||||||
use Icefox\DTO\DataObject;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
|
|
||||||
readonly class CollectionDataObject
|
|
||||||
{
|
|
||||||
use DataObject;
|
|
||||||
/**
|
|
||||||
* @param Collection<OptionalNullableData> $values
|
|
||||||
*/
|
|
||||||
public function __construct(public Collection $values) {}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Classes;
|
|
||||||
|
|
||||||
use Icefox\DTO\DataObject;
|
|
||||||
use Illuminate\Validation\Validator;
|
|
||||||
|
|
||||||
readonly class FailsReturnsDefault
|
|
||||||
{
|
|
||||||
use DataObject;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
public string $string,
|
|
||||||
public int $int = 42,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public static function fails(Validator $validator): ?static
|
|
||||||
{
|
|
||||||
return new self(string: 'default_value');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Classes;
|
|
||||||
|
|
||||||
use Icefox\DTO\DataObject;
|
|
||||||
use Illuminate\Validation\Validator;
|
|
||||||
|
|
||||||
readonly class FailsReturnsNull
|
|
||||||
{
|
|
||||||
use DataObject;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
public string $string,
|
|
||||||
public int $int,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public static function fails(Validator $validator): ?static
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Classes;
|
|
||||||
|
|
||||||
use Icefox\DTO\DataObject;
|
|
||||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
|
||||||
use Illuminate\Validation\Validator;
|
|
||||||
|
|
||||||
readonly class FailsWithHttpResponse
|
|
||||||
{
|
|
||||||
use DataObject;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
public string $string,
|
|
||||||
public int $int,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
public static function fails(Validator $validator): ?static
|
|
||||||
{
|
|
||||||
throw new HttpResponseException(
|
|
||||||
response()->json(['errors' => $validator->errors()], 422)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Classes;
|
|
||||||
|
|
||||||
use Icefox\DTO\Attributes\FromInput;
|
|
||||||
use Icefox\DTO\DataObject;
|
|
||||||
|
|
||||||
readonly class FromInputObject
|
|
||||||
{
|
|
||||||
use DataObject;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
#[FromInput('other_name')]
|
|
||||||
public string $text,
|
|
||||||
public int $standard,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Classes;
|
|
||||||
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Icefox\DTO\DataObject;
|
|
||||||
|
|
||||||
readonly class ObjectWithoutMapper
|
|
||||||
{
|
|
||||||
use DataObject;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
public Carbon $date,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Classes;
|
|
||||||
|
|
||||||
use Icefox\DTO\DataObject;
|
|
||||||
|
|
||||||
readonly class OptionalData
|
|
||||||
{
|
|
||||||
use DataObject;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
public string $string = 'xyz',
|
|
||||||
public float $float = 0.777,
|
|
||||||
public int $int = 3,
|
|
||||||
public bool $bool = false,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Classes;
|
|
||||||
|
|
||||||
use Icefox\DTO\DataObject;
|
|
||||||
|
|
||||||
readonly class OptionalNullableData
|
|
||||||
{
|
|
||||||
use DataObject;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
public string $string,
|
|
||||||
public ?int $int,
|
|
||||||
public float $float = 0.999,
|
|
||||||
public bool $bool = false,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Classes;
|
|
||||||
|
|
||||||
use Icefox\DTO\DataObject;
|
|
||||||
|
|
||||||
readonly class PrimitiveData
|
|
||||||
{
|
|
||||||
use DataObject;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
public string $string,
|
|
||||||
public int $int,
|
|
||||||
public float $float,
|
|
||||||
public bool $bool,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Classes;
|
|
||||||
|
|
||||||
use Icefox\DTO\DataObject;
|
|
||||||
|
|
||||||
readonly class RecursiveDataObject
|
|
||||||
{
|
|
||||||
use DataObject;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
public string $string,
|
|
||||||
public PrimitiveData $extra,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Classes;
|
|
||||||
|
|
||||||
use Carbon\CarbonPeriodImmutable;
|
|
||||||
use Icefox\DTO\Attributes\CastWith;
|
|
||||||
use Icefox\DTO\DataObject;
|
|
||||||
|
|
||||||
readonly class WithMapperObject
|
|
||||||
{
|
|
||||||
use DataObject;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
#[CastWith(CarbonPeriodMapper::class)]
|
|
||||||
public CarbonPeriodImmutable $period,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
127
tests/DataObject/DataObjectTest.php
Normal file
127
tests/DataObject/DataObjectTest.php
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\DataObject;
|
||||||
|
|
||||||
|
use Icefox\DTO\Attributes\FromInput;
|
||||||
|
use Icefox\DTO\DataObjectFactory;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Psr\Log\NullLogger;
|
||||||
|
|
||||||
|
readonly class Element
|
||||||
|
{
|
||||||
|
public function __construct(public int $value) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly class Node
|
||||||
|
{
|
||||||
|
public function __construct(public Element $element) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('basic nested object', function () {
|
||||||
|
$input = DataObjectFactory::mapInput(Node::class, ['element' => ['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<int, MappedCollectionItem> $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<int, MappedCollectionItem> $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<int,AnnotatedArrayItem> $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]]]);
|
||||||
|
});
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests;
|
|
||||||
|
|
||||||
use Icefox\DTO\Attributes\FromInput;
|
|
||||||
use Icefox\DTO\DataObjectFactory;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Psr\Log\NullLogger;
|
|
||||||
|
|
||||||
readonly class MappedCollectionItem
|
|
||||||
{
|
|
||||||
public function __construct(#[FromInput('id_item')] public int $idItem) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
readonly class MappedCollectionRoot
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param Collection<int, MappedCollectionItem> $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 ],
|
|
||||||
],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
namespace Tests\FailedValidation;
|
|
||||||
|
|
||||||
use Illuminate\Validation\ValidationException;
|
|
||||||
use Tests\Classes\FailsReturnsDefault;
|
|
||||||
use Tests\Classes\FailsReturnsNull;
|
|
||||||
use Tests\Classes\FailsWithHttpResponse;
|
|
||||||
use Tests\Classes\PrimitiveData;
|
|
||||||
|
|
||||||
describe('fails method behavior', function () {
|
|
||||||
|
|
||||||
it('throws ValidationException when class does not implement fails()', function () {
|
|
||||||
expect(function () {
|
|
||||||
PrimitiveData::fromArray([
|
|
||||||
'int' => 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']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Logging;
|
|
||||||
|
|
||||||
use Psr\Log\AbstractLogger;
|
|
||||||
|
|
||||||
class CustomLogger extends AbstractLogger
|
|
||||||
{
|
|
||||||
public array $logs = [];
|
|
||||||
|
|
||||||
public function log($level, string|\Stringable $message, array $context = []): void
|
|
||||||
{
|
|
||||||
$this->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 = [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,281 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Tests\Logging;
|
|
||||||
|
|
||||||
use Icefox\DTO\Log;
|
|
||||||
use Psr\Log\LogLevel;
|
|
||||||
use Psr\Log\NullLogger;
|
|
||||||
use Tests\Classes\PrimitiveData;
|
|
||||||
use Tests\TestCase;
|
|
||||||
|
|
||||||
describe('logger resolution', function () {
|
|
||||||
|
|
||||||
afterEach(function () {
|
|
||||||
config()->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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Tests;
|
namespace Tests\Rules;
|
||||||
|
|
||||||
use Icefox\DTO\Attributes\Flat;
|
use Icefox\DTO\Attributes\Flat;
|
||||||
use Icefox\DTO\Attributes\Overwrite;
|
use Icefox\DTO\Attributes\Overwrite;
|
||||||
|
|
@ -2,14 +2,10 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Tests;
|
namespace Tests\Values;
|
||||||
|
|
||||||
use Carbon\CarbonPeriod;
|
use Carbon\CarbonPeriod;
|
||||||
use Icefox\DTO\Attributes\CastWith;
|
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 Icefox\DTO\ValueFactory;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
@ -17,4 +17,7 @@ return [
|
||||||
'internals' => LogLevel::DEBUG,
|
'internals' => LogLevel::DEBUG,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
'listTypes' => [
|
||||||
|
Collection::class,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue