Skip to content

Commit f9f9b07

Browse files
author
Andrey Helldar
committed
[6.x] Added creation of custom Cast classes for Eloquent
1 parent 4d4c098 commit f9f9b07

File tree

4 files changed

+223
-16
lines changed

4 files changed

+223
-16
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
3+
namespace Illuminate\Contracts\Database\Eloquent;
4+
5+
interface Castable
6+
{
7+
/**
8+
* Get a given attribute from the model.
9+
*
10+
* @param mixed $value
11+
*
12+
* @return mixed
13+
*/
14+
public function get($value);
15+
16+
/**
17+
* Set a given attribute on the model.
18+
*
19+
* @param mixed $value
20+
*
21+
* @return mixed
22+
*/
23+
public function set($value);
24+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,10 @@ public function setAttribute($key, $value)
441441
$value = $this->fromDateTime($value);
442442
}
443443

444+
if ($this->isCustomCastable($key) && ! is_null($value)) {
445+
$value = $this->toCustomCastable($key, $value);
446+
}
447+
444448
if ($this->isJsonCastable($key) && ! is_null($value)) {
445449
$value = $this->castAttributeAsJson($key, $value);
446450
}

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

Lines changed: 94 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
namespace Illuminate\Database\Eloquent\Concerns;
44

5+
use Illuminate\Container\Container;
6+
use Illuminate\Contracts\Container\BindingResolutionException;
7+
use Illuminate\Contracts\Database\Eloquent\Castable;
58
use Illuminate\Contracts\Support\Arrayable;
69
use Illuminate\Database\Eloquent\JsonEncodingException;
710
use Illuminate\Support\Collection;
@@ -25,11 +28,13 @@ trait HasCasts
2528
*/
2629
public function hasCast($key, $types = null)
2730
{
28-
if (array_key_exists($key, $this->getCasts())) {
29-
return $types ? in_array($this->getCastType($key), (array) $types, true) : true;
31+
if (! array_key_exists($key, $this->getCasts())) {
32+
return false;
3033
}
3134

32-
return false;
35+
return $types
36+
? in_array($this->getCastType($key), (array) $types, true)
37+
: true;
3338
}
3439

3540
/**
@@ -39,11 +44,14 @@ public function hasCast($key, $types = null)
3944
*/
4045
public function getCasts()
4146
{
42-
if ($this->getIncrementing()) {
43-
return array_merge([$this->getKeyName() => $this->getKeyType()], $this->casts);
44-
}
47+
return $this->getIncrementing()
48+
? array_merge([$this->getKeyName() => $this->getKeyType()], $this->casts)
49+
: $this->casts;
50+
}
4551

46-
return $this->casts;
52+
public function getCast($key)
53+
{
54+
return $this->getCasts()[$key] ?? null;
4755
}
4856

4957
/**
@@ -52,6 +60,8 @@ public function getCasts()
5260
* @param string $key
5361
* @param mixed $current
5462
*
63+
* @throws BindingResolutionException
64+
*
5565
* @return bool
5666
*/
5767
public function originalIsEquivalent($key, $current)
@@ -87,6 +97,8 @@ public function originalIsEquivalent($key, $current)
8797
* @param array $attributes
8898
* @param array $mutatedAttributes
8999
*
100+
* @throws BindingResolutionException
101+
*
90102
* @return array
91103
*/
92104
protected function addCastAttributesToArray(array $attributes, array $mutatedAttributes)
@@ -106,12 +118,11 @@ protected function addCastAttributesToArray(array $attributes, array $mutatedAtt
106118
// If the attribute cast was a date or a datetime, we will serialize the date as
107119
// a string. This allows the developers to customize how dates are serialized
108120
// into an array without affecting how they are persisted into the storage.
109-
if ($attributes[$key] &&
110-
($value === 'date' || $value === 'datetime')) {
121+
if (($value === 'date' || $value === 'datetime')) {
111122
$attributes[$key] = $this->serializeDate($attributes[$key]);
112123
}
113124

114-
if ($attributes[$key] && $this->isCustomDateTimeCast($value)) {
125+
if ($this->isCustomDateTimeCast($value)) {
115126
$attributes[$key] = $attributes[$key]->format(explode(':', $value, 2)[1]);
116127
}
117128

@@ -129,9 +140,11 @@ protected function addCastAttributesToArray(array $attributes, array $mutatedAtt
129140
* @param string $key
130141
* @param mixed $value
131142
*
143+
* @throws BindingResolutionException
144+
*
132145
* @return mixed
133146
*/
134-
protected function castAttribute($key, $value)
147+
protected function castAttribute($key, $value = null)
135148
{
136149
if (is_null($value)) {
137150
return $value;
@@ -146,7 +159,7 @@ protected function castAttribute($key, $value)
146159
case 'double':
147160
return $this->fromFloat($value);
148161
case 'decimal':
149-
return $this->asDecimal($value, explode(':', $this->getCasts()[$key], 2)[1]);
162+
return $this->asDecimal($value, explode(':', $this->getCast($key), 2)[1]);
150163
case 'string':
151164
return (string) $value;
152165
case 'bool':
@@ -167,7 +180,9 @@ protected function castAttribute($key, $value)
167180
case 'timestamp':
168181
return $this->asTimestamp($value);
169182
default:
170-
return $value;
183+
return $this->isCustomCastable($key)
184+
? $this->fromCustomCastable($key, $value)
185+
: $value;
171186
}
172187
}
173188

@@ -180,15 +195,17 @@ protected function castAttribute($key, $value)
180195
*/
181196
protected function getCastType($key)
182197
{
183-
if ($this->isCustomDateTimeCast($this->getCasts()[$key])) {
198+
$cast = $this->getCast($key);
199+
200+
if ($this->isCustomDateTimeCast($cast)) {
184201
return 'custom_datetime';
185202
}
186203

187-
if ($this->isDecimalCast($this->getCasts()[$key])) {
204+
if ($this->isDecimalCast($cast)) {
188205
return 'decimal';
189206
}
190207

191-
return trim(strtolower($this->getCasts()[$key]));
208+
return trim(strtolower($cast));
192209
}
193210

194211
/**
@@ -250,6 +267,22 @@ protected function isDateAttribute($key)
250267
$this->isDateCastable($key);
251268
}
252269

270+
/**
271+
* Is the checked value a custom Cast.
272+
*
273+
* @param string $key
274+
*
275+
* @return bool
276+
*/
277+
protected function isCustomCastable($key)
278+
{
279+
if ($cast = $this->getCast($key)) {
280+
return is_subclass_of($cast, Castable::class);
281+
}
282+
283+
return false;
284+
}
285+
253286
/**
254287
* Determine whether a value is Date / DateTime castable for inbound manipulation.
255288
*
@@ -273,4 +306,49 @@ protected function isJsonCastable($key)
273306
{
274307
return $this->hasCast($key, ['array', 'json', 'object', 'collection']);
275308
}
309+
310+
/**
311+
* Getting the execution result from a user Cast object.
312+
*
313+
* @param string $key
314+
* @param null $value
315+
*
316+
* @throws BindingResolutionException
317+
*
318+
* @return mixed
319+
*/
320+
protected function fromCustomCastable($key, $value = null)
321+
{
322+
return $this
323+
->normalizeHandlerToCallable($key)
324+
->get($value);
325+
}
326+
327+
/**
328+
* @param $key
329+
* @param null $value
330+
*
331+
* @throws BindingResolutionException
332+
*
333+
* @return mixed
334+
*/
335+
protected function toCustomCastable($key, $value = null)
336+
{
337+
return $this
338+
->normalizeHandlerToCallable($key)
339+
->set($value);
340+
}
341+
342+
/**
343+
* @param string $key
344+
*
345+
* @throws BindingResolutionException
346+
*
347+
* @return Castable
348+
*/
349+
protected function normalizeHandlerToCallable($key)
350+
{
351+
return Container::getInstance()
352+
->make($this->getCast($key));
353+
}
276354
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Integration\Database;
4+
5+
use Illuminate\Contracts\Database\Eloquent\Castable;
6+
use Illuminate\Database\Eloquent\Model;
7+
use Illuminate\Database\Schema\Blueprint;
8+
use Illuminate\Support\Facades\Schema;
9+
10+
class EloquentModelCustomCastingTest extends DatabaseTestCase
11+
{
12+
public function testFoo()
13+
{
14+
$item = TestModel::create([
15+
'field_1' => 'foobar',
16+
'field_2' => 20,
17+
'field_3' => '08:19:12',
18+
]);
19+
20+
$this->assertSame(['f', 'o', 'o', 'b', 'a', 'r'], $item->toArray()['field_1']);
21+
22+
$this->assertSame(0.2, $item->toArray()['field_2']);
23+
24+
$this->assertIsNumeric($item->toArray()['field_3']);
25+
26+
$this->assertSame(
27+
strtotime('08:19:12'),
28+
$item->toArray()['field_3']
29+
);
30+
}
31+
32+
protected function setUp(): void
33+
{
34+
parent::setUp();
35+
36+
Schema::create('test_model1', function (Blueprint $table) {
37+
$table->increments('id');
38+
$table->string('field_1')->nullable();
39+
$table->integer('field_2')->nullable();
40+
$table->time('field_3')->nullable();
41+
});
42+
}
43+
}
44+
45+
class TestModel extends Model
46+
{
47+
public $table = 'test_model1';
48+
49+
public $timestamps = false;
50+
51+
public $casts = [
52+
'field_1' => StringCast::class,
53+
'field_2' => NumberCast::class,
54+
'field_3' => TimeCast::class,
55+
];
56+
57+
protected $guarded = ['id'];
58+
}
59+
60+
class TimeCast implements Castable
61+
{
62+
public function get($value)
63+
{
64+
return strtotime($value);
65+
}
66+
67+
public function set($value)
68+
{
69+
return is_numeric($value)
70+
? date('H:i:s', strtotime($value))
71+
: $value;
72+
}
73+
}
74+
75+
class StringCast implements Castable
76+
{
77+
public function get($value)
78+
{
79+
return mb_str_split($value);
80+
}
81+
82+
public function set($value)
83+
{
84+
return is_array($value)
85+
? implode('', $value)
86+
: $value;
87+
}
88+
}
89+
90+
class NumberCast implements Castable
91+
{
92+
public function get($value)
93+
{
94+
return $value / 100;
95+
}
96+
97+
public function set($value)
98+
{
99+
return $value;
100+
}
101+
}

0 commit comments

Comments
 (0)