From 87b7117c89fa12ec079722309ecae76135decbc5 Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Sat, 12 Apr 2025 01:21:46 -0400 Subject: [PATCH 1/7] [12.x] Adds Collection mapping into classes --- .../Eloquent/Casts/AsCollectionMap.php | 101 ++++++++++++++++++ .../Casts/AsEncryptedCollectionMap.php | 70 ++++++++++++ .../Eloquent/Concerns/HasAttributes.php | 6 +- .../Database/DatabaseCustomCastsTest.php | 87 +++++++++++++++ .../EloquentModelEncryptedCastingTest.php | 52 +++++++++ types/Database/Eloquent/Casts/Castable.php | 30 ++++++ 6 files changed, 344 insertions(+), 2 deletions(-) create mode 100644 src/Illuminate/Database/Eloquent/Casts/AsCollectionMap.php create mode 100644 src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollectionMap.php diff --git a/src/Illuminate/Database/Eloquent/Casts/AsCollectionMap.php b/src/Illuminate/Database/Eloquent/Casts/AsCollectionMap.php new file mode 100644 index 000000000000..6a5e7d387244 --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Casts/AsCollectionMap.php @@ -0,0 +1,101 @@ +|(callable(mixed):TValue), 1?: string} $arguments + * @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection, iterable> + */ + public static function castUsing(array $arguments) + { + return new class($arguments) implements CastsAttributes + { + public function __construct(protected array $arguments) + { + } + + public function get($model, $key, $value, $attributes) + { + if (! isset($attributes[$key])) { + return; + } + + $data = Json::decode($attributes[$key]); + + if (!is_array($data)) { + return null; + } + + $this->arguments[0] ??= ''; + + if (is_callable($this->arguments[0])) { + return Collection::make($data)->map($this->arguments[0]); + } + + [$class, $method] = Str::parseCallback($this->arguments[0]); + + if ($method) { + return Collection::make($data)->map([$class, $method]); + } + + if ($class) { + return Collection::make($data)->mapInto($class); + } + + throw new InvalidArgumentException('No class or callable has been set to map the Collection.'); + } + + public function set($model, $key, $value, $attributes) + { + return [$key => Json::encode($value)]; + } + }; + } + + /** + * Specify the class to map into each item in the Collection cast. + * + * @param class-string $class + * @return string + */ + public static function into($class) + { + return static::class.':'.$class; + } + + /** + * Specify the callable to map each item in the Collection cast. + * + * @param callable-string|array{0: class-string, 1: string} $callback + * @param string|null $method + * @return string + */ + public static function using($callback, $method = null) + { + if ($callback instanceof Closure) { + throw new InvalidArgumentException('The provided callback should be a callable array or string.'); + } + + if (is_array($callback) && is_callable($callback)) { + [$callback, $method] = [$callback[0], $callback[1]]; + } + + return $method === null + ? static::class.':'.$callback + : static::class.':'.$callback.'@'.$method; + + } +} diff --git a/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollectionMap.php b/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollectionMap.php new file mode 100644 index 000000000000..7d52df926df3 --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollectionMap.php @@ -0,0 +1,70 @@ +|(callable(mixed):TValue)} $arguments + * @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection, iterable> + */ + public static function castUsing(array $arguments) + { + return new class($arguments) implements CastsAttributes + { + public function __construct(protected array $arguments) + { + } + + public function get($model, $key, $value, $attributes) + { + if (! isset($attributes[$key])) { + return; + } + + $data = Json::decode(Crypt::decryptString($attributes[$key])); + + if (!is_array($data)) { + return null; + } + + $this->arguments[0] ??= ''; + + if (is_callable($this->arguments[0])) { + return Collection::make($data)->map($this->arguments[0]); + } + + [$class, $method] = Str::parseCallback($this->arguments[0]); + + if ($method) { + return Collection::make($data)->map([$class, $method]); + } + + if ($class) { + return Collection::make($data)->mapInto($class); + } + + throw new InvalidArgumentException('No class or callable has been set to map the Collection.'); + } + + public function set($model, $key, $value, $attributes) + { + if (! is_null($value)) { + return [$key => Crypt::encryptString(Json::encode($value))]; + } + + return null; + } + }; + } +} diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 0d0fc454bf0b..92d028fe5cae 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -15,8 +15,10 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Database\Eloquent\Casts\AsArrayObject; use Illuminate\Database\Eloquent\Casts\AsCollection; +use Illuminate\Database\Eloquent\Casts\AsCollectionMap; use Illuminate\Database\Eloquent\Casts\AsEncryptedArrayObject; use Illuminate\Database\Eloquent\Casts\AsEncryptedCollection; +use Illuminate\Database\Eloquent\Casts\AsEncryptedCollectionMap; use Illuminate\Database\Eloquent\Casts\AsEnumArrayObject; use Illuminate\Database\Eloquent\Casts\AsEnumCollection; use Illuminate\Database\Eloquent\Casts\Attribute; @@ -2237,11 +2239,11 @@ public function originalIsEquivalent($key) } elseif ($this->hasCast($key, static::$primitiveCastTypes)) { return $this->castAttribute($key, $attribute) === $this->castAttribute($key, $original); - } elseif ($this->isClassCastable($key) && Str::startsWith($this->getCasts()[$key], [AsArrayObject::class, AsCollection::class])) { + } elseif ($this->isClassCastable($key) && Str::startsWith($this->getCasts()[$key], [AsArrayObject::class, AsCollection::class, AsCollectionMap::class])) { return $this->fromJson($attribute) === $this->fromJson($original); } elseif ($this->isClassCastable($key) && Str::startsWith($this->getCasts()[$key], [AsEnumArrayObject::class, AsEnumCollection::class])) { return $this->fromJson($attribute) === $this->fromJson($original); - } elseif ($this->isClassCastable($key) && $original !== null && Str::startsWith($this->getCasts()[$key], [AsEncryptedArrayObject::class, AsEncryptedCollection::class])) { + } elseif ($this->isClassCastable($key) && $original !== null && Str::startsWith($this->getCasts()[$key], [AsEncryptedArrayObject::class, AsEncryptedCollection::class, AsEncryptedCollectionMap::class])) { if (empty(static::currentEncrypter()->getPreviousKeys())) { return $this->fromEncryptedString($attribute) === $this->fromEncryptedString($original); } diff --git a/tests/Integration/Database/DatabaseCustomCastsTest.php b/tests/Integration/Database/DatabaseCustomCastsTest.php index 0337edf26dcc..1842907fea2e 100644 --- a/tests/Integration/Database/DatabaseCustomCastsTest.php +++ b/tests/Integration/Database/DatabaseCustomCastsTest.php @@ -4,12 +4,16 @@ use Illuminate\Database\Eloquent\Casts\AsArrayObject; use Illuminate\Database\Eloquent\Casts\AsCollection; +use Illuminate\Database\Eloquent\Casts\AsCollectionMap; use Illuminate\Database\Eloquent\Casts\AsStringable; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Schema; +use Illuminate\Support\Fluent; use Illuminate\Support\Stringable; +use InvalidArgumentException; +use PHPUnit\Framework\Attributes\DataProvider; class DatabaseCustomCastsTest extends DatabaseTestCase { @@ -151,6 +155,74 @@ public function test_custom_casting_nullable_values() $model->array_object_json->toArray() ); } + + public function test_custom_casting_map_into_collection(): void + { + $model = new TestEloquentModelWithCustomCastsNullable(); + $model->mergeCasts(['collection' => AsCollectionMap::into(Fluent::class)]); + $model->fill([ + 'collection' => [ + ['name' => 'Taylor'] + ] + ]); + + $fluent = $model->collection->first(); + + $this->assertInstanceOf(Fluent::class, $fluent); + $this->assertSame('Taylor', $fluent->name); + } + + public static function provideMapArguments() + { + return [ + [TestCollectionMapCallable::class], + [TestCollectionMapCallable::class, 'make'], + [TestCollectionMapCallable::class . '@' .'make'], + ]; + } + + #[DataProvider('provideMapArguments')] + public function test_custom_casting_map_collection($class, $method = null): void + { + $model = new TestEloquentModelWithCustomCastsNullable(); + $model->mergeCasts(['collection' => AsCollectionMap::using($class, $method)]); + $model->fill([ + 'collection' => [ + ['name' => 'Taylor'] + ] + ]); + + $result = $model->collection->first(); + + $this->assertInstanceOf(TestCollectionMapCallable::class, $result); + $this->assertSame('Taylor', $result->name); + } + + public function test_custom_casting_map_collection_throw_when_no_arguments(): void + { + $model = new TestEloquentModelWithCustomCastsNullable(); + $model->mergeCasts(['collection' => AsCollectionMap::class]); + $model->fill([ + 'collection' => [ + ['name' => 'Taylor'] + ] + ]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('No class or callable has been set to map the Collection.'); + + $model->collection->first(); + } + + public function test_custom_casting_map_collection_throw_when_using_closure(): void + { + $model = new TestEloquentModelWithCustomCastsNullable(); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The provided callback should be a callable array or string.'); + + $model->mergeCasts(['collection' => AsCollectionMap::using(TestCollectionMapCallable::make(...))]); + } } class TestEloquentModelWithCustomCasts extends Model @@ -197,3 +269,18 @@ class TestEloquentModelWithCustomCastsNullable extends Model 'stringable' => AsStringable::class, ]; } + +class TestCollectionMapCallable +{ + public $name; + + public function __construct($data) + { + $this->name = $data['name']; + } + + public static function make($data) + { + return new static($data); + } +} diff --git a/tests/Integration/Database/EloquentModelEncryptedCastingTest.php b/tests/Integration/Database/EloquentModelEncryptedCastingTest.php index f4a00d3c98ca..d8c98accb802 100644 --- a/tests/Integration/Database/EloquentModelEncryptedCastingTest.php +++ b/tests/Integration/Database/EloquentModelEncryptedCastingTest.php @@ -6,11 +6,13 @@ use Illuminate\Database\Eloquent\Casts\ArrayObject; use Illuminate\Database\Eloquent\Casts\AsEncryptedArrayObject; use Illuminate\Database\Eloquent\Casts\AsEncryptedCollection; +use Illuminate\Database\Eloquent\Casts\AsEncryptedCollectionMap; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Schema; +use Illuminate\Support\Fluent; use stdClass; class EloquentModelEncryptedCastingTest extends DatabaseTestCase @@ -231,6 +233,56 @@ public function testAsEncryptedCollection() $this->assertNull($subject->fresh()->secret_collection); } + public function testAsEncryptedCollectionMap() + { + $this->encrypter->expects('encryptString') + ->twice() + ->with('[{"key1":"value1"}]') + ->andReturn('encrypted-secret-collection-string-1'); + $this->encrypter->expects('encryptString') + ->times(10) + ->with('[{"key1":"value1"},{"key2":"value2"}]') + ->andReturn('encrypted-secret-collection-string-2'); + $this->encrypter->expects('decryptString') + ->once() + ->with('encrypted-secret-collection-string-2') + ->andReturn('[{"key1":"value1"},{"key2":"value2"}]'); + + $subject = new EncryptedCast; + + $subject->mergeCasts(['secret_collection' => AsEncryptedCollectionMap::into(Fluent::class)]); + + $subject->secret_collection = new Collection([new Fluent(['key1' => 'value1'])]); + $subject->secret_collection->push(new Fluent(['key2' => 'value2'])); + + $subject->save(); + + $this->assertInstanceOf(Collection::class, $subject->secret_collection); + $this->assertSame('value1', $subject->secret_collection->get(0)->key1); + $this->assertSame('value2', $subject->secret_collection->get(1)->key2); + $this->assertDatabaseHas('encrypted_casts', [ + 'id' => $subject->id, + 'secret_collection' => 'encrypted-secret-collection-string-2', + ]); + + $subject = $subject->fresh(); + + $this->assertInstanceOf(Collection::class, $subject->secret_collection); + $this->assertSame('value1', $subject->secret_collection->get(0)->key1); + $this->assertSame('value2', $subject->secret_collection->get(1)->key2); + + $subject->secret_collection = null; + $subject->save(); + + $this->assertNull($subject->secret_collection); + $this->assertDatabaseHas('encrypted_casts', [ + 'id' => $subject->id, + 'secret_collection' => null, + ]); + + $this->assertNull($subject->fresh()->secret_collection); + } + public function testAsEncryptedArrayObject() { $this->encrypter->expects('encryptString') diff --git a/types/Database/Eloquent/Casts/Castable.php b/types/Database/Eloquent/Casts/Castable.php index bbb934cbe4ee..e8047db6b618 100644 --- a/types/Database/Eloquent/Casts/Castable.php +++ b/types/Database/Eloquent/Casts/Castable.php @@ -12,6 +12,21 @@ \Illuminate\Database\Eloquent\Casts\AsCollection::castUsing([]), ); +assertType( + '\Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection, iterable>', + \Illuminate\Database\Eloquent\Casts\AsCollectionMap::castUsing([\Post::class]), +); + +assertType( + '\Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection, iterable>', + \Illuminate\Database\Eloquent\Casts\AsCollectionMap::castUsing([fn(): \Post => new \Post]), +); + +assertType( + '\Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection, iterable>', + \Illuminate\Database\Eloquent\Casts\AsCollectionMap::castUsing([\Post::class, 'make']), +); + assertType( 'Illuminate\Contracts\Database\Eloquent\CastsAttributes, iterable>', \Illuminate\Database\Eloquent\Casts\AsEncryptedArrayObject::castUsing([]), @@ -22,6 +37,21 @@ \Illuminate\Database\Eloquent\Casts\AsEncryptedCollection::castUsing([]), ); +assertType( + '\Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection, iterable>', + \Illuminate\Database\Eloquent\Casts\AsEncryptedCollectionMap::castUsing([\Post::class]), +); + +assertType( + '\Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection, iterable>', + \Illuminate\Database\Eloquent\Casts\AsEncryptedCollectionMap::castUsing([fn(): \Post => new \Post]), +); + +assertType( + '\Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection, iterable>', + \Illuminate\Database\Eloquent\Casts\AsEncryptedCollectionMap::castUsing([\Post::class, 'make']), +); + assertType( 'Illuminate\Contracts\Database\Eloquent\CastsAttributes, iterable>', \Illuminate\Database\Eloquent\Casts\AsEnumArrayObject::castUsing([\UserType::class]), From 9a2e7e25896c33088785504a3a327e139feee7dd Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Sat, 12 Apr 2025 01:41:32 -0400 Subject: [PATCH 2/7] Style fixes --- .../Database/Eloquent/Casts/AsCollectionMap.php | 7 +++---- .../Eloquent/Casts/AsEncryptedCollectionMap.php | 2 +- .../Database/DatabaseCustomCastsTest.php | 14 +++++++------- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Casts/AsCollectionMap.php b/src/Illuminate/Database/Eloquent/Casts/AsCollectionMap.php index 6a5e7d387244..96845936fa0d 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsCollectionMap.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsCollectionMap.php @@ -35,7 +35,7 @@ public function get($model, $key, $value, $attributes) $data = Json::decode($attributes[$key]); - if (!is_array($data)) { + if (! is_array($data)) { return null; } @@ -79,8 +79,8 @@ public static function into($class) /** * Specify the callable to map each item in the Collection cast. * - * @param callable-string|array{0: class-string, 1: string} $callback - * @param string|null $method + * @param callable-string|array{0: class-string, 1: string} $callback + * @param string|null $method * @return string */ public static function using($callback, $method = null) @@ -96,6 +96,5 @@ public static function using($callback, $method = null) return $method === null ? static::class.':'.$callback : static::class.':'.$callback.'@'.$method; - } } diff --git a/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollectionMap.php b/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollectionMap.php index 7d52df926df3..96667d5d45c6 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollectionMap.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollectionMap.php @@ -34,7 +34,7 @@ public function get($model, $key, $value, $attributes) $data = Json::decode(Crypt::decryptString($attributes[$key])); - if (!is_array($data)) { + if (! is_array($data)) { return null; } diff --git a/tests/Integration/Database/DatabaseCustomCastsTest.php b/tests/Integration/Database/DatabaseCustomCastsTest.php index 1842907fea2e..8a637dcc711f 100644 --- a/tests/Integration/Database/DatabaseCustomCastsTest.php +++ b/tests/Integration/Database/DatabaseCustomCastsTest.php @@ -162,8 +162,8 @@ public function test_custom_casting_map_into_collection(): void $model->mergeCasts(['collection' => AsCollectionMap::into(Fluent::class)]); $model->fill([ 'collection' => [ - ['name' => 'Taylor'] - ] + ['name' => 'Taylor'], + ], ]); $fluent = $model->collection->first(); @@ -177,7 +177,7 @@ public static function provideMapArguments() return [ [TestCollectionMapCallable::class], [TestCollectionMapCallable::class, 'make'], - [TestCollectionMapCallable::class . '@' .'make'], + [TestCollectionMapCallable::class.'@'.'make'], ]; } @@ -188,8 +188,8 @@ public function test_custom_casting_map_collection($class, $method = null): void $model->mergeCasts(['collection' => AsCollectionMap::using($class, $method)]); $model->fill([ 'collection' => [ - ['name' => 'Taylor'] - ] + ['name' => 'Taylor'], + ], ]); $result = $model->collection->first(); @@ -204,8 +204,8 @@ public function test_custom_casting_map_collection_throw_when_no_arguments(): vo $model->mergeCasts(['collection' => AsCollectionMap::class]); $model->fill([ 'collection' => [ - ['name' => 'Taylor'] - ] + ['name' => 'Taylor'], + ], ]); $this->expectException(InvalidArgumentException::class); From b4ac245811321b980fdf32a8fa8f3412b86b64d2 Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Sat, 12 Apr 2025 01:41:41 -0400 Subject: [PATCH 3/7] Fixes static analysis --- types/Database/Eloquent/Casts/Castable.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/types/Database/Eloquent/Casts/Castable.php b/types/Database/Eloquent/Casts/Castable.php index e8047db6b618..60bf452722ad 100644 --- a/types/Database/Eloquent/Casts/Castable.php +++ b/types/Database/Eloquent/Casts/Castable.php @@ -13,17 +13,17 @@ ); assertType( - '\Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection, iterable>', + 'Illuminate\Contracts\Database\Eloquent\CastsAttributes, iterable>', \Illuminate\Database\Eloquent\Casts\AsCollectionMap::castUsing([\Post::class]), ); assertType( - '\Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection, iterable>', - \Illuminate\Database\Eloquent\Casts\AsCollectionMap::castUsing([fn(): \Post => new \Post]), + 'Illuminate\Contracts\Database\Eloquent\CastsAttributes, iterable>', + \Illuminate\Database\Eloquent\Casts\AsCollectionMap::castUsing([fn (): \Post => new \Post]), ); assertType( - '\Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection, iterable>', + 'Illuminate\Contracts\Database\Eloquent\CastsAttributes, iterable>', \Illuminate\Database\Eloquent\Casts\AsCollectionMap::castUsing([\Post::class, 'make']), ); @@ -38,17 +38,17 @@ ); assertType( - '\Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection, iterable>', + 'Illuminate\Contracts\Database\Eloquent\CastsAttributes, iterable>', \Illuminate\Database\Eloquent\Casts\AsEncryptedCollectionMap::castUsing([\Post::class]), ); assertType( - '\Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection, iterable>', - \Illuminate\Database\Eloquent\Casts\AsEncryptedCollectionMap::castUsing([fn(): \Post => new \Post]), + 'Illuminate\Contracts\Database\Eloquent\CastsAttributes, iterable>', + \Illuminate\Database\Eloquent\Casts\AsEncryptedCollectionMap::castUsing([fn (): \Post => new \Post]), ); assertType( - '\Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection, iterable>', + 'Illuminate\Contracts\Database\Eloquent\CastsAttributes, iterable>', \Illuminate\Database\Eloquent\Casts\AsEncryptedCollectionMap::castUsing([\Post::class, 'make']), ); From 990aaa4bc62ad141735dd4aaf2e58d025bcc95c9 Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Sat, 12 Apr 2025 14:21:56 -0400 Subject: [PATCH 4/7] Refactoring --- .../Eloquent/Casts/AsCollectionMap.php | 36 +++++++++------ .../Casts/AsEncryptedCollectionMap.php | 46 ++++++++++--------- 2 files changed, 46 insertions(+), 36 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Casts/AsCollectionMap.php b/src/Illuminate/Database/Eloquent/Casts/AsCollectionMap.php index 96845936fa0d..93f8a1d4cc4f 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsCollectionMap.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsCollectionMap.php @@ -23,8 +23,22 @@ public static function castUsing(array $arguments) { return new class($arguments) implements CastsAttributes { - public function __construct(protected array $arguments) + protected $callable; + + public function __construct(array $arguments) { + $arguments = array_values($arguments); + + if (empty($arguments) || empty($arguments[0])) { + throw new InvalidArgumentException('No class or callable has been set to map the Collection.'); + } + + if (isset($arguments[1]) && ! is_array($arguments[0])) { + $arguments = [$arguments[0], $arguments[1]]; + unset($arguments[1]); + } + + $this->callable = $arguments[0]; } public function get($model, $key, $value, $attributes) @@ -39,23 +53,15 @@ public function get($model, $key, $value, $attributes) return null; } - $this->arguments[0] ??= ''; - - if (is_callable($this->arguments[0])) { - return Collection::make($data)->map($this->arguments[0]); + if (is_callable($this->callable)) { + return Collection::make($data)->map($this->callable); } - [$class, $method] = Str::parseCallback($this->arguments[0]); - - if ($method) { - return Collection::make($data)->map([$class, $method]); - } - - if ($class) { - return Collection::make($data)->mapInto($class); - } + [$class, $method] = Str::parseCallback($this->callable); - throw new InvalidArgumentException('No class or callable has been set to map the Collection.'); + return $method + ? Collection::make($data)->map([$class, $method]) + : Collection::make($data)->mapInto($class); } public function set($model, $key, $value, $attributes) diff --git a/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollectionMap.php b/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollectionMap.php index 96667d5d45c6..406e8de1c572 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollectionMap.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollectionMap.php @@ -22,14 +22,28 @@ public static function castUsing(array $arguments) { return new class($arguments) implements CastsAttributes { - public function __construct(protected array $arguments) + protected $callable; + + public function __construct(array $arguments) { + $arguments = array_values($arguments); + + if (empty($arguments) || empty($arguments[0])) { + throw new InvalidArgumentException('No class or callable has been set to map the Collection.'); + } + + if (isset($arguments[1]) && ! is_array($arguments[1])) { + $arguments = [$arguments[0], $arguments[1]]; + unset($arguments[1]); + } + + $this->callable = $arguments[0]; } public function get($model, $key, $value, $attributes) { if (! isset($attributes[$key])) { - return; + return null; } $data = Json::decode(Crypt::decryptString($attributes[$key])); @@ -38,32 +52,22 @@ public function get($model, $key, $value, $attributes) return null; } - $this->arguments[0] ??= ''; - - if (is_callable($this->arguments[0])) { - return Collection::make($data)->map($this->arguments[0]); + if (is_callable($this->callable)) { + return Collection::make($data)->map($this->callable); } - [$class, $method] = Str::parseCallback($this->arguments[0]); - - if ($method) { - return Collection::make($data)->map([$class, $method]); - } + [$class, $method] = Str::parseCallback($this->callable); - if ($class) { - return Collection::make($data)->mapInto($class); - } - - throw new InvalidArgumentException('No class or callable has been set to map the Collection.'); + return $method + ? Collection::make($data)->map([$class, $method]) + : Collection::make($data)->mapInto($class); } public function set($model, $key, $value, $attributes) { - if (! is_null($value)) { - return [$key => Crypt::encryptString(Json::encode($value))]; - } - - return null; + return is_null($value) + ? null + : [$key => Crypt::encryptString(Json::encode($value))]; } }; } From dcd072ef34fafdf3afa74b6c5f2023b1cfb92be4 Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Sat, 12 Apr 2025 15:56:26 -0400 Subject: [PATCH 5/7] Fixes test --- tests/Integration/Database/DatabaseCustomCastsTest.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/Integration/Database/DatabaseCustomCastsTest.php b/tests/Integration/Database/DatabaseCustomCastsTest.php index 8a637dcc711f..8f3d851112cf 100644 --- a/tests/Integration/Database/DatabaseCustomCastsTest.php +++ b/tests/Integration/Database/DatabaseCustomCastsTest.php @@ -202,16 +202,15 @@ public function test_custom_casting_map_collection_throw_when_no_arguments(): vo { $model = new TestEloquentModelWithCustomCastsNullable(); $model->mergeCasts(['collection' => AsCollectionMap::class]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('No class or callable has been set to map the Collection.'); + $model->fill([ 'collection' => [ ['name' => 'Taylor'], ], ]); - - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('No class or callable has been set to map the Collection.'); - - $model->collection->first(); } public function test_custom_casting_map_collection_throw_when_using_closure(): void From 5c6fd28ab2fe63c3a38a08c52d092f6811b2fdcb Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Sat, 12 Apr 2025 16:52:45 -0400 Subject: [PATCH 6/7] Removed brainfart --- .../Database/Eloquent/Casts/AsCollectionMap.php | 9 +++------ .../Database/Eloquent/Casts/AsEncryptedCollectionMap.php | 9 +++------ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Casts/AsCollectionMap.php b/src/Illuminate/Database/Eloquent/Casts/AsCollectionMap.php index 93f8a1d4cc4f..63ef5ccd514b 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsCollectionMap.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsCollectionMap.php @@ -33,12 +33,9 @@ public function __construct(array $arguments) throw new InvalidArgumentException('No class or callable has been set to map the Collection.'); } - if (isset($arguments[1]) && ! is_array($arguments[0])) { - $arguments = [$arguments[0], $arguments[1]]; - unset($arguments[1]); - } - - $this->callable = $arguments[0]; + $this->callable = isset($arguments[1]) && ! is_array($arguments[0]) + ? [$arguments[0], $arguments[1]] + : $arguments[0]; } public function get($model, $key, $value, $attributes) diff --git a/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollectionMap.php b/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollectionMap.php index 406e8de1c572..994a8a548640 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollectionMap.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollectionMap.php @@ -32,12 +32,9 @@ public function __construct(array $arguments) throw new InvalidArgumentException('No class or callable has been set to map the Collection.'); } - if (isset($arguments[1]) && ! is_array($arguments[1])) { - $arguments = [$arguments[0], $arguments[1]]; - unset($arguments[1]); - } - - $this->callable = $arguments[0]; + $this->callable = isset($arguments[1]) && ! is_array($arguments[0]) + ? [$arguments[0], $arguments[1]] + : $arguments[0]; } public function get($model, $key, $value, $attributes) From 697512413133b6b5ff08a92633407bb6d601986a Mon Sep 17 00:00:00 2001 From: Italo Israel Baeza Cabrera Date: Sat, 12 Apr 2025 16:54:47 -0400 Subject: [PATCH 7/7] Added missing helper static methods. --- .../Casts/AsEncryptedCollectionMap.php | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollectionMap.php b/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollectionMap.php index 994a8a548640..8964cf544c93 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollectionMap.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollectionMap.php @@ -68,4 +68,37 @@ public function set($model, $key, $value, $attributes) } }; } + + /** + * Specify the class to map into each item in the Collection cast. + * + * @param class-string $class + * @return string + */ + public static function into($class) + { + return static::class.':'.$class; + } + + /** + * Specify the callable to map each item in the Collection cast. + * + * @param callable-string|array{0: class-string, 1: string} $callback + * @param string|null $method + * @return string + */ + public static function using($callback, $method = null) + { + if ($callback instanceof Closure) { + throw new InvalidArgumentException('The provided callback should be a callable array or string.'); + } + + if (is_array($callback) && is_callable($callback)) { + [$callback, $method] = [$callback[0], $callback[1]]; + } + + return $method === null + ? static::class.':'.$callback + : static::class.':'.$callback.'@'.$method; + } }