From 4ee6171055d6855ee50998bc2b714c2518c4313f Mon Sep 17 00:00:00 2001 From: Nikola Svetozarevic Date: Thu, 10 Oct 2024 17:15:55 +0200 Subject: [PATCH 01/21] Point to the right problem when `ArgumentCountError` exception is thrown in the constructor --- src/Exceptions/CannotCreateData.php | 3 +-- src/Resolvers/DataFromArrayResolver.php | 17 +++++++++++++---- tests/CreationTest.php | 13 +++++++++++++ .../DataWithArgumentCountErrorException.php | 15 +++++++++++++++ 4 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 tests/Fakes/DataWithArgumentCountErrorException.php diff --git a/src/Exceptions/CannotCreateData.php b/src/Exceptions/CannotCreateData.php index 11a2d2c05..b867f224d 100644 --- a/src/Exceptions/CannotCreateData.php +++ b/src/Exceptions/CannotCreateData.php @@ -24,7 +24,6 @@ public static function noNormalizerFound(string $dataClass, mixed $value): self public static function constructorMissingParameters( DataClass $dataClass, array $parameters, - Throwable $previous, ): self { $parameters = collect($parameters); @@ -42,6 +41,6 @@ public static function constructorMissingParameters( ->map(fn (DataProperty|DataParameter $parameter) => $parameter->name) ->join(', ')}."; - return new self($message, previous: $previous); + return new self($message); } } diff --git a/src/Resolvers/DataFromArrayResolver.php b/src/Resolvers/DataFromArrayResolver.php index 825ded0c6..2427e9ec5 100644 --- a/src/Resolvers/DataFromArrayResolver.php +++ b/src/Resolvers/DataFromArrayResolver.php @@ -91,14 +91,23 @@ protected function createData( } } - try { - return new $dataClass->name(...$parameters); - } catch (ArgumentCountError $error) { + if ($this->isAnyParameterMissing($dataClass, array_keys($parameters))) { throw CannotCreateData::constructorMissingParameters( $dataClass, $parameters, - $error ); } + + return new $dataClass->name(...$parameters); + } + + protected function isAnyParameterMissing(DataClass $dataClass, array $parameters): bool + { + return $dataClass + ->constructorMethod + ->parameters + ->pluck('name') + ->diff($parameters) + ->isNotEmpty(); } } diff --git a/tests/CreationTest.php b/tests/CreationTest.php index d8bc3c559..39b6ace9f 100644 --- a/tests/CreationTest.php +++ b/tests/CreationTest.php @@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Route; use Illuminate\Validation\ValidationException; +use Spatie\LaravelData\Tests\Fakes\DataWithArgumentCountErrorException; use function Pest\Laravel\postJson; use Spatie\LaravelData\Attributes\Computed; @@ -751,6 +752,18 @@ public function __construct( yield 'one param' => [['first' => 'First'], 'Could not create `Spatie\LaravelData\Tests\Fakes\MultiData`: the constructor requires 2 parameters, 1 given. Parameters given: first. Parameters missing: second.'], ]); +it('throws a readable exception message when the ArgumentCountError exception is thrown in the constructor', function () { + try { + DataWithArgumentCountErrorException::from(['string' => 'string']); + } catch (ArgumentCountError $e) { + expect($e->getMessage())->toBe('This function expects exactly 2 arguments, 1 given.'); + expect($e->getFile())->toContain('/tests/Fakes/DataWithArgumentCountErrorException.php'); + expect($e->getLine())->toBe(13); + + return; + } +}); + it('throws a readable exception message when the constructor of a nested data object fails', function () { expect(fn () => NestedData::from([ 'simple' => [], diff --git a/tests/Fakes/DataWithArgumentCountErrorException.php b/tests/Fakes/DataWithArgumentCountErrorException.php new file mode 100644 index 000000000..fe02e8195 --- /dev/null +++ b/tests/Fakes/DataWithArgumentCountErrorException.php @@ -0,0 +1,15 @@ + Date: Fri, 25 Oct 2024 13:29:59 +0200 Subject: [PATCH 02/21] Add optional parameter to the test case --- tests/CreationTest.php | 2 +- tests/Fakes/DataWithArgumentCountErrorException.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/CreationTest.php b/tests/CreationTest.php index 39b6ace9f..9768577ed 100644 --- a/tests/CreationTest.php +++ b/tests/CreationTest.php @@ -758,7 +758,7 @@ public function __construct( } catch (ArgumentCountError $e) { expect($e->getMessage())->toBe('This function expects exactly 2 arguments, 1 given.'); expect($e->getFile())->toContain('/tests/Fakes/DataWithArgumentCountErrorException.php'); - expect($e->getLine())->toBe(13); + expect($e->getLine())->toBe(14); return; } diff --git a/tests/Fakes/DataWithArgumentCountErrorException.php b/tests/Fakes/DataWithArgumentCountErrorException.php index fe02e8195..5cd8fdd0d 100644 --- a/tests/Fakes/DataWithArgumentCountErrorException.php +++ b/tests/Fakes/DataWithArgumentCountErrorException.php @@ -9,6 +9,7 @@ class DataWithArgumentCountErrorException extends Data { public function __construct( public string $string, + public string $optional = 'default', ) { throw new ArgumentCountError('This function expects exactly 2 arguments, 1 given.'); } From 3021fe8a0fbee5dadc6a294b76d86004b30ea076 Mon Sep 17 00:00:00 2001 From: Nikola Svetozarevic Date: Fri, 25 Oct 2024 16:19:26 +0200 Subject: [PATCH 03/21] Switch approach to checking the exception trance --- src/Exceptions/CannotCreateData.php | 3 ++- src/Resolvers/DataFromArrayResolver.php | 26 ++++++++++--------- tests/CreationTest.php | 4 +-- .../DataWithArgumentCountErrorException.php | 4 ++- 4 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/Exceptions/CannotCreateData.php b/src/Exceptions/CannotCreateData.php index b867f224d..11a2d2c05 100644 --- a/src/Exceptions/CannotCreateData.php +++ b/src/Exceptions/CannotCreateData.php @@ -24,6 +24,7 @@ public static function noNormalizerFound(string $dataClass, mixed $value): self public static function constructorMissingParameters( DataClass $dataClass, array $parameters, + Throwable $previous, ): self { $parameters = collect($parameters); @@ -41,6 +42,6 @@ public static function constructorMissingParameters( ->map(fn (DataProperty|DataParameter $parameter) => $parameter->name) ->join(', ')}."; - return new self($message); + return new self($message, previous: $previous); } } diff --git a/src/Resolvers/DataFromArrayResolver.php b/src/Resolvers/DataFromArrayResolver.php index 2427e9ec5..eddbdcfad 100644 --- a/src/Resolvers/DataFromArrayResolver.php +++ b/src/Resolvers/DataFromArrayResolver.php @@ -91,23 +91,25 @@ protected function createData( } } - if ($this->isAnyParameterMissing($dataClass, array_keys($parameters))) { - throw CannotCreateData::constructorMissingParameters( - $dataClass, - $parameters, - ); + try { + return new $dataClass->name(...$parameters); + } catch (ArgumentCountError $error) { + if ($this->isExceptionCausedByDataClass($error, $dataClass)) { + throw CannotCreateData::constructorMissingParameters( + $dataClass, + $parameters, + $error, + ); + } else { + throw $error; + } } return new $dataClass->name(...$parameters); } - protected function isAnyParameterMissing(DataClass $dataClass, array $parameters): bool + protected function isExceptionCausedByDataClass(ArgumentCountError $error, DataClass $dataClass): bool { - return $dataClass - ->constructorMethod - ->parameters - ->pluck('name') - ->diff($parameters) - ->isNotEmpty(); + return str_contains($error->getMessage(), sprintf('Too few arguments to function %s::__construct', $dataClass->name)); } } diff --git a/tests/CreationTest.php b/tests/CreationTest.php index 9768577ed..df442bf4d 100644 --- a/tests/CreationTest.php +++ b/tests/CreationTest.php @@ -11,7 +11,6 @@ use Illuminate\Support\Facades\Route; use Illuminate\Validation\ValidationException; -use Spatie\LaravelData\Tests\Fakes\DataWithArgumentCountErrorException; use function Pest\Laravel\postJson; use Spatie\LaravelData\Attributes\Computed; @@ -45,6 +44,7 @@ use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomCursorPaginatedDataCollection; use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomDataCollection; use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomPaginatedDataCollection; +use Spatie\LaravelData\Tests\Fakes\DataWithArgumentCountErrorException; use Spatie\LaravelData\Tests\Fakes\EnumData; use Spatie\LaravelData\Tests\Fakes\Enums\DummyBackedEnum; use Spatie\LaravelData\Tests\Fakes\ModelData; @@ -758,7 +758,7 @@ public function __construct( } catch (ArgumentCountError $e) { expect($e->getMessage())->toBe('This function expects exactly 2 arguments, 1 given.'); expect($e->getFile())->toContain('/tests/Fakes/DataWithArgumentCountErrorException.php'); - expect($e->getLine())->toBe(14); + expect($e->getLine())->toBe(17); return; } diff --git a/tests/Fakes/DataWithArgumentCountErrorException.php b/tests/Fakes/DataWithArgumentCountErrorException.php index 5cd8fdd0d..654da0e1c 100644 --- a/tests/Fakes/DataWithArgumentCountErrorException.php +++ b/tests/Fakes/DataWithArgumentCountErrorException.php @@ -9,7 +9,9 @@ class DataWithArgumentCountErrorException extends Data { public function __construct( public string $string, - public string $optional = 'default', + public string $promotedOptional = 'default', + private string $privatePromotedOptional = 'optional', + string $optional = 'test', ) { throw new ArgumentCountError('This function expects exactly 2 arguments, 1 given.'); } From 6c2bc2ba4506a0e15139f4dc7f6f2310592767eb Mon Sep 17 00:00:00 2001 From: Nikola Svetozarevic Date: Fri, 25 Oct 2024 16:20:57 +0200 Subject: [PATCH 04/21] Remove duplicate code --- src/Resolvers/DataFromArrayResolver.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Resolvers/DataFromArrayResolver.php b/src/Resolvers/DataFromArrayResolver.php index eddbdcfad..ae1d71cc6 100644 --- a/src/Resolvers/DataFromArrayResolver.php +++ b/src/Resolvers/DataFromArrayResolver.php @@ -104,8 +104,6 @@ protected function createData( throw $error; } } - - return new $dataClass->name(...$parameters); } protected function isExceptionCausedByDataClass(ArgumentCountError $error, DataClass $dataClass): bool From 4302957473659e18e942227fe85df770d43ca518 Mon Sep 17 00:00:00 2001 From: Nikola Svetozarevic Date: Thu, 31 Oct 2024 13:26:56 +0100 Subject: [PATCH 05/21] Revert "Switch approach to checking the exception trance" This reverts commit 3021fe8a0fbee5dadc6a294b76d86004b30ea076. --- src/Exceptions/CannotCreateData.php | 3 +-- src/Resolvers/DataFromArrayResolver.php | 26 +++++++++---------- tests/CreationTest.php | 4 +-- .../DataWithArgumentCountErrorException.php | 4 +-- 4 files changed, 16 insertions(+), 21 deletions(-) diff --git a/src/Exceptions/CannotCreateData.php b/src/Exceptions/CannotCreateData.php index 11a2d2c05..b867f224d 100644 --- a/src/Exceptions/CannotCreateData.php +++ b/src/Exceptions/CannotCreateData.php @@ -24,7 +24,6 @@ public static function noNormalizerFound(string $dataClass, mixed $value): self public static function constructorMissingParameters( DataClass $dataClass, array $parameters, - Throwable $previous, ): self { $parameters = collect($parameters); @@ -42,6 +41,6 @@ public static function constructorMissingParameters( ->map(fn (DataProperty|DataParameter $parameter) => $parameter->name) ->join(', ')}."; - return new self($message, previous: $previous); + return new self($message); } } diff --git a/src/Resolvers/DataFromArrayResolver.php b/src/Resolvers/DataFromArrayResolver.php index ae1d71cc6..f464318bc 100644 --- a/src/Resolvers/DataFromArrayResolver.php +++ b/src/Resolvers/DataFromArrayResolver.php @@ -91,23 +91,21 @@ protected function createData( } } - try { - return new $dataClass->name(...$parameters); - } catch (ArgumentCountError $error) { - if ($this->isExceptionCausedByDataClass($error, $dataClass)) { - throw CannotCreateData::constructorMissingParameters( - $dataClass, - $parameters, - $error, - ); - } else { - throw $error; - } + if ($this->isAnyParameterMissing($dataClass, array_keys($parameters))) { + throw CannotCreateData::constructorMissingParameters( + $dataClass, + $parameters, + ); } } - protected function isExceptionCausedByDataClass(ArgumentCountError $error, DataClass $dataClass): bool + protected function isAnyParameterMissing(DataClass $dataClass, array $parameters): bool { - return str_contains($error->getMessage(), sprintf('Too few arguments to function %s::__construct', $dataClass->name)); + return $dataClass + ->constructorMethod + ->parameters + ->pluck('name') + ->diff($parameters) + ->isNotEmpty(); } } diff --git a/tests/CreationTest.php b/tests/CreationTest.php index df442bf4d..9768577ed 100644 --- a/tests/CreationTest.php +++ b/tests/CreationTest.php @@ -11,6 +11,7 @@ use Illuminate\Support\Facades\Route; use Illuminate\Validation\ValidationException; +use Spatie\LaravelData\Tests\Fakes\DataWithArgumentCountErrorException; use function Pest\Laravel\postJson; use Spatie\LaravelData\Attributes\Computed; @@ -44,7 +45,6 @@ use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomCursorPaginatedDataCollection; use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomDataCollection; use Spatie\LaravelData\Tests\Fakes\DataCollections\CustomPaginatedDataCollection; -use Spatie\LaravelData\Tests\Fakes\DataWithArgumentCountErrorException; use Spatie\LaravelData\Tests\Fakes\EnumData; use Spatie\LaravelData\Tests\Fakes\Enums\DummyBackedEnum; use Spatie\LaravelData\Tests\Fakes\ModelData; @@ -758,7 +758,7 @@ public function __construct( } catch (ArgumentCountError $e) { expect($e->getMessage())->toBe('This function expects exactly 2 arguments, 1 given.'); expect($e->getFile())->toContain('/tests/Fakes/DataWithArgumentCountErrorException.php'); - expect($e->getLine())->toBe(17); + expect($e->getLine())->toBe(14); return; } diff --git a/tests/Fakes/DataWithArgumentCountErrorException.php b/tests/Fakes/DataWithArgumentCountErrorException.php index 654da0e1c..5cd8fdd0d 100644 --- a/tests/Fakes/DataWithArgumentCountErrorException.php +++ b/tests/Fakes/DataWithArgumentCountErrorException.php @@ -9,9 +9,7 @@ class DataWithArgumentCountErrorException extends Data { public function __construct( public string $string, - public string $promotedOptional = 'default', - private string $privatePromotedOptional = 'optional', - string $optional = 'test', + public string $optional = 'default', ) { throw new ArgumentCountError('This function expects exactly 2 arguments, 1 given.'); } From cb03eb6f108d754e1119ba8134be4b0f06d378f5 Mon Sep 17 00:00:00 2001 From: Nikola Svetozarevic Date: Thu, 31 Oct 2024 13:30:56 +0100 Subject: [PATCH 06/21] Filter parameters with default value --- src/Resolvers/DataFromArrayResolver.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Resolvers/DataFromArrayResolver.php b/src/Resolvers/DataFromArrayResolver.php index f464318bc..0a241ff45 100644 --- a/src/Resolvers/DataFromArrayResolver.php +++ b/src/Resolvers/DataFromArrayResolver.php @@ -9,6 +9,8 @@ use Spatie\LaravelData\Optional; use Spatie\LaravelData\Support\DataClass; use Spatie\LaravelData\Support\DataConfig; +use Spatie\LaravelData\Support\DataParameter; +use Spatie\LaravelData\Support\DataProperty; /** * @template TData of BaseData @@ -97,6 +99,8 @@ protected function createData( $parameters, ); } + + return new $dataClass->name(...$parameters); } protected function isAnyParameterMissing(DataClass $dataClass, array $parameters): bool @@ -104,6 +108,7 @@ protected function isAnyParameterMissing(DataClass $dataClass, array $parameters return $dataClass ->constructorMethod ->parameters + ->filter(fn (DataParameter|DataProperty $parameter) => ! $parameter->hasDefaultValue) ->pluck('name') ->diff($parameters) ->isNotEmpty(); From 66d580c8abd987ade18a8803ffff01f6b888b114 Mon Sep 17 00:00:00 2001 From: Nikola Svetozarevic Date: Fri, 13 Dec 2024 17:33:01 +0100 Subject: [PATCH 07/21] Apply feedback changes --- src/Exceptions/CannotCreateData.php | 3 ++- src/Resolvers/DataFromArrayResolver.php | 19 ++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/Exceptions/CannotCreateData.php b/src/Exceptions/CannotCreateData.php index b867f224d..11a2d2c05 100644 --- a/src/Exceptions/CannotCreateData.php +++ b/src/Exceptions/CannotCreateData.php @@ -24,6 +24,7 @@ public static function noNormalizerFound(string $dataClass, mixed $value): self public static function constructorMissingParameters( DataClass $dataClass, array $parameters, + Throwable $previous, ): self { $parameters = collect($parameters); @@ -41,6 +42,6 @@ public static function constructorMissingParameters( ->map(fn (DataProperty|DataParameter $parameter) => $parameter->name) ->join(', ')}."; - return new self($message); + return new self($message, previous: $previous); } } diff --git a/src/Resolvers/DataFromArrayResolver.php b/src/Resolvers/DataFromArrayResolver.php index 0a241ff45..bf7edc55d 100644 --- a/src/Resolvers/DataFromArrayResolver.php +++ b/src/Resolvers/DataFromArrayResolver.php @@ -93,14 +93,19 @@ protected function createData( } } - if ($this->isAnyParameterMissing($dataClass, array_keys($parameters))) { - throw CannotCreateData::constructorMissingParameters( - $dataClass, - $parameters, - ); + try { + return new $dataClass->name(...$parameters); + } catch (ArgumentCountError $error) { + if ($this->isAnyParameterMissing($dataClass, array_keys($parameters))) { + throw CannotCreateData::constructorMissingParameters( + $dataClass, + $parameters, + $error + ); + } else { + throw $error; + } } - - return new $dataClass->name(...$parameters); } protected function isAnyParameterMissing(DataClass $dataClass, array $parameters): bool From da4125553d05241a98eacab730f130c6961cbca7 Mon Sep 17 00:00:00 2001 From: Enzo Innocenzi Date: Tue, 22 Oct 2024 04:19:53 -0700 Subject: [PATCH 08/21] feat: support "null to optional" (#881) * feat: support "null to optional" * chore: bump minimum version of `spatie/laravel-typescript-transformer` --- composer.json | 2 +- .../DataTypeScriptTransformer.php | 4 +++- .../DataTypeScriptTransformerTest.php | 18 ++++++++++++++++++ ...ullable_types_to_optional_properties__1.txt | 3 +++ 4 files changed, 25 insertions(+), 2 deletions(-) create mode 100644 tests/__snapshots__/DataTypeScriptTransformerTest__it_supports_converting_nullable_types_to_optional_properties__1.txt diff --git a/composer.json b/composer.json index 9ec72d609..c641c0c5c 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,7 @@ "phpstan/extension-installer": "^1.1", "phpunit/phpunit": "^10.0", "spatie/invade": "^1.0", - "spatie/laravel-typescript-transformer": "^2.3", + "spatie/laravel-typescript-transformer": "^2.5", "spatie/pest-plugin-snapshots": "^2.1", "spatie/test-time": "^1.2" }, diff --git a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php index 81bf5ce89..66cdff010 100644 --- a/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php +++ b/src/Support/TypeScriptTransformer/DataTypeScriptTransformer.php @@ -80,11 +80,13 @@ function (string $carry, ReflectionProperty $property) use ($isOptional, $dataCl fn (object $attribute) => $attribute instanceof TypeScriptOptional ) || ($dataProperty->type->lazyType && $dataProperty->type->lazyType !== ClosureLazy::class) - || $dataProperty->type->isOptional; + || $dataProperty->type->isOptional + || ($dataProperty->type->isNullable && $this->config->shouldConsiderNullAsOptional()); $transformed = $this->typeToTypeScript( $type, $missingSymbols, + $this->config->shouldConsiderNullAsOptional(), currentClass: $property->getDeclaringClass()->getName(), ); diff --git a/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php b/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php index 8988300ce..7515401a2 100644 --- a/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php +++ b/tests/Support/TypeScriptTransformer/DataTypeScriptTransformerTest.php @@ -322,3 +322,21 @@ public function __construct( $transformer->transform($reflection, 'DataObject')->transformed ); }); + +it('supports converting nullable types to optional properties', function () { + $config = TypeScriptTransformerConfig::create(); + $config->nullToOptional(true); + + $data = new class ('foo') extends Data { + public function __construct( + public ?string $nullable, + ) { + } + }; + + $transformer = new DataTypeScriptTransformer($config); + $reflection = new ReflectionClass($data); + + expect($transformer->canTransform($reflection))->toBeTrue(); + assertMatchesSnapshot($transformer->transform($reflection, 'DataObject')->transformed); +}); diff --git a/tests/__snapshots__/DataTypeScriptTransformerTest__it_supports_converting_nullable_types_to_optional_properties__1.txt b/tests/__snapshots__/DataTypeScriptTransformerTest__it_supports_converting_nullable_types_to_optional_properties__1.txt new file mode 100644 index 000000000..c18ab6613 --- /dev/null +++ b/tests/__snapshots__/DataTypeScriptTransformerTest__it_supports_converting_nullable_types_to_optional_properties__1.txt @@ -0,0 +1,3 @@ +{ +nullable?: string; +} \ No newline at end of file From 7a136d5b7e45537ee8ec543200344bd6ba9389e6 Mon Sep 17 00:00:00 2001 From: Erik Gaal Date: Tue, 22 Oct 2024 13:20:40 +0200 Subject: [PATCH 09/21] Register optimize commands (#880) --- src/LaravelDataServiceProvider.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/LaravelDataServiceProvider.php b/src/LaravelDataServiceProvider.php index 774633699..d46ce15ec 100644 --- a/src/LaravelDataServiceProvider.php +++ b/src/LaravelDataServiceProvider.php @@ -79,5 +79,12 @@ public function packageBooted(): void if ($enableVarDumperCaster) { (new VarDumperManager())->initialize(); } + + if (method_exists($this, 'optimizes')) { + $this->optimizes( + optimize: 'data:cache-structures', + key: 'laravel-data', + ); + } } } From a790c3846b70c059237f02990df11dbce258e7d7 Mon Sep 17 00:00:00 2001 From: riasvdv Date: Tue, 22 Oct 2024 11:21:35 +0000 Subject: [PATCH 10/21] Update CHANGELOG --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f610134ec..fe8871507 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to `laravel-data` will be documented in this file. +## 4.11.0 - 2024-10-22 + +### What's Changed + +* feat: support "null to optional" by @innocenzi in https://github.com/spatie/laravel-data/pull/881 +* Register optimize commands by @erikgaal in https://github.com/spatie/laravel-data/pull/880 + +**Full Changelog**: https://github.com/spatie/laravel-data/compare/4.10.1...4.11.0 + ## 4.10.1 - 2024-10-07 - Fix an issue where optional default values would disable validation From 44c686d28de6cc7a6b7a915e1529a6b642e55945 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Wed, 23 Oct 2024 09:11:37 +0200 Subject: [PATCH 11/21] Fix an issue where the caching command would crash if an issue did not exist --- src/Support/Caching/DataClassFinder.php | 2 +- .../DataStructuresCacheCommandTest.php | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Support/Caching/DataClassFinder.php b/src/Support/Caching/DataClassFinder.php index e1397cdce..0b4b43d3a 100644 --- a/src/Support/Caching/DataClassFinder.php +++ b/src/Support/Caching/DataClassFinder.php @@ -10,7 +10,7 @@ class DataClassFinder public static function fromConfig(array $config): self { return new self( - directories: $config['directories'], + directories: array_filter($config['directories'], 'is_dir'), useReflection: $config['reflection_discovery']['enabled'], reflectionBasePath: $config['reflection_discovery']['base_path'], reflectionRootNamespace: $config['reflection_discovery']['root_namespace'], diff --git a/tests/Commands/DataStructuresCacheCommandTest.php b/tests/Commands/DataStructuresCacheCommandTest.php index a578dffc2..f4a918430 100644 --- a/tests/Commands/DataStructuresCacheCommandTest.php +++ b/tests/Commands/DataStructuresCacheCommandTest.php @@ -37,3 +37,27 @@ function () { expect(invade($config)->transformers)->toHaveCount(count(config('data.transformers'))); expect(invade($config)->casts)->toHaveCount(count(config('data.casts'))); }); + +it('will ignore non existing directories', function () { + // Ensure we cache + App::forgetInstance(DataConfig::class); + + app()->singleton( + DataConfig::class, + function () { + return app()->make(DataStructureCache::class)->getConfig() ?? DataConfig::createFromConfig(config('data')); + } + ); + + config()->set('data.structure_caching.directories', [ + __DIR__.'/../Fakes', + __DIR__.'/../NonExisting', + ]); + + config()->set('data.structure_caching.reflection_discovery.base_path', __DIR__.'/../Fakes'); + config()->set('data.structure_caching.reflection_discovery.root_namespace', 'Spatie\LaravelData\Tests\Fakes'); + + $this->artisan('data:cache-structures')->assertExitCode(0); + + expect(cache()->has('laravel-data.data-class.'. SimpleData::class))->toBeTrue(); +}); From 22270e2c8b248b45d990b50d6da8a3ffa92b1644 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Wed, 23 Oct 2024 09:14:53 +0200 Subject: [PATCH 12/21] Fix PHPStan issues --- src/Commands/DataMakeCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Commands/DataMakeCommand.php b/src/Commands/DataMakeCommand.php index 1b26dac17..075c51fac 100644 --- a/src/Commands/DataMakeCommand.php +++ b/src/Commands/DataMakeCommand.php @@ -30,14 +30,14 @@ protected function resolveStubPath($stub): string protected function getDefaultNamespace($rootNamespace): string { - $namespace = trim($this->option('namespace') ?? 'Data', '\\'); + $namespace = trim($this->option('namespace'), '\\'); return trim($rootNamespace . '\\' . $namespace, '\\'); } protected function qualifyClass($name): string { - $suffix = trim($this->option('suffix') ?? 'Data'); + $suffix = trim($this->option('suffix')); if (! empty($suffix) && ! Str::endsWith($name, $suffix)) { $name = $name . $suffix; } From eba43850f425384bf126a972ba53901fadd8d367 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Wed, 23 Oct 2024 07:16:59 +0000 Subject: [PATCH 13/21] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe8871507..61d8eb232 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to `laravel-data` will be documented in this file. +## 4.11.1 - 2024-10-23 + +- Fix an issue where the cache structures command did not work if the directory did not exist (#892) + ## 4.11.0 - 2024-10-22 ### What's Changed From 385468585f7f21710e72b1c80377ada3f77454b5 Mon Sep 17 00:00:00 2001 From: circle33 Date: Wed, 25 Dec 2024 02:29:15 +0000 Subject: [PATCH 14/21] Trigger Action From 7b17f8209ce915453bf4365a2fd1c01e376d4409 Mon Sep 17 00:00:00 2001 From: circle33 Date: Wed, 25 Dec 2024 02:34:39 +0000 Subject: [PATCH 15/21] trigger action --- phpstan.neon.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index a91cc695a..508afc1aa 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,6 +1,6 @@ includes: - phpstan-baseline.neon - + parameters: level: 5 paths: From 744d01c9b2061b470427c6ff5c3c0f7d4a2d4211 Mon Sep 17 00:00:00 2001 From: circle33 Date: Wed, 25 Dec 2024 03:04:27 +0000 Subject: [PATCH 16/21] fix phpstan issue --- phpstan-baseline.neon | 2 +- phpstan.neon.dist | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 32fc4f3e6..22a10b390 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -121,7 +121,7 @@ parameters: path: src/Support/DataConfig.php - - message: "#^Call to method Illuminate\\\\Support\\\\Collection\\<\\(int\\|string\\),string\\|null\\>\\:\\:isEmpty\\(\\) will always evaluate to false\\.$#" + message: "#^Call to method Illuminate\\\\Support\\\\Collection\\<\\(string\\|int\\),null\\|string\\>\\:\\:isEmpty\\(\\) will always evaluate to false\\.$#" count: 1 path: src/Support/Validation/RuleDenormalizer.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 508afc1aa..a91cc695a 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,6 +1,6 @@ includes: - phpstan-baseline.neon - + parameters: level: 5 paths: From 3d6a04f5f64069a99c05136771f8bc5fd15a8de4 Mon Sep 17 00:00:00 2001 From: circle33 Date: Wed, 25 Dec 2024 03:07:39 +0000 Subject: [PATCH 17/21] fix phpstan issue --- phpstan-baseline.neon | 5 ----- 1 file changed, 5 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 22a10b390..c83f5b75e 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -120,11 +120,6 @@ parameters: count: 1 path: src/Support/DataConfig.php - - - message: "#^Call to method Illuminate\\\\Support\\\\Collection\\<\\(string\\|int\\),null\\|string\\>\\:\\:isEmpty\\(\\) will always evaluate to false\\.$#" - count: 1 - path: src/Support/Validation/RuleDenormalizer.php - - message: "#^Call to an undefined method DateTimeInterface\\:\\:setTimezone\\(\\)\\.$#" count: 1 From df950366ef15e7d852200988e9199c508a764d8f Mon Sep 17 00:00:00 2001 From: circle33 Date: Wed, 25 Dec 2024 03:14:20 +0000 Subject: [PATCH 18/21] fix phpstan issue --- phpstan.neon.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index a91cc695a..508afc1aa 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,6 +1,6 @@ includes: - phpstan-baseline.neon - + parameters: level: 5 paths: From 0c4001af6d0abe98153a1a32f8ba52167f70f038 Mon Sep 17 00:00:00 2001 From: circle33 Date: Wed, 25 Dec 2024 03:18:34 +0000 Subject: [PATCH 19/21] fix github action issue --- phpstan.neon.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 508afc1aa..a91cc695a 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,6 +1,6 @@ includes: - phpstan-baseline.neon - + parameters: level: 5 paths: From 4e7d41cba9964d2fc68d04572b189df6e2d1e915 Mon Sep 17 00:00:00 2001 From: circle33 Date: Thu, 26 Dec 2024 03:09:29 +0000 Subject: [PATCH 20/21] Update to be compatible with phpdocumentor/reflection-docblock 5.6.1 --- .../Annotations/CollectionAnnotationReader.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Support/Annotations/CollectionAnnotationReader.php b/src/Support/Annotations/CollectionAnnotationReader.php index 9187c3855..930c374fa 100644 --- a/src/Support/Annotations/CollectionAnnotationReader.php +++ b/src/Support/Annotations/CollectionAnnotationReader.php @@ -4,7 +4,7 @@ use Iterator; use IteratorAggregate; -use phpDocumentor\Reflection\DocBlock\Tags\Generic; +use phpDocumentor\Reflection\DocBlock\Tag; use phpDocumentor\Reflection\DocBlockFactory; use phpDocumentor\Reflection\TypeResolver; use phpDocumentor\Reflection\Types\Context; @@ -100,12 +100,12 @@ protected function getCollectionReturnType(ReflectionClass $class): ?array $valueType = null; foreach ($docBlock->getTags() as $tag) { - if (! $tag instanceof Generic) { + if (! $tag instanceof Tag) { continue; } if ($tag->getName() === 'template') { - $description = $tag->getDescription(); + $description = (string)$tag; if (preg_match('/^(\w+)\s+of\s+([^\s]+)/', $description, $matches)) { $templateTypes[$matches[1]] = $this->resolve($matches[2]); @@ -115,15 +115,15 @@ protected function getCollectionReturnType(ReflectionClass $class): ?array } if ($tag->getName() === 'extends') { - $description = $tag->getDescription(); + $description = (string)$tag; if (preg_match('/<\s*([^,\s]+)?\s*(?:,\s*([^>\s]+))?\s*>/', $description, $matches)) { if (count($matches) === 3) { - $keyType = $templateTypes[$matches[1]] ?? $this->resolve($matches[1]); - $valueType = $templateTypes[$matches[2]] ?? $this->resolve($matches[2]); + $keyType = $templateTypes[class_basename($matches[1])] ?? $this->resolve($matches[1]); + $valueType = $templateTypes[class_basename($matches[2])] ?? $this->resolve($matches[2]); } else { $keyType = null; - $valueType = $templateTypes[$matches[1]] ?? $this->resolve($matches[1]); + $valueType = $templateTypes[class_basename($matches[1])] ?? $this->resolve($matches[1]); } $keyType = $keyType ? explode('|', $keyType)[0] : null; From fbbda2a79a781b4a53a742c30adbc8929e96ee81 Mon Sep 17 00:00:00 2001 From: circle33 Date: Thu, 26 Dec 2024 04:18:06 +0000 Subject: [PATCH 21/21] refactor: improving readability --- .../CollectionAnnotationReader.php | 71 +++++++++++-------- 1 file changed, 42 insertions(+), 29 deletions(-) diff --git a/src/Support/Annotations/CollectionAnnotationReader.php b/src/Support/Annotations/CollectionAnnotationReader.php index 930c374fa..835000481 100644 --- a/src/Support/Annotations/CollectionAnnotationReader.php +++ b/src/Support/Annotations/CollectionAnnotationReader.php @@ -84,9 +84,7 @@ protected function isIterable(ReflectionClass $class): bool protected function getCollectionReturnType(ReflectionClass $class): ?array { $docBlockFactory = DocBlockFactory::createInstance(); - $this->context = $this->contextResolver->execute($class); - $docComment = $class->getDocComment(); if ($docComment === false) { @@ -96,50 +94,65 @@ protected function getCollectionReturnType(ReflectionClass $class): ?array $docBlock = $docBlockFactory->create($docComment, $this->context); $templateTypes = []; - $keyType = null; - $valueType = null; foreach ($docBlock->getTags() as $tag) { if (! $tag instanceof Tag) { continue; } - if ($tag->getName() === 'template') { - $description = (string)$tag; + $tagName = $tag->getName(); + $description = (string) $tag; - if (preg_match('/^(\w+)\s+of\s+([^\s]+)/', $description, $matches)) { - $templateTypes[$matches[1]] = $this->resolve($matches[2]); - } + if ($tagName === 'template') { + $this->processTemplateTag($description, $templateTypes); continue; } - if ($tag->getName() === 'extends') { - $description = (string)$tag; - - if (preg_match('/<\s*([^,\s]+)?\s*(?:,\s*([^>\s]+))?\s*>/', $description, $matches)) { - if (count($matches) === 3) { - $keyType = $templateTypes[class_basename($matches[1])] ?? $this->resolve($matches[1]); - $valueType = $templateTypes[class_basename($matches[2])] ?? $this->resolve($matches[2]); - } else { - $keyType = null; - $valueType = $templateTypes[class_basename($matches[1])] ?? $this->resolve($matches[1]); - } - - $keyType = $keyType ? explode('|', $keyType)[0] : null; - $valueType = explode('|', $valueType)[0]; - - return [ - 'keyType' => $keyType, - 'valueType' => $valueType, - ]; - } + if ($tagName === 'extends') { + return $this->processExtendsTag($description, $templateTypes); } } return null; } + private function processTemplateTag(string $description, array &$templateTypes): void + { + // The pattern matches strings like "T of SomeType", capturing "T" as the template name + // and "SomeType" as the type associated with the template. + if (preg_match('/^(\w+)\s+of\s+([^\s]+)/', $description, $matches)) { + $templateTypes[$matches[1]] = $this->resolve($matches[2]); + } + } + + private function processExtendsTag(string $description, array $templateTypes): ?array + { + // The pattern matches strings like "" or "", + // capturing "KeyType" and "ValueType" or just "ValueType" if no key type is specified. + if (preg_match('/<\s*([^,\s]+)?\s*(?:,\s*([^>\s]+))?\s*>/', $description, $matches)) { + $keyType = null; + $valueType = null; + + if (count($matches) === 3) { + $keyType = $templateTypes[class_basename($matches[1])] ?? $this->resolve($matches[1]); + $valueType = $templateTypes[class_basename($matches[2])] ?? $this->resolve($matches[2]); + } else { + $valueType = $templateTypes[class_basename($matches[1])] ?? $this->resolve($matches[1]); + } + + $keyType = $keyType ? explode('|', $keyType)[0] : null; + $valueType = explode('|', $valueType)[0]; + + return [ + 'keyType' => $keyType, + 'valueType' => $valueType, + ]; + } + + return null; + } + protected function resolve(string $type): ?string { $type = (string) $this->typeResolver->resolve($type, $this->context);