Skip to content

Commit 932e601

Browse files
authored
[9.x] Allow for null handling in custom casts (#38039)
* Pass null to custom cast set method when value is null * Add integration test for custom casts on Eloquent Model * Rename Address class to AddressCast in EloquentModelCustomCastingTest Prevents a duplicate naming conflict * Allow for proper null value handling in custom CastsAttributes implementations * Fix codestyle issue in EloquentModelCustomCastingTest.php
1 parent b1de554 commit 932e601

File tree

2 files changed

+269
-16
lines changed

2 files changed

+269
-16
lines changed

src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php

+6-16
Original file line numberDiff line numberDiff line change
@@ -859,22 +859,12 @@ protected function setClassCastableAttribute($key, $value)
859859
{
860860
$caster = $this->resolveCasterClass($key);
861861

862-
if (is_null($value)) {
863-
$this->attributes = array_merge($this->attributes, array_map(
864-
function () {
865-
},
866-
$this->normalizeCastClassResponse($key, $caster->set(
867-
$this, $key, $this->{$key}, $this->attributes
868-
))
869-
));
870-
} else {
871-
$this->attributes = array_merge(
872-
$this->attributes,
873-
$this->normalizeCastClassResponse($key, $caster->set(
874-
$this, $key, $value, $this->attributes
875-
))
876-
);
877-
}
862+
$this->attributes = array_merge(
863+
$this->attributes,
864+
$this->normalizeCastClassResponse($key, $caster->set(
865+
$this, $key, $value, $this->attributes
866+
))
867+
);
878868

879869
if ($caster instanceof CastsInboundAttributes || ! is_object($value)) {
880870
unset($this->classCastCache[$key]);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Integration\Database;
4+
5+
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
6+
use Illuminate\Database\Capsule\Manager as DB;
7+
use Illuminate\Database\Eloquent\Model as Eloquent;
8+
use Illuminate\Database\Schema\Blueprint;
9+
use InvalidArgumentException;
10+
use PHPUnit\Framework\TestCase;
11+
12+
/**
13+
* @group integration
14+
*/
15+
class EloquentModelCustomCastingTest extends TestCase
16+
{
17+
protected function setUp(): void
18+
{
19+
$db = new DB;
20+
21+
$db->addConnection([
22+
'driver' => 'sqlite',
23+
'database' => ':memory:',
24+
]);
25+
26+
$db->bootEloquent();
27+
$db->setAsGlobal();
28+
29+
$this->createSchema();
30+
}
31+
32+
/**
33+
* Setup the database schema.
34+
*
35+
* @return void
36+
*/
37+
public function createSchema()
38+
{
39+
$this->schema()->create('casting_table', function (Blueprint $table) {
40+
$table->increments('id');
41+
$table->string('address_line_one');
42+
$table->string('address_line_two');
43+
$table->string('string_field');
44+
$table->timestamps();
45+
});
46+
}
47+
48+
/**
49+
* Tear down the database schema.
50+
*
51+
* @return void
52+
*/
53+
protected function tearDown(): void
54+
{
55+
$this->schema()->drop('casting_table');
56+
}
57+
58+
/**
59+
* Tests...
60+
*/
61+
public function testSavingCastedAttributesToDatabase()
62+
{
63+
/** @var \Illuminate\Tests\Integration\Database\CustomCasts $model */
64+
$model = CustomCasts::create([
65+
'address' => new AddressModel('address_line_one_value', 'address_line_two_value'),
66+
'string_field' => null,
67+
]);
68+
69+
$this->assertSame('address_line_one_value', $model->getOriginal('address_line_one'));
70+
$this->assertSame('address_line_one_value', $model->getAttribute('address_line_one'));
71+
72+
$this->assertSame('address_line_two_value', $model->getOriginal('address_line_two'));
73+
$this->assertSame('address_line_two_value', $model->getAttribute('address_line_two'));
74+
75+
$this->assertSame(null, $model->getOriginal('string_field'));
76+
$this->assertSame(null, $model->getAttribute('string_field'));
77+
$this->assertSame('', $model->getRawOriginal('string_field'));
78+
79+
/** @var \Illuminate\Tests\Integration\Database\CustomCasts $another_model */
80+
$another_model = CustomCasts::create([
81+
'address_line_one' => 'address_line_one_value',
82+
'address_line_two' => 'address_line_two_value',
83+
'string_field' => 'string_value',
84+
]);
85+
86+
$this->assertInstanceOf(AddressModel::class, $another_model->address);
87+
88+
$this->assertSame('address_line_one_value', $model->address->lineOne);
89+
$this->assertSame('address_line_two_value', $model->address->lineTwo);
90+
}
91+
92+
public function testInvalidArgumentExceptionOnInvalidValue()
93+
{
94+
/** @var \Illuminate\Tests\Integration\Database\CustomCasts $model */
95+
$model = CustomCasts::create([
96+
'address' => new AddressModel('address_line_one_value', 'address_line_two_value'),
97+
'string_field' => 'string_value',
98+
]);
99+
100+
$this->expectException(InvalidArgumentException::class);
101+
$this->expectExceptionMessage('The given value is not an Address instance.');
102+
$model->address = 'single_string';
103+
104+
// Ensure model values remain unchanged
105+
$this->assertSame('address_line_one_value', $model->address->lineOne);
106+
$this->assertSame('address_line_two_value', $model->address->lineTwo);
107+
}
108+
109+
public function testInvalidArgumentExceptionOnNull()
110+
{
111+
/** @var \Illuminate\Tests\Integration\Database\CustomCasts $model */
112+
$model = CustomCasts::create([
113+
'address' => new AddressModel('address_line_one_value', 'address_line_two_value'),
114+
'string_field' => 'string_value',
115+
]);
116+
117+
$this->expectException(InvalidArgumentException::class);
118+
$this->expectExceptionMessage('The given value is not an Address instance.');
119+
$model->address = null;
120+
121+
// Ensure model values remain unchanged
122+
$this->assertSame('address_line_one_value', $model->address->lineOne);
123+
$this->assertSame('address_line_two_value', $model->address->lineTwo);
124+
}
125+
126+
/**
127+
* Get a database connection instance.
128+
*
129+
* @return \Illuminate\Database\Connection
130+
*/
131+
protected function connection()
132+
{
133+
return Eloquent::getConnectionResolver()->connection();
134+
}
135+
136+
/**
137+
* Get a schema builder instance.
138+
*
139+
* @return \Illuminate\Database\Schema\Builder
140+
*/
141+
protected function schema()
142+
{
143+
return $this->connection()->getSchemaBuilder();
144+
}
145+
}
146+
147+
/**
148+
* Eloquent Casts...
149+
*/
150+
class AddressCast implements CastsAttributes
151+
{
152+
/**
153+
* Cast the given value.
154+
*
155+
* @param \Illuminate\Database\Eloquent\Model $model
156+
* @param string $key
157+
* @param mixed $value
158+
* @param array $attributes
159+
* @return AddressModel
160+
*/
161+
public function get($model, $key, $value, $attributes)
162+
{
163+
return new AddressModel(
164+
$attributes['address_line_one'],
165+
$attributes['address_line_two'],
166+
);
167+
}
168+
169+
/**
170+
* Prepare the given value for storage.
171+
*
172+
* @param \Illuminate\Database\Eloquent\Model $model
173+
* @param string $key
174+
* @param AddressModel $value
175+
* @param array $attributes
176+
* @return array
177+
*/
178+
public function set($model, $key, $value, $attributes)
179+
{
180+
if (! $value instanceof AddressModel) {
181+
throw new InvalidArgumentException('The given value is not an Address instance.');
182+
}
183+
184+
return [
185+
'address_line_one' => $value->lineOne,
186+
'address_line_two' => $value->lineTwo,
187+
];
188+
}
189+
}
190+
191+
class NonNullableString implements CastsAttributes
192+
{
193+
/**
194+
* Cast the given value.
195+
*
196+
* @param \Illuminate\Database\Eloquent\Model $model
197+
* @param string $key
198+
* @param string $value
199+
* @param array $attributes
200+
* @return string|null
201+
*/
202+
public function get($model, $key, $value, $attributes)
203+
{
204+
return ($value != '') ? $value : null;
205+
}
206+
207+
/**
208+
* Prepare the given value for storage.
209+
*
210+
* @param \Illuminate\Database\Eloquent\Model $model
211+
* @param string $key
212+
* @param string|null $value
213+
* @param array $attributes
214+
* @return string
215+
*/
216+
public function set($model, $key, $value, $attributes)
217+
{
218+
return $value ?? '';
219+
}
220+
}
221+
222+
/**
223+
* Eloquent Models...
224+
*/
225+
class CustomCasts extends Eloquent
226+
{
227+
/**
228+
* @var string
229+
*/
230+
protected $table = 'casting_table';
231+
232+
/**
233+
* @var string[]
234+
*/
235+
protected $guarded = [];
236+
237+
/**
238+
* @var array
239+
*/
240+
protected $casts = [
241+
'address' => AddressCast::class,
242+
'string_field' => NonNullableString::class,
243+
];
244+
}
245+
246+
class AddressModel
247+
{
248+
/**
249+
* @var string
250+
*/
251+
public $lineOne;
252+
253+
/**
254+
* @var string
255+
*/
256+
public $lineTwo;
257+
258+
public function __construct($address_line_one, $address_line_two)
259+
{
260+
$this->lineOne = $address_line_one;
261+
$this->lineTwo = $address_line_two;
262+
}
263+
}

0 commit comments

Comments
 (0)