From a4d3b81a4d273ee87cefda2d674b8c87f7f1cf9c Mon Sep 17 00:00:00 2001 From: Patrick Organ Date: Fri, 25 Nov 2022 08:24:04 -0500 Subject: [PATCH 01/15] add dependabot configuration --- .github/dependabot.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..a76dd83f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 + +updates: + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" From 6b67094dc704fed0c3405fb91ca9c9b08af4795c Mon Sep 17 00:00:00 2001 From: Patrick Organ Date: Fri, 25 Nov 2022 08:24:04 -0500 Subject: [PATCH 02/15] add workflow to auto-merge dependabot PRs --- .github/workflows/dependabot-auto-merge.yml | 32 +++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/dependabot-auto-merge.yml diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 00000000..e7e28b90 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,32 @@ +name: dependabot-auto-merge +on: pull_request_target + +permissions: + pull-requests: write + contents: write + +jobs: + dependabot: + runs-on: ubuntu-latest + if: ${{ github.actor == 'dependabot[bot]' }} + steps: + + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@v1.3.5 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Auto-merge Dependabot PRs for semver-minor updates + if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} + + - name: Auto-merge Dependabot PRs for semver-patch updates + if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}} + run: gh pr merge --auto --merge "$PR_URL" + env: + PR_URL: ${{github.event.pull_request.html_url}} + GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} From 9bf602ffcd89a9d0366135afedb1569811f4a476 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Nov 2022 14:17:58 +0000 Subject: [PATCH 03/15] Bump ramsey/composer-install from 1 to 2 Bumps [ramsey/composer-install](https://github.com/ramsey/composer-install) from 1 to 2. - [Release notes](https://github.com/ramsey/composer-install/releases) - [Commits](https://github.com/ramsey/composer-install/compare/v1...v2) --- updated-dependencies: - dependency-name: ramsey/composer-install dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/phpstan.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 5e06f4a4..56c4e3a2 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -25,7 +25,7 @@ jobs: coverage: none - name: Install composer dependencies - uses: ramsey/composer-install@v1 + uses: ramsey/composer-install@v2 - name: Run PHPStan run: ./vendor/bin/phpstan --error-format=github From b2fc0fdf651dc1a1dd5aeffce103e154a4504ef0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Nov 2022 14:18:01 +0000 Subject: [PATCH 04/15] Bump actions/checkout from 2 to 3 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/php-cs-fixer.yml | 2 +- .github/workflows/phpstan.yml | 2 +- .github/workflows/run-tests.yml | 2 +- .github/workflows/update-changelog.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml index a83d7080..4917b37e 100644 --- a/.github/workflows/php-cs-fixer.yml +++ b/.github/workflows/php-cs-fixer.yml @@ -8,7 +8,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: ref: ${{ github.head_ref }} diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 5e06f4a4..30ae9768 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -16,7 +16,7 @@ jobs: name: phpstan runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 09cf5311..6a5a68e5 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml index fa56639f..b20f3b6f 100644 --- a/.github/workflows/update-changelog.yml +++ b/.github/workflows/update-changelog.yml @@ -10,7 +10,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: ref: main From f452e8d15fe64a18fbd97d37ddd53621136fd4f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20G=C3=B6llner?= Date: Tue, 29 Nov 2022 15:59:50 +0100 Subject: [PATCH 05/15] add missing `array $context` to cast method --- docs/getting-started/quickstart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md index e57fa80c..90f064d0 100644 --- a/docs/getting-started/quickstart.md +++ b/docs/getting-started/quickstart.md @@ -99,7 +99,7 @@ That's because the status property expects a `PostStatus` enum object, but it ge ```php class PostStatusCast implements Cast { - public function cast(DataProperty $property, mixed $value): PostStatus + public function cast(DataProperty $property, mixed $value, array $context): PostStatus { return PostStatus::from($value); } From 5d53680f07cb97c8eb6c52b9cc245ba9c69587ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Nikolaou?= Date: Wed, 7 Dec 2022 02:23:45 +0200 Subject: [PATCH 06/15] Cast date times with a timezone --- docs/advanced-usage/working-with-dates.md | 9 ++ src/Casts/DateTimeInterfaceCast.php | 9 +- tests/Casts/DateTimeInterfaceCastTest.php | 102 ++++++++++++++++------ 3 files changed, 90 insertions(+), 30 deletions(-) diff --git a/docs/advanced-usage/working-with-dates.md b/docs/advanced-usage/working-with-dates.md index 13e0fc64..ca875c10 100644 --- a/docs/advanced-usage/working-with-dates.md +++ b/docs/advanced-usage/working-with-dates.md @@ -56,6 +56,15 @@ Now when casting a date, a valid format will be searched. When none can be found When a transformers hasn't explicitly stated it's format, the first format within the array is used. +## Casting dates in a different time zone + +Sometimes a date can be in a different timezone than the timezone you application uses. For example, if you application uses `Europe/Brussels` but your date is in `UTC`: + +```php +#[WithCast(DateTimeInterfaceCast::class, timeZone: 'UTC')] +public DateTime $date +``` + ## Changing time zones When casting a date you may want to set an alternative timezone this can be achieved as such: diff --git a/src/Casts/DateTimeInterfaceCast.php b/src/Casts/DateTimeInterfaceCast.php index 64a1d40c..26584da6 100644 --- a/src/Casts/DateTimeInterfaceCast.php +++ b/src/Casts/DateTimeInterfaceCast.php @@ -12,7 +12,8 @@ class DateTimeInterfaceCast implements Cast public function __construct( protected null|string|array $format = null, protected ?string $type = null, - protected ?string $setTimeZone = null + protected ?string $setTimeZone = null, + protected ?string $timeZone = null ) { } @@ -28,7 +29,11 @@ public function cast(DataProperty $property, mixed $value, array $context): Date /** @var DateTimeInterface|null $datetime */ $datetime = $formats - ->map(fn (string $format) => rescue(fn () => $type::createFromFormat($format, $value), report: false)) + ->map(fn (string $format) => rescue(fn () => $type::createFromFormat( + $format, + $value, + isset($this->timeZone) ? new DateTimeZone($this->timeZone) : null + ), report: false)) ->first(fn ($value) => (bool) $value); if (! $datetime) { diff --git a/tests/Casts/DateTimeInterfaceCastTest.php b/tests/Casts/DateTimeInterfaceCastTest.php index 449a2782..8301a65c 100644 --- a/tests/Casts/DateTimeInterfaceCastTest.php +++ b/tests/Casts/DateTimeInterfaceCastTest.php @@ -98,35 +98,81 @@ public DateTimeImmutable $dateTimeImmutable; }; - expect( - $caster->cast( - DataProperty::create(new ReflectionProperty($class, 'carbon')), - '19-05-1994 00:00:00', - [] - )->getTimezone() - )->toEqual(CarbonTimeZone::create('Europe/Brussels')); + expect($caster->cast( + DataProperty::create(new ReflectionProperty($class, 'carbon')), + '19-05-1994 00:00:00', + [] + )) + ->format('Y-m-d H:i:s')->toEqual('1994-05-19 02:00:00') + ->getTimezone()->toEqual(CarbonTimeZone::create('Europe/Brussels')); + + expect($caster->cast( + DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), + '19-05-1994 00:00:00', + [] + )) + ->format('Y-m-d H:i:s')->toEqual('1994-05-19 02:00:00') + ->getTimezone()->toEqual(CarbonTimeZone::create('Europe/Brussels')); + + expect($caster->cast( + DataProperty::create(new ReflectionProperty($class, 'dateTime')), + '19-05-1994 00:00:00', + [] + )) + ->format('Y-m-d H:i:s')->toEqual('1994-05-19 02:00:00') + ->getTimezone()->toEqual(new DateTimeZone('Europe/Brussels')); + + expect($caster->cast( + DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), + '19-05-1994 00:00:00', + [] + )) + ->format('Y-m-d H:i:s')->toEqual('1994-05-19 02:00:00') + ->getTimezone()->toEqual(new DateTimeZone('Europe/Brussels')); +}); - expect( - $caster->cast( - DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), - '19-05-1994 00:00:00', - [] - )->getTimezone() - )->toEqual(CarbonTimeZone::create('Europe/Brussels')); +it('can cast date times with a timezone', function () { + $caster = new DateTimeInterfaceCast('d-m-Y H:i:s', timeZone: 'Europe/Brussels'); - expect( - $caster->cast( - DataProperty::create(new ReflectionProperty($class, 'dateTime')), - '19-05-1994 00:00:00', - [] - )->getTimezone() - )->toEqual(new DateTimeZone('Europe/Brussels')); + $class = new class () { + public Carbon $carbon; - expect( - $caster->cast( - DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), - '19-05-1994 00:00:00', - [] - )->getTimezone() - )->toEqual(new DateTimeZone('Europe/Brussels')); + public CarbonImmutable $carbonImmutable; + + public DateTime $dateTime; + + public DateTimeImmutable $dateTimeImmutable; + }; + + expect($caster->cast( + DataProperty::create(new ReflectionProperty($class, 'carbon')), + '19-05-1994 00:00:00', + [] + )) + ->format('Y-m-d H:i:s')->toEqual('1994-05-19 00:00:00') + ->getTimezone()->toEqual(CarbonTimeZone::create('Europe/Brussels')); + + expect($caster->cast( + DataProperty::create(new ReflectionProperty($class, 'carbonImmutable')), + '19-05-1994 00:00:00', + [] + )) + ->format('Y-m-d H:i:s')->toEqual('1994-05-19 00:00:00') + ->getTimezone()->toEqual(CarbonTimeZone::create('Europe/Brussels')); + + expect($caster->cast( + DataProperty::create(new ReflectionProperty($class, 'dateTime')), + '19-05-1994 00:00:00', + [] + )) + ->format('Y-m-d H:i:s')->toEqual('1994-05-19 00:00:00') + ->getTimezone()->toEqual(new DateTimeZone('Europe/Brussels')); + + expect($caster->cast( + DataProperty::create(new ReflectionProperty($class, 'dateTimeImmutable')), + '19-05-1994 00:00:00', + [] + )) + ->format('Y-m-d H:i:s')->toEqual('1994-05-19 00:00:00') + ->getTimezone()->toEqual(new DateTimeZone('Europe/Brussels')); }); From c1a83d0856ddf4db7f9f15461727aac6899c2f82 Mon Sep 17 00:00:00 2001 From: David Heremans Date: Wed, 7 Dec 2022 10:29:34 +0100 Subject: [PATCH 07/15] Fixed copy-paste issue --- docs/questions-issues.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/questions-issues.md b/docs/questions-issues.md index 05a17931..102de060 100644 --- a/docs/questions-issues.md +++ b/docs/questions-issues.md @@ -3,6 +3,6 @@ title: Questions and issues weight: 5 --- -Find yourself stuck using the package? Found a bug? Do you have general questions or suggestions for improving Laravel Event Sourcing? Feel free to [create an issue on GitHub](https://github.com/spatie/laravel-data/issues), we'll try to address it as soon as possible. +Find yourself stuck using the package? Found a bug? Do you have general questions or suggestions for improving Laravel Data? Feel free to [create an issue on GitHub](https://github.com/spatie/laravel-data/issues), we'll try to address it as soon as possible. If you've found a bug regarding security please mail [freek@spatie.be](mailto:freek@spatie.be) instead of using the issue tracker. From 0fe88c254605a717b7a4fb7fdd18d7ac86b6afb4 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Wed, 7 Dec 2022 14:14:46 +0100 Subject: [PATCH 08/15] Remove request default pipeline --- src/Resolvers/DataFromSomethingResolver.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Resolvers/DataFromSomethingResolver.php b/src/Resolvers/DataFromSomethingResolver.php index fcdec677..73dc1eb5 100644 --- a/src/Resolvers/DataFromSomethingResolver.php +++ b/src/Resolvers/DataFromSomethingResolver.php @@ -92,12 +92,9 @@ protected function createFromCustomCreationMethod(string $class, array $payloads foreach ($payloads as $payload) { if ($payload instanceof Request) { - DataPipeline::create() + $class::pipeline() ->normalizer(ArrayableNormalizer::class) ->into($class) - ->through(AuthorizedDataPipe::class) - ->through(MapPropertiesDataPipe::class) - ->through(ValidatePropertiesDataPipe::class) ->using($payload) ->execute(); } From 16f716b54947da925a5af51ea71e71d5e3c78081 Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Wed, 7 Dec 2022 13:15:15 +0000 Subject: [PATCH 09/15] Fix styling --- src/Resolvers/DataFromSomethingResolver.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Resolvers/DataFromSomethingResolver.php b/src/Resolvers/DataFromSomethingResolver.php index 73dc1eb5..ce75e5cb 100644 --- a/src/Resolvers/DataFromSomethingResolver.php +++ b/src/Resolvers/DataFromSomethingResolver.php @@ -5,10 +5,6 @@ use Illuminate\Http\Request; use Illuminate\Support\Collection; use Spatie\LaravelData\Contracts\BaseData; -use Spatie\LaravelData\DataPipeline; -use Spatie\LaravelData\DataPipes\AuthorizedDataPipe; -use Spatie\LaravelData\DataPipes\MapPropertiesDataPipe; -use Spatie\LaravelData\DataPipes\ValidatePropertiesDataPipe; use Spatie\LaravelData\Normalizers\ArrayableNormalizer; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataMethod; From f71c0221a8e6e9437c7168a10d48ab799424be12 Mon Sep 17 00:00:00 2001 From: David Heremans Date: Wed, 7 Dec 2022 10:29:34 +0100 Subject: [PATCH 10/15] Fixed copy-paste issue --- docs/questions-issues.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/questions-issues.md b/docs/questions-issues.md index 05a17931..102de060 100644 --- a/docs/questions-issues.md +++ b/docs/questions-issues.md @@ -3,6 +3,6 @@ title: Questions and issues weight: 5 --- -Find yourself stuck using the package? Found a bug? Do you have general questions or suggestions for improving Laravel Event Sourcing? Feel free to [create an issue on GitHub](https://github.com/spatie/laravel-data/issues), we'll try to address it as soon as possible. +Find yourself stuck using the package? Found a bug? Do you have general questions or suggestions for improving Laravel Data? Feel free to [create an issue on GitHub](https://github.com/spatie/laravel-data/issues), we'll try to address it as soon as possible. If you've found a bug regarding security please mail [freek@spatie.be](mailto:freek@spatie.be) instead of using the issue tracker. From aeeb1e160cace6cc3845bd9c95cdfa548135496b Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Wed, 7 Dec 2022 14:14:46 +0100 Subject: [PATCH 11/15] Remove request default pipeline --- src/Resolvers/DataFromSomethingResolver.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Resolvers/DataFromSomethingResolver.php b/src/Resolvers/DataFromSomethingResolver.php index fcdec677..73dc1eb5 100644 --- a/src/Resolvers/DataFromSomethingResolver.php +++ b/src/Resolvers/DataFromSomethingResolver.php @@ -92,12 +92,9 @@ protected function createFromCustomCreationMethod(string $class, array $payloads foreach ($payloads as $payload) { if ($payload instanceof Request) { - DataPipeline::create() + $class::pipeline() ->normalizer(ArrayableNormalizer::class) ->into($class) - ->through(AuthorizedDataPipe::class) - ->through(MapPropertiesDataPipe::class) - ->through(ValidatePropertiesDataPipe::class) ->using($payload) ->execute(); } From 49e6cc75585120e796ab9ba4880aa734a2a8fcdd Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Wed, 7 Dec 2022 13:15:15 +0000 Subject: [PATCH 12/15] Fix styling --- src/Resolvers/DataFromSomethingResolver.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Resolvers/DataFromSomethingResolver.php b/src/Resolvers/DataFromSomethingResolver.php index 73dc1eb5..ce75e5cb 100644 --- a/src/Resolvers/DataFromSomethingResolver.php +++ b/src/Resolvers/DataFromSomethingResolver.php @@ -5,10 +5,6 @@ use Illuminate\Http\Request; use Illuminate\Support\Collection; use Spatie\LaravelData\Contracts\BaseData; -use Spatie\LaravelData\DataPipeline; -use Spatie\LaravelData\DataPipes\AuthorizedDataPipe; -use Spatie\LaravelData\DataPipes\MapPropertiesDataPipe; -use Spatie\LaravelData\DataPipes\ValidatePropertiesDataPipe; use Spatie\LaravelData\Normalizers\ArrayableNormalizer; use Spatie\LaravelData\Support\DataConfig; use Spatie\LaravelData\Support\DataMethod; From edaff7eb48f1d021d8b4b615bb00c5c002daed99 Mon Sep 17 00:00:00 2001 From: Ruben Van Assche Date: Wed, 7 Dec 2022 15:27:04 +0100 Subject: [PATCH 13/15] docs update --- docs/advanced-usage/working-with-dates.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/advanced-usage/working-with-dates.md b/docs/advanced-usage/working-with-dates.md index ca875c10..0ae42544 100644 --- a/docs/advanced-usage/working-with-dates.md +++ b/docs/advanced-usage/working-with-dates.md @@ -58,13 +58,15 @@ When a transformers hasn't explicitly stated it's format, the first format withi ## Casting dates in a different time zone -Sometimes a date can be in a different timezone than the timezone you application uses. For example, if you application uses `Europe/Brussels` but your date is in `UTC`: +Sometimes a date can be in a different timezone than the timezone you application uses. For example, if your application uses `Europe/Brussels` but your date is in `UTC`: ```php #[WithCast(DateTimeInterfaceCast::class, timeZone: 'UTC')] public DateTime $date ``` +The date will be created with the `UTC` timezone but will be the same as in the `Europe/Brussels` timezone. + ## Changing time zones When casting a date you may want to set an alternative timezone this can be achieved as such: @@ -74,6 +76,8 @@ When casting a date you may want to set an alternative timezone this can be achi public DateTime $date ``` +In this case the time will be transformed, if our original time was in `UTC` then one or two hours (depending on summer time) will be added. + You can also change the timezone of a property which is getting transformed: ```php From 1d005c7e43bc90133bb973b75edd05dda7f792bb Mon Sep 17 00:00:00 2001 From: rubenvanassche Date: Wed, 7 Dec 2022 14:52:58 +0000 Subject: [PATCH 14/15] Update CHANGELOG --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3fa5f5b..de9dc76d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to `laravel-data` will be documented in this file. +## 2.1.0 - 2022-12-07 + +- Stop using custom pipeline when creating data magically from requests +- set timezones in datetime casts (#287 ) + ## 2.0.16 - 2022-11-18 - add support for optional properties in TypeScript transformer (#153) From 689703a5c40e218f855b0a5e263bc872775380af Mon Sep 17 00:00:00 2001 From: Jonathan Havens Date: Wed, 30 Nov 2022 21:58:01 -0800 Subject: [PATCH 15/15] ignore phpstorm attributes when instantiating and add readonly property --- src/Resolvers/DataFromArrayResolver.php | 5 +- src/Support/DataClass.php | 9 ++- src/Support/DataProperty.php | 9 ++- tests/Support/DataClassTest.php | 46 ++++++++++++++++ tests/Support/DataPropertyTest.php | 73 +++++++++++++++++++++++++ 5 files changed, 135 insertions(+), 7 deletions(-) diff --git a/src/Resolvers/DataFromArrayResolver.php b/src/Resolvers/DataFromArrayResolver.php index 2c5a1020..650e60dc 100644 --- a/src/Resolvers/DataFromArrayResolver.php +++ b/src/Resolvers/DataFromArrayResolver.php @@ -40,7 +40,10 @@ public function execute(string $class, Collection $properties): BaseData $dataClass ->properties ->filter( - fn (DataProperty $property) => ! $property->isPromoted && $properties->has($property->name) + fn (DataProperty $property) => + ! $property->isPromoted && + ! $property->isReadonly && + $properties->has($property->name) ) ->each(function (DataProperty $property) use ($properties, $data) { $data->{$property->name} = $properties->get($property->name); diff --git a/src/Support/DataClass.php b/src/Support/DataClass.php index 2d4ba7c1..bc16b1c0 100644 --- a/src/Support/DataClass.php +++ b/src/Support/DataClass.php @@ -3,6 +3,7 @@ namespace Spatie\LaravelData\Support; use Illuminate\Support\Collection; +use Illuminate\Support\Str; use ReflectionAttribute; use ReflectionClass; use ReflectionMethod; @@ -31,6 +32,7 @@ public function __construct( public readonly Collection $properties, public readonly Collection $methods, public readonly ?DataMethod $constructorMethod, + public readonly bool $isReadonly, public readonly bool $appendable, public readonly bool $includeable, public readonly bool $responsable, @@ -43,9 +45,9 @@ public function __construct( public static function create(ReflectionClass $class): self { - $attributes = collect($class->getAttributes())->map( - fn (ReflectionAttribute $reflectionAttribute) => $reflectionAttribute->newInstance() - ); + $attributes = collect($class->getAttributes()) + ->filter(fn (ReflectionAttribute $reflectionAttribute) => class_exists($reflectionAttribute->getName())) + ->map(fn (ReflectionAttribute $reflectionAttribute) => $reflectionAttribute->newInstance()); $methods = collect($class->getMethods()); @@ -62,6 +64,7 @@ public static function create(ReflectionClass $class): self properties: $properties, methods: self::resolveMethods($class), constructorMethod: DataMethod::createConstructor($constructor, $properties), + isReadonly: method_exists($class, 'isReadOnly') && $class->isReadOnly(), appendable: $class->implementsInterface(AppendableData::class), includeable: $class->implementsInterface(IncludeableData::class), responsable: $class->implementsInterface(ResponsableData::class), diff --git a/src/Support/DataProperty.php b/src/Support/DataProperty.php index 58d9112b..19b1bc93 100644 --- a/src/Support/DataProperty.php +++ b/src/Support/DataProperty.php @@ -3,6 +3,7 @@ namespace Spatie\LaravelData\Support; use Illuminate\Support\Collection; +use Illuminate\Support\Str; use ReflectionAttribute; use ReflectionProperty; use Spatie\LaravelData\Attributes\WithCast; @@ -24,6 +25,7 @@ public function __construct( public readonly DataType $type, public readonly bool $validate, public readonly bool $isPromoted, + public readonly bool $isReadonly, public readonly bool $hasDefaultValue, public readonly mixed $defaultValue, public readonly ?Cast $cast, @@ -41,9 +43,9 @@ public static function create( ?NameMapper $classInputNameMapper = null, ?NameMapper $classOutputNameMapper = null, ): self { - $attributes = collect($property->getAttributes())->map( - fn (ReflectionAttribute $reflectionAttribute) => $reflectionAttribute->newInstance() - ); + $attributes = collect($property->getAttributes()) + ->filter(fn (ReflectionAttribute $reflectionAttribute) => class_exists($reflectionAttribute->getName())) + ->map(fn (ReflectionAttribute $reflectionAttribute) => $reflectionAttribute->newInstance()); $mappers = NameMappersResolver::create()->execute($attributes); @@ -65,6 +67,7 @@ className: $property->class, type: DataType::create($property), validate: ! $attributes->contains(fn (object $attribute) => $attribute instanceof WithoutValidation), isPromoted: $property->isPromoted(), + isReadonly: $property->isReadOnly(), hasDefaultValue: $property->isPromoted() ? $hasDefaultValue : $property->hasDefaultValue(), defaultValue: $property->isPromoted() ? $defaultValue : $property->getDefaultValue(), cast: $attributes->first(fn (object $attribute) => $attribute instanceof WithCast)?->get(), diff --git a/tests/Support/DataClassTest.php b/tests/Support/DataClassTest.php index 730a42d9..ea32dbe5 100644 --- a/tests/Support/DataClassTest.php +++ b/tests/Support/DataClassTest.php @@ -1,9 +1,11 @@ hasDefaultValue->toBeTrue() ->defaultValue->toEqual('Hello Again'); }); + +it('wont throw an error if a non existing attribute is used on a data class', function () { + expect(PhpStormClassAttributeData::from(['property' => 'hello'])->property)->toEqual('hello') + ->and(NonExistingAttributeData::from(['property' => 'hello'])->property)->toEqual('hello') + ->and(PhpStormClassAttributeData::from((object)['property' => 'hello'])->property)->toEqual('hello') + ->and(PhpStormClassAttributeData::from('{"property": "hello"}')->property)->toEqual('hello') + ->and(ModelWithPhpStormAttributeData::from((new DummyModel)->fill(['id' => 1]))->id)->toEqual(1); +}); + +#[\JetBrains\PhpStorm\Immutable] +class PhpStormClassAttributeData extends Data +{ + public readonly string $property; + + public function __construct(string $property) + { + $this->property = $property; + } +} + +#[\Foo\Bar] +class NonExistingAttributeData extends Data +{ + public readonly string $property; + + public function __construct(string $property) + { + $this->property = $property; + } +} + +#[\JetBrains\PhpStorm\Immutable] +class ModelWithPhpStormAttributeData extends Data +{ + public function __construct( + public int $id + ) { + } + + public static function fromDummyModel(DummyModel $model) + { + return new self($model->id); + } +} diff --git a/tests/Support/DataPropertyTest.php b/tests/Support/DataPropertyTest.php index 2ecb7424..50057a72 100644 --- a/tests/Support/DataPropertyTest.php +++ b/tests/Support/DataPropertyTest.php @@ -7,8 +7,10 @@ use Spatie\LaravelData\Attributes\WithoutValidation; use Spatie\LaravelData\Attributes\WithTransformer; use Spatie\LaravelData\Casts\DateTimeInterfaceCast; +use Spatie\LaravelData\Data; use Spatie\LaravelData\DataCollection; use Spatie\LaravelData\Support\DataProperty; +use Spatie\LaravelData\Tests\Fakes\DummyModel; use Spatie\LaravelData\Tests\Fakes\SimpleData; use Spatie\LaravelData\Transformers\DateTimeInterfaceTransformer; @@ -126,3 +128,74 @@ public function __construct( })->validate )->toBeFalse(); }); + +it('wont throw an error if non existing attribute is used on a data class property', function () { + expect(NonExistingPropertyAttributeData::from(['property' => 'hello'])->property)->toEqual('hello') + ->and(PhpStormAttributeData::from(['property' => 'hello'])->property)->toEqual('hello') + ->and(PhpStormAttributeData::from('{"property": "hello"}')->property)->toEqual('hello') + ->and(PhpStormAttributeData::from((object) ['property' => 'hello'])->property)->toEqual('hello') + ->and(ModelWithPhpStormAttributePropertyData::from((new DummyModel)->fill(['id' => 1]))->id)->toEqual(1) + ->and(ModelWithPromotedPhpStormAttributePropertyData::from((new DummyModel)->fill(['id' => 1]))->id)->toEqual(1); +}); + +class NonExistingPropertyAttributeData extends Data +{ + #[\Foo\Bar] + public readonly string $property; + + public function __construct(string $property) + { + $this->property = $property; + } +} + +class PhpStormAttributeData extends Data +{ + #[\JetBrains\PhpStorm\Immutable] + public readonly string $property; + + public function __construct(string $property) + { + $this->property = $property; + } +} + +class PromotedPhpStormAttributeData extends Data +{ + public function __construct( + #[\JetBrains\PhpStorm\Immutable] + public readonly string $property) + { + // + } +} + +class ModelWithPhpStormAttributePropertyData extends Data +{ + #[\JetBrains\PhpStorm\Immutable] + public int $id; + + public function __construct(int $id) + { + $this->id = $id; + } + + public static function fromDummyModel(DummyModel $model) + { + return new self($model->id); + } +} + +class ModelWithPromotedPhpStormAttributePropertyData extends Data +{ + public function __construct( + #[\JetBrains\PhpStorm\Immutable] + public int $id + ) { + } + + public static function fromDummyModel(DummyModel $model) + { + return new self($model->id); + } +}