Skip to content

Commit 60125ea

Browse files
[12.x] Extends AsCollection to map items into objects or other values (#55383)
* [12.x] Extends AsCollection for map * formatting * fix signature --------- Co-authored-by: Taylor Otwell <[email protected]>
1 parent 1f184bb commit 60125ea

File tree

4 files changed

+204
-11
lines changed

4 files changed

+204
-11
lines changed

src/Illuminate/Database/Eloquent/Casts/AsCollection.php

+39-5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Illuminate\Contracts\Database\Eloquent\Castable;
66
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
77
use Illuminate\Support\Collection;
8+
use Illuminate\Support\Str;
89
use InvalidArgumentException;
910

1011
class AsCollection implements Castable
@@ -21,6 +22,7 @@ public static function castUsing(array $arguments)
2122
{
2223
public function __construct(protected array $arguments)
2324
{
25+
$this->arguments = array_pad(array_values($this->arguments), 2, '');
2426
}
2527

2628
public function get($model, $key, $value, $attributes)
@@ -31,13 +33,29 @@ public function get($model, $key, $value, $attributes)
3133

3234
$data = Json::decode($attributes[$key]);
3335

34-
$collectionClass = $this->arguments[0] ?? Collection::class;
36+
$collectionClass = empty($this->arguments[0]) ? Collection::class : $this->arguments[0];
3537

3638
if (! is_a($collectionClass, Collection::class, true)) {
3739
throw new InvalidArgumentException('The provided class must extend ['.Collection::class.'].');
3840
}
3941

40-
return is_array($data) ? new $collectionClass($data) : null;
42+
if (! is_array($data)) {
43+
return null;
44+
}
45+
46+
$instance = new $collectionClass($data);
47+
48+
if (! isset($this->arguments[1]) || ! $this->arguments[1]) {
49+
return $instance;
50+
}
51+
52+
if (is_string($this->arguments[1])) {
53+
$this->arguments[1] = Str::parseCallback($this->arguments[1]);
54+
}
55+
56+
return is_callable($this->arguments[1])
57+
? $instance->map($this->arguments[1])
58+
: $instance->mapInto($this->arguments[1][0]);
4159
}
4260

4361
public function set($model, $key, $value, $attributes)
@@ -48,13 +66,29 @@ public function set($model, $key, $value, $attributes)
4866
}
4967

5068
/**
51-
* Specify the collection for the cast.
69+
* Specify the type of object each item in the collection should be mapped to.
70+
*
71+
* @param array{class-string, string}|class-string $map
72+
* @return string
73+
*/
74+
public static function of($map)
75+
{
76+
return static::using('', $map);
77+
}
78+
79+
/**
80+
* Specify the collection type for the cast.
5281
*
5382
* @param class-string $class
83+
* @param array{class-string, string}|class-string $map
5484
* @return string
5585
*/
56-
public static function using($class)
86+
public static function using($class, $map = null)
5787
{
58-
return static::class.':'.$class;
88+
if (is_array($map) && is_callable($map)) {
89+
$map = $map[0].'@'.$map[1];
90+
}
91+
92+
return static::class.':'.implode(',', [$class, $map]);
5993
}
6094
}

src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollection.php

+36-6
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
77
use Illuminate\Support\Collection;
88
use Illuminate\Support\Facades\Crypt;
9+
use Illuminate\Support\Str;
910
use InvalidArgumentException;
1011

1112
class AsEncryptedCollection implements Castable
@@ -22,21 +23,34 @@ public static function castUsing(array $arguments)
2223
{
2324
public function __construct(protected array $arguments)
2425
{
26+
$this->arguments = array_pad(array_values($this->arguments), 2, '');
2527
}
2628

2729
public function get($model, $key, $value, $attributes)
2830
{
29-
$collectionClass = $this->arguments[0] ?? Collection::class;
31+
$collectionClass = empty($this->arguments[0]) ? Collection::class : $this->arguments[0];
3032

3133
if (! is_a($collectionClass, Collection::class, true)) {
3234
throw new InvalidArgumentException('The provided class must extend ['.Collection::class.'].');
3335
}
3436

35-
if (isset($attributes[$key])) {
36-
return new $collectionClass(Json::decode(Crypt::decryptString($attributes[$key])));
37+
if (! isset($attributes[$key])) {
38+
return null;
3739
}
3840

39-
return null;
41+
$instance = new $collectionClass(Json::decode(Crypt::decryptString($attributes[$key])));
42+
43+
if (! isset($this->arguments[1]) || ! $this->arguments[1]) {
44+
return $instance;
45+
}
46+
47+
if (is_string($this->arguments[1])) {
48+
$this->arguments[1] = Str::parseCallback($this->arguments[1]);
49+
}
50+
51+
return is_callable($this->arguments[1])
52+
? $instance->map($this->arguments[1])
53+
: $instance->mapInto($this->arguments[1][0]);
4054
}
4155

4256
public function set($model, $key, $value, $attributes)
@@ -50,14 +64,30 @@ public function set($model, $key, $value, $attributes)
5064
};
5165
}
5266

67+
/**
68+
* Specify the type of object each item in the collection should be mapped to.
69+
*
70+
* @param array{class-string, string}|class-string $map
71+
* @return string
72+
*/
73+
public static function of($map)
74+
{
75+
return static::using('', $map);
76+
}
77+
5378
/**
5479
* Specify the collection for the cast.
5580
*
5681
* @param class-string $class
82+
* @param array{class-string, string}|class-string $map
5783
* @return string
5884
*/
59-
public static function using($class)
85+
public static function using($class, $map = null)
6086
{
61-
return static::class.':'.$class;
87+
if (is_array($map) && is_callable($map)) {
88+
$map = $map[0].'@'.$map[1];
89+
}
90+
91+
return static::class.':'.implode(',', [$class, $map]);
6292
}
6393
}

tests/Integration/Database/DatabaseCustomCastsTest.php

+76
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
use Illuminate\Database\Eloquent\Casts\AsStringable;
88
use Illuminate\Database\Eloquent\Model;
99
use Illuminate\Database\Schema\Blueprint;
10+
use Illuminate\Support\Collection;
1011
use Illuminate\Support\Facades\Hash;
1112
use Illuminate\Support\Facades\Schema;
13+
use Illuminate\Support\Fluent;
1214
use Illuminate\Support\Stringable;
1315

1416
class DatabaseCustomCastsTest extends DatabaseTestCase
@@ -151,6 +153,68 @@ public function test_custom_casting_nullable_values()
151153
$model->array_object_json->toArray()
152154
);
153155
}
156+
157+
public function test_as_collection_with_map_into()
158+
{
159+
$model = new TestEloquentModelWithCustomCasts();
160+
$model->mergeCasts([
161+
'collection' => AsCollection::of(Fluent::class),
162+
]);
163+
164+
$model->setRawAttributes([
165+
'collection' => json_encode([['foo' => 'bar']]),
166+
]);
167+
168+
$this->assertInstanceOf(Fluent::class, $model->collection->first());
169+
$this->assertSame('bar', $model->collection->first()->foo);
170+
}
171+
172+
public function test_as_custom_collection_with_map_into()
173+
{
174+
$model = new TestEloquentModelWithCustomCasts();
175+
$model->mergeCasts([
176+
'collection' => AsCollection::using(CustomCollection::class, Fluent::class),
177+
]);
178+
179+
$model->setRawAttributes([
180+
'collection' => json_encode([['foo' => 'bar']]),
181+
]);
182+
183+
$this->assertInstanceOf(CustomCollection::class, $model->collection);
184+
$this->assertInstanceOf(Fluent::class, $model->collection->first());
185+
$this->assertSame('bar', $model->collection->first()->foo);
186+
}
187+
188+
public function test_as_collection_with_map_callback(): void
189+
{
190+
$model = new TestEloquentModelWithCustomCasts();
191+
$model->mergeCasts([
192+
'collection' => AsCollection::of([FluentWithCallback::class, 'make']),
193+
]);
194+
195+
$model->setRawAttributes([
196+
'collection' => json_encode([['foo' => 'bar']]),
197+
]);
198+
199+
$this->assertInstanceOf(FluentWithCallback::class, $model->collection->first());
200+
$this->assertSame('bar', $model->collection->first()->foo);
201+
}
202+
203+
public function test_as_custom_collection_with_map_callback(): void
204+
{
205+
$model = new TestEloquentModelWithCustomCasts();
206+
$model->mergeCasts([
207+
'collection' => AsCollection::using(CustomCollection::class, [FluentWithCallback::class, 'make']),
208+
]);
209+
210+
$model->setRawAttributes([
211+
'collection' => json_encode([['foo' => 'bar']]),
212+
]);
213+
214+
$this->assertInstanceOf(CustomCollection::class, $model->collection);
215+
$this->assertInstanceOf(FluentWithCallback::class, $model->collection->first());
216+
$this->assertSame('bar', $model->collection->first()->foo);
217+
}
154218
}
155219

156220
class TestEloquentModelWithCustomCasts extends Model
@@ -197,3 +261,15 @@ class TestEloquentModelWithCustomCastsNullable extends Model
197261
'stringable' => AsStringable::class,
198262
];
199263
}
264+
265+
class FluentWithCallback extends Fluent
266+
{
267+
public static function make($attributes = [])
268+
{
269+
return new static($attributes);
270+
}
271+
}
272+
273+
class CustomCollection extends Collection
274+
{
275+
}

tests/Integration/Database/EloquentModelEncryptedCastingTest.php

+53
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Illuminate\Support\Collection;
1212
use Illuminate\Support\Facades\Crypt;
1313
use Illuminate\Support\Facades\Schema;
14+
use Illuminate\Support\Fluent;
1415
use stdClass;
1516

1617
class EloquentModelEncryptedCastingTest extends DatabaseTestCase
@@ -231,6 +232,58 @@ public function testAsEncryptedCollection()
231232
$this->assertNull($subject->fresh()->secret_collection);
232233
}
233234

235+
public function testAsEncryptedCollectionMap()
236+
{
237+
$this->encrypter->expects('encryptString')
238+
->twice()
239+
->with('[{"key1":"value1"}]')
240+
->andReturn('encrypted-secret-collection-string-1');
241+
$this->encrypter->expects('encryptString')
242+
->times(12)
243+
->with('[{"key1":"value1"},{"key2":"value2"}]')
244+
->andReturn('encrypted-secret-collection-string-2');
245+
$this->encrypter->expects('decryptString')
246+
->once()
247+
->with('encrypted-secret-collection-string-2')
248+
->andReturn('[{"key1":"value1"},{"key2":"value2"}]');
249+
250+
$subject = new EncryptedCast;
251+
252+
$subject->mergeCasts(['secret_collection' => AsEncryptedCollection::of(Fluent::class)]);
253+
254+
$subject->secret_collection = new Collection([new Fluent(['key1' => 'value1'])]);
255+
$subject->secret_collection->push(new Fluent(['key2' => 'value2']));
256+
257+
$subject->save();
258+
259+
$this->assertInstanceOf(Collection::class, $subject->secret_collection);
260+
$this->assertInstanceOf(Fluent::class, $subject->secret_collection->first());
261+
$this->assertSame('value1', $subject->secret_collection->get(0)->key1);
262+
$this->assertSame('value2', $subject->secret_collection->get(1)->key2);
263+
$this->assertDatabaseHas('encrypted_casts', [
264+
'id' => $subject->id,
265+
'secret_collection' => 'encrypted-secret-collection-string-2',
266+
]);
267+
268+
$subject = $subject->fresh();
269+
270+
$this->assertInstanceOf(Collection::class, $subject->secret_collection);
271+
$this->assertInstanceOf(Fluent::class, $subject->secret_collection->first());
272+
$this->assertSame('value1', $subject->secret_collection->get(0)->key1);
273+
$this->assertSame('value2', $subject->secret_collection->get(1)->key2);
274+
275+
$subject->secret_collection = null;
276+
$subject->save();
277+
278+
$this->assertNull($subject->secret_collection);
279+
$this->assertDatabaseHas('encrypted_casts', [
280+
'id' => $subject->id,
281+
'secret_collection' => null,
282+
]);
283+
284+
$this->assertNull($subject->fresh()->secret_collection);
285+
}
286+
234287
public function testAsEncryptedArrayObject()
235288
{
236289
$this->encrypter->expects('encryptString')

0 commit comments

Comments
 (0)