Skip to content

Commit fd91472

Browse files
[12.x] Extends AsCollection for map
1 parent 64fb35b commit fd91472

File tree

4 files changed

+208
-10
lines changed

4 files changed

+208
-10
lines changed

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

Lines changed: 40 additions & 4 deletions
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,31 @@ 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 (!$this->arguments[1]) {
49+
return $instance;
50+
}
51+
52+
if (is_callable($this->arguments[1])) {
53+
return $instance->map($this->arguments[1]);
54+
}
55+
56+
[$class, $method] = Str::parseCallback($this->arguments[1]);
57+
58+
return $method
59+
? $instance->map([$class, $method])
60+
: $instance->mapInto($class);
4161
}
4262

4363
public function set($model, $key, $value, $attributes)
@@ -51,10 +71,26 @@ public function set($model, $key, $value, $attributes)
5171
* Specify the collection for the cast.
5272
*
5373
* @param class-string $class
74+
* @param array{class-string, string}|class-string $map
75+
* @return string
76+
*/
77+
public static function using($class, $map = null)
78+
{
79+
if (is_array($map) && is_callable($map)) {
80+
$map = $map[0].'@'.$map[1];
81+
}
82+
83+
return static::class.':'.implode(',', [$class, $map]);
84+
}
85+
86+
/**
87+
* Specify the callback to map each item.
88+
*
89+
* @param array{class-string, string}|class-string $map
5490
* @return string
5591
*/
56-
public static function using($class)
92+
public static function map($map)
5793
{
58-
return static::class.':'.$class;
94+
return static::using('', $map);
5995
}
6096
}

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

Lines changed: 38 additions & 6 deletions
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,36 @@ 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 (!$this->arguments[1]) {
44+
return $instance;
45+
}
46+
47+
if (is_callable($this->arguments[1])) {
48+
return $instance->map($this->arguments[1]);
49+
}
50+
51+
[$class, $method] = Str::parseCallback($this->arguments[1]);
52+
53+
return $method
54+
? $instance->map([$class, $method])
55+
: $instance->mapInto($class);
4056
}
4157

4258
public function set($model, $key, $value, $attributes)
@@ -54,10 +70,26 @@ public function set($model, $key, $value, $attributes)
5470
* Specify the collection for the cast.
5571
*
5672
* @param class-string $class
73+
* @param array{class-string, string}|class-string $map
74+
* @return string
75+
*/
76+
public static function using($class, $map = null)
77+
{
78+
if (is_array($map) && is_callable($map)) {
79+
$map = $map[0].'@'.$map[1];
80+
}
81+
82+
return static::class.':'.implode(',', [$class, $map]);
83+
}
84+
85+
/**
86+
* Specify the callback to map each item.
87+
*
88+
* @param array{class-string, string}|class-string $map
5789
* @return string
5890
*/
59-
public static function using($class)
91+
public static function map($map)
6092
{
61-
return static::class.':'.$class;
93+
return static::using('', $map);
6294
}
6395
}

tests/Integration/Database/DatabaseCustomCastsTest.php

Lines changed: 77 additions & 0 deletions
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::map(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::map([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,16 @@ class TestEloquentModelWithCustomCastsNullable extends Model
197261
'stringable' => AsStringable::class,
198262
];
199263
}
264+
265+
class FluentWithCallback extends Fluent
266+
{
267+
public static function make(array $array)
268+
{
269+
return new static($array);
270+
}
271+
}
272+
273+
class CustomCollection extends Collection
274+
{
275+
276+
}

tests/Integration/Database/EloquentModelEncryptedCastingTest.php

Lines changed: 53 additions & 0 deletions
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::map(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)