From 692b058999f2d5b1c74d34a6b64766012d75b3fa Mon Sep 17 00:00:00 2001 From: soyuka Date: Sat, 21 Mar 2026 08:33:15 +0100 Subject: [PATCH] fix(laravel): resolve casts defined via casts() method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit | Q | A | ------------- | --- | Branch? | 4.2 | Tickets | Closes #7662 | License | MIT | Doc PR | ∅ Models created with newInstanceWithoutConstructor() skip initializeHasAttributes(), so casts() method results are never merged into $casts. Use reflection to call casts() in getCastsWithDates(). --- .../Eloquent/Metadata/ModelMetadata.php | 17 ++++++- .../Eloquent/Metadata/ModelMetadataTest.php | 20 ++++++++ .../app/Models/BookWithMethodCasts.php | 49 +++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 src/Laravel/workbench/app/Models/BookWithMethodCasts.php diff --git a/src/Laravel/Eloquent/Metadata/ModelMetadata.php b/src/Laravel/Eloquent/Metadata/ModelMetadata.php index 3888af1293..edf71770e1 100644 --- a/src/Laravel/Eloquent/Metadata/ModelMetadata.php +++ b/src/Laravel/Eloquent/Metadata/ModelMetadata.php @@ -268,6 +268,13 @@ private function getCastType(string $column, Model $model): ?string /** * Gets the model casts, including any date casts. * + * In Laravel 11+, casts can be defined via the protected casts() method + * in addition to the $casts property. Since models may be instantiated + * without calling the constructor (newInstanceWithoutConstructor), + * initializeHasAttributes() is never called and the casts() method + * results are not merged into $casts. We call casts() via reflection + * to ensure both sources are included. + * * @return array */ private function getCastsWithDates(Model $model): array @@ -280,7 +287,15 @@ private function getCastsWithDates(Model $model): array } } - return array_merge($dateCasts, $model->getCasts()); + $casts = $model->getCasts(); + + try { + $castsMethod = new \ReflectionMethod($model, 'casts'); + $casts = array_merge($casts, $castsMethod->invoke($model)); + } catch (\ReflectionException) { + } + + return array_merge($dateCasts, $casts); } /** diff --git a/src/Laravel/Tests/Eloquent/Metadata/ModelMetadataTest.php b/src/Laravel/Tests/Eloquent/Metadata/ModelMetadataTest.php index b81462b6f2..037a20e248 100644 --- a/src/Laravel/Tests/Eloquent/Metadata/ModelMetadataTest.php +++ b/src/Laravel/Tests/Eloquent/Metadata/ModelMetadataTest.php @@ -20,6 +20,7 @@ use Orchestra\Testbench\Concerns\WithWorkbench; use Orchestra\Testbench\TestCase; use Workbench\App\Models\Book; +use Workbench\App\Models\BookWithMethodCasts; use Workbench\App\Models\Device; /** @@ -82,6 +83,25 @@ public function secret(): HasMany // @phpstan-ignore-line $this->assertCount(1, $metadata->getRelations($model)); } + /** + * Casts defined via the casts() method (Laravel 11+) should be detected + * just like those defined via the $casts property. + * + * @see https://github.com/api-platform/core/issues/7662 + */ + public function testCastsMethodIsDetected(): void + { + // Use newInstanceWithoutConstructor to replicate how API Platform creates models + $refl = new \ReflectionClass(BookWithMethodCasts::class); + $model = $refl->newInstanceWithoutConstructor(); + + $metadata = new ModelMetadata(); + $attributes = $metadata->getAttributes($model); + + $this->assertSame('boolean', $attributes['is_available']['cast']); + $this->assertSame('datetime', $attributes['publication_date']['cast']); + } + /** * When a model has a custom primary key (e.g. device_id) and a HasMany * relation whose foreign key on the related table has the same name, diff --git a/src/Laravel/workbench/app/Models/BookWithMethodCasts.php b/src/Laravel/workbench/app/Models/BookWithMethodCasts.php new file mode 100644 index 0000000000..2215581c5a --- /dev/null +++ b/src/Laravel/workbench/app/Models/BookWithMethodCasts.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace Workbench\App\Models; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use Illuminate\Database\Eloquent\Concerns\HasUlids; +use Illuminate\Database\Eloquent\Model; + +/** + * Model that uses the casts() method instead of the $casts property. + * + * @see https://github.com/api-platform/core/issues/7662 + */ +#[ApiResource( + operations: [ + new Get(), + new GetCollection(), + ], +)] +class BookWithMethodCasts extends Model +{ + use HasUlids; + + protected $table = 'books'; + + protected $visible = ['name', 'isbn', 'publication_date', 'is_available']; + protected $fillable = ['name', 'isbn', 'publication_date', 'is_available']; + + protected function casts(): array + { + return [ + 'is_available' => 'boolean', + 'publication_date' => 'datetime', + ]; + } +}