Skip to content

Commit e14ae87

Browse files
authored
fix(laravel): resolve casts defined via casts() method (#7859)
Closes #7662
1 parent 0224d80 commit e14ae87

3 files changed

Lines changed: 85 additions & 1 deletion

File tree

src/Laravel/Eloquent/Metadata/ModelMetadata.php

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,13 @@ private function getCastType(string $column, Model $model): ?string
268268
/**
269269
* Gets the model casts, including any date casts.
270270
*
271+
* In Laravel 11+, casts can be defined via the protected casts() method
272+
* in addition to the $casts property. Since models may be instantiated
273+
* without calling the constructor (newInstanceWithoutConstructor),
274+
* initializeHasAttributes() is never called and the casts() method
275+
* results are not merged into $casts. We call casts() via reflection
276+
* to ensure both sources are included.
277+
*
271278
* @return array<string, mixed>
272279
*/
273280
private function getCastsWithDates(Model $model): array
@@ -280,7 +287,15 @@ private function getCastsWithDates(Model $model): array
280287
}
281288
}
282289

283-
return array_merge($dateCasts, $model->getCasts());
290+
$casts = $model->getCasts();
291+
292+
try {
293+
$castsMethod = new \ReflectionMethod($model, 'casts');
294+
$casts = array_merge($casts, $castsMethod->invoke($model));
295+
} catch (\ReflectionException) {
296+
}
297+
298+
return array_merge($dateCasts, $casts);
284299
}
285300

286301
/**

src/Laravel/Tests/Eloquent/Metadata/ModelMetadataTest.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Orchestra\Testbench\Concerns\WithWorkbench;
2121
use Orchestra\Testbench\TestCase;
2222
use Workbench\App\Models\Book;
23+
use Workbench\App\Models\BookWithMethodCasts;
2324
use Workbench\App\Models\Device;
2425

2526
/**
@@ -82,6 +83,25 @@ public function secret(): HasMany // @phpstan-ignore-line
8283
$this->assertCount(1, $metadata->getRelations($model));
8384
}
8485

86+
/**
87+
* Casts defined via the casts() method (Laravel 11+) should be detected
88+
* just like those defined via the $casts property.
89+
*
90+
* @see https://github.com/api-platform/core/issues/7662
91+
*/
92+
public function testCastsMethodIsDetected(): void
93+
{
94+
// Use newInstanceWithoutConstructor to replicate how API Platform creates models
95+
$refl = new \ReflectionClass(BookWithMethodCasts::class);
96+
$model = $refl->newInstanceWithoutConstructor();
97+
98+
$metadata = new ModelMetadata();
99+
$attributes = $metadata->getAttributes($model);
100+
101+
$this->assertSame('boolean', $attributes['is_available']['cast']);
102+
$this->assertSame('datetime', $attributes['publication_date']['cast']);
103+
}
104+
85105
/**
86106
* When a model has a custom primary key (e.g. device_id) and a HasMany
87107
* relation whose foreign key on the related table has the same name,
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <dunglas@gmail.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Workbench\App\Models;
15+
16+
use ApiPlatform\Metadata\ApiResource;
17+
use ApiPlatform\Metadata\Get;
18+
use ApiPlatform\Metadata\GetCollection;
19+
use Illuminate\Database\Eloquent\Concerns\HasUlids;
20+
use Illuminate\Database\Eloquent\Model;
21+
22+
/**
23+
* Model that uses the casts() method instead of the $casts property.
24+
*
25+
* @see https://github.com/api-platform/core/issues/7662
26+
*/
27+
#[ApiResource(
28+
operations: [
29+
new Get(),
30+
new GetCollection(),
31+
],
32+
)]
33+
class BookWithMethodCasts extends Model
34+
{
35+
use HasUlids;
36+
37+
protected $table = 'books';
38+
39+
protected $visible = ['name', 'isbn', 'publication_date', 'is_available'];
40+
protected $fillable = ['name', 'isbn', 'publication_date', 'is_available'];
41+
42+
protected function casts(): array
43+
{
44+
return [
45+
'is_available' => 'boolean',
46+
'publication_date' => 'datetime',
47+
];
48+
}
49+
}

0 commit comments

Comments
 (0)