diff --git a/composer.json b/composer.json index b415c1c..5f263ef 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,7 @@ { "name": "icefox/dto", "type": "library", + "version": "0.0.1", "require": { "laravel/framework": "^11.0", "psr/log": "^3.0", @@ -18,15 +19,12 @@ "license": "GPL-2.0-only", "autoload": { "psr-4": { - "Icefox\\DTO\\": "src/" + "Icefox\\Data\\": "src/" } }, "autoload-dev": { "psr-4": { - "Tests\\": "tests/", - "Workbench\\App\\": "workbench/app/", - "Workbench\\Database\\Factories\\": "workbench/database/factories/", - "Workbench\\Database\\Seeders\\": "workbench/database/seeders/" + "Tests\\": "tests/" } }, "authors": [ diff --git a/src/Attributes/CastWith.php b/src/Attributes/CastWith.php index 6d544c5..25c32cf 100644 --- a/src/Attributes/CastWith.php +++ b/src/Attributes/CastWith.php @@ -2,8 +2,7 @@ declare(strict_types=1); -namespace Icefox\DTO\Attributes; - +namespace Icefox\Data\Attributes; use Attribute; #[Attribute(Attribute::TARGET_PARAMETER)] diff --git a/src/Attributes/Flat.php b/src/Attributes/Flat.php index 27d73ad..2664003 100644 --- a/src/Attributes/Flat.php +++ b/src/Attributes/Flat.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Icefox\DTO\Attributes; +namespace Icefox\Data\Attributes; use Attribute; diff --git a/src/Attributes/FromInput.php b/src/Attributes/FromInput.php index 0a536bc..d43b697 100644 --- a/src/Attributes/FromInput.php +++ b/src/Attributes/FromInput.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Icefox\DTO\Attributes; +namespace Icefox\Data\Attributes; use Attribute; diff --git a/src/Attributes/FromRouteParameter.php b/src/Attributes/FromRouteParameter.php index 4eb11d1..d104e1c 100644 --- a/src/Attributes/FromRouteParameter.php +++ b/src/Attributes/FromRouteParameter.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Icefox\DTO\Attributes; +namespace Icefox\Data\Attributes; use Attribute; diff --git a/src/Attributes/Overwrite.php b/src/Attributes/Overwrite.php index 63a6453..a797cc6 100644 --- a/src/Attributes/Overwrite.php +++ b/src/Attributes/Overwrite.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Icefox\DTO\Attributes; +namespace Icefox\Data\Attributes; use Attribute; diff --git a/src/CustomHandlers.php b/src/CustomHandlers.php index f76b988..cf0eb15 100644 --- a/src/CustomHandlers.php +++ b/src/CustomHandlers.php @@ -1,8 +1,8 @@ make($class); @@ -104,13 +109,19 @@ class DataObjectFactory } 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 (class_exists($valueType)) { + $input[$parameterName] = $isListType + ? array_map( + fn($element) => self::mapInput($valueType, $element, $routeParameters, $logger), + $rawInput[$parameterName], + ) + : self::mapInput($valueType, $rawInput[$parameterName], $routeParameters, $logger); + continue; + } + if (array_key_exists($parameterName, $rawInput)) { + $input[$parameterName] = $rawInput[$parameterName]; + continue; + } } if ($reflectionType instanceof ReflectionNamedType) { diff --git a/src/Factories/RuleFactory.php b/src/Factories/RuleFactory.php index 4dc4acf..df1bdd7 100644 --- a/src/Factories/RuleFactory.php +++ b/src/Factories/RuleFactory.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace Icefox\DTO\Factories; +namespace Icefox\Data\Factories; -use Icefox\DTO\Attributes\Flat; -use Icefox\DTO\Attributes\Overwrite; -use Icefox\DTO\ParameterMeta; -use Icefox\DTO\ReflectionHelper; -use Icefox\DTO\Factories\RuleFactory; +use Icefox\Data\Attributes\Flat; +use Icefox\Data\Attributes\Overwrite; +use Icefox\Data\ParameterMeta; +use Icefox\Data\ReflectionHelper; +use Icefox\Data\Factories\RuleFactory; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Log; use Illuminate\Validation\Rule; diff --git a/src/Factories/ValueFactory.php b/src/Factories/ValueFactory.php index 5509c51..835d7ea 100644 --- a/src/Factories/ValueFactory.php +++ b/src/Factories/ValueFactory.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace Icefox\DTO\Factories; +namespace Icefox\Data\Factories; -use Icefox\DTO\Attributes\CastWith; -use Icefox\DTO\Attributes\Flat; -use Icefox\DTO\ReflectionHelper; +use Icefox\Data\Attributes\CastWith; +use Icefox\Data\Attributes\Flat; +use Icefox\Data\ReflectionHelper; use Illuminate\Support\Facades\App; use ReflectionNamedType; use phpDocumentor\Reflection\PseudoTypes\Generic; diff --git a/src/IData.php b/src/IData.php new file mode 100644 index 0000000..0f2e749 --- /dev/null +++ b/src/IData.php @@ -0,0 +1,5 @@ +publishes([ + __DIR__ . '../../workbench/config/dto.php' => config_path('dto.php'), + ]); + $this->app->beforeResolving(function ($abstract, $parameters, $app) { if ($app->has($abstract)) { return; } - if (is_subclass_of($abstract, IDataObject::class)) { + if (is_subclass_of($abstract, IData::class)) { $app->bind($abstract, fn($container) => DataObjectFactory::fromRequest($abstract, $container['request'])); } }); } } - diff --git a/src/ReflectionHelper.php b/src/ReflectionHelper.php index 47ebd39..fa64c03 100644 --- a/src/ReflectionHelper.php +++ b/src/ReflectionHelper.php @@ -1,6 +1,6 @@ toBe(123) ->and($result['address']['street'])->toBe('Main St'); }); + +readonly class SimpleWithDefaults +{ + public function __construct( + public string $name, + public int $age, + public string $city = 'Unknown', + ) {} + + /** + * @return array + */ + public static function defaults(): array + { + return [ + 'city' => 'New York', + 'age' => 25, + ]; + } +} + +test('defaults with fromArray - basic usage', function () { + $object = DataObjectFactory::fromArray( + SimpleWithDefaults::class, + ['name' => 'John'], + [], + ); + + expect($object->name)->toBe('John') + ->and($object->age)->toBe(25) + ->and($object->city)->toBe('New York'); +}); + +readonly class NestedWithDefaults +{ + public function __construct( + public string $title, + public SimpleWithDefaults $user, + ) {} + + /** + * @return array> + */ + public static function defaults(): array + { + return [ + 'user' => [ + 'name' => 'Default User', + 'age' => 30, + ], + ]; + } +} + +test('defaults with nested objects', function () { + $object = DataObjectFactory::fromArray( + NestedWithDefaults::class, + ['title' => 'Admin Dashboard'], + [], + ); + + expect($object->title)->toBe('Admin Dashboard') + ->and($object->user->name)->toBe('Default User') + ->and($object->user->age)->toBe(30) + ->and($object->user->city)->toBe('Unknown'); +}); + +test('defaults merged with input - input overrides defaults', function () { + $object = DataObjectFactory::fromArray( + SimpleWithDefaults::class, + ['name' => 'Alice', 'age' => 40, 'city' => 'Los Angeles'], + [], + ); + + expect($object->name)->toBe('Alice') + ->and($object->age)->toBe(40) + ->and($object->city)->toBe('Los Angeles'); +}); + + +test('defaults with simple nested structures', function () { + $object = DataObjectFactory::fromArray( + NestedWithDefaults::class, + ['title' => 'Simple Project'], + [], + ); + + expect($object->title)->toBe('Simple Project') + ->and($object->user->name)->toBe('Default User') + ->and($object->user->age)->toBe(30); +}); + +readonly class ArrayWithDefaults +{ + /** + * @param array $settings + */ + public function __construct( + public string $projectName, + public array $settings, + ) {} + /** + * @return array> + */ + public static function defaults(): array + { + return [ + 'settings' => [ + 'theme' => 'dark', + 'notifications' => true, + 'language' => 'en', + ], + ]; + } +} + +test('defaults with array structures', function () { + $object = DataObjectFactory::fromArray( + ArrayWithDefaults::class, + ['projectName' => 'New Project'], + [], + ); + + expect($object->projectName)->toBe('New Project') + ->and($object->settings)->toBe([ + 'theme' => 'dark', + 'notifications' => true, + 'language' => 'en', + ]); +}); + +test('defaults partially overridden by input', function () { + $object = DataObjectFactory::fromArray( + ArrayWithDefaults::class, + [ + 'projectName' => 'Custom Project', + 'settings' => [ + 'theme' => 'light', + 'language' => 'fr', + ], + ], + [], + ); + + expect($object->projectName)->toBe('Custom Project') + ->and($object->settings)->toBe([ + 'theme' => 'light', + 'notifications' => true, // From defaults + 'language' => 'fr', + ]); +}); + +readonly class WithInputMappingAndDefaults +{ + public function __construct( + #[FromInput('full_name')] + public string $name, + #[FromInput('user_age')] + public int $age, + public string $role = 'user', + ) {} + /** + * @return array + */ + public static function defaults(): array + { + return [ + 'name' => 'Default Name', + 'role' => 'admin', + 'age' => 18, + ]; + } +} + +test('defaults work with FromInput attribute mapping', function () { + $object = DataObjectFactory::fromArray( + WithInputMappingAndDefaults::class, + ['full_name' => 'John Doe'], + [], + ); + + expect($object->name)->toBe('John Doe') // From input mapping + ->and($object->age)->toBe(18) // From defaults + ->and($object->role)->toBe('admin'); // From defaults +}); + +test('array_merge_recursive behavior with nested arrays', function () { + $object = DataObjectFactory::fromArray( + ArrayWithDefaults::class, + [ + 'projectName' => 'Test Project', + 'settings' => [ + 'new_setting' => 'custom_value', + ], + ], + [], + ); + + expect($object->settings)->toBe([ + 'theme' => 'dark', // From defaults + 'notifications' => true, // From defaults + 'language' => 'en', // From defaults + 'new_setting' => 'custom_value', // From input + ]); +}); diff --git a/tests/Http/RequestTest.php b/tests/Http/RequestTest.php index c92e06d..d0713e3 100644 --- a/tests/Http/RequestTest.php +++ b/tests/Http/RequestTest.php @@ -2,13 +2,13 @@ namespace Tests\Http; -use Icefox\DTO\IDataObject; +use Icefox\Data\IData; use Illuminate\Http\Exceptions\HttpResponseException; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Validator as ValidatorFacade; use Illuminate\Validation\Validator; -readonly class Basic implements IDataObject +readonly class Basic implements IData { public string $reply; public function __construct(string $message) @@ -35,7 +35,7 @@ test('fails on validation error', function () { ]); }); -readonly class WithCustomValidator implements IDataObject +readonly class WithCustomValidator implements IData { public string $reply; public function __construct(string $message) @@ -66,7 +66,7 @@ test('replies with custom validator', function () { ]); }); -readonly class WithCustomFailure implements IDataObject +readonly class WithCustomFailure implements IData { public function __construct(public bool $flag) {} @@ -87,7 +87,7 @@ test('uses custom response', function () { ->assertJson(['result' => 'invalid, but that is ok']); }); -readonly class WithDefaultObjectOnFailure implements IDataObject +readonly class WithDefaultObjectOnFailure implements IData { public function __construct(public bool $flag) {} diff --git a/tests/Rules/RulesTest.php b/tests/Rules/RulesTest.php index 16ec1a7..f902d73 100644 --- a/tests/Rules/RulesTest.php +++ b/tests/Rules/RulesTest.php @@ -4,9 +4,9 @@ declare(strict_types=1); namespace Tests\Rules; -use Icefox\DTO\Attributes\Flat; -use Icefox\DTO\Attributes\Overwrite; -use Icefox\DTO\Factories\RuleFactory; +use Icefox\Data\Attributes\Flat; +use Icefox\Data\Attributes\Overwrite; +use Icefox\Data\Factories\RuleFactory; use Illuminate\Support\Collection; readonly class BasicPrimitives diff --git a/tests/Values/ValuesTest.php b/tests/Values/ValuesTest.php index 17cd822..e592d17 100644 --- a/tests/Values/ValuesTest.php +++ b/tests/Values/ValuesTest.php @@ -5,8 +5,8 @@ declare(strict_types=1); namespace Tests\Values; use Carbon\CarbonPeriod; -use Icefox\DTO\Attributes\CastWith; -use Icefox\DTO\Factories\ValueFactory; +use Icefox\Data\Attributes\CastWith; +use Icefox\Data\Factories\ValueFactory; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; diff --git a/workbench/config/dto.php b/workbench/config/dto.php index a06c16b..ecf8413 100644 --- a/workbench/config/dto.php +++ b/workbench/config/dto.php @@ -1,12 +1,12 @@ [ - Collection::class => CustomHandlers::CollectionRules(...), + Collection::class => CustomHandlers::class . "::CollectionRules", ], 'logging' => [ 'channel' => 'dto',