Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] ArrayObject + Collection Custom Casts #36245

Merged
merged 5 commits into from
Feb 13, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/Illuminate/Database/Eloquent/Casts/ArrayObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

namespace Illuminate\Database\Eloquent\Casts;

use ArrayObject as BaseArrayObject;
use Illuminate\Contracts\Support\Arrayable;
use JsonSerializable;

class ArrayObject extends BaseArrayObject implements Arrayable, JsonSerializable
{
/**
* Get a collection containing the underlying array.
*
* @return \Illuminate\Support\Collection
*/
public function collect()
{
return collect($this->getArrayCopy());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the ArrayObject is already Traversable, you don't need to get an ->getArrayCopy() here.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe one of my project is exactly waiting for that

}

/**
* Get the instance as an array.
*
* @return array
*/
public function toArray()
{
return $this->getArrayCopy();
}

/**
* Get the array that should be JSON serialized.
*
* @return array
*/
public function jsonSerialize()
{
return $this->getArrayCopy();
}
}
35 changes: 35 additions & 0 deletions src/Illuminate/Database/Eloquent/Casts/AsArrayObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace Illuminate\Database\Eloquent\Casts;

use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;

class AsArrayObject implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @param array $arguments
* @return object|string
*/
public static function castUsing(array $arguments)
{
return new class implements CastsAttributes {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This anonymous class makes me feel warm and fuzzy

public function get($model, $key, $value, $attributes)
{
return new ArrayObject(json_decode($attributes[$key], true));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArrayObject construct will only take an array, or object. json_decode() can possibly return bool, null among others.

(also worth noting json_decode() does not handle invalid json, which you either have to use the exception flag for, or json_last_error())

https://www.php.net/manual/en/function.json-decode.php#refsect1-function.json-decode-returnvalues

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then PHP will throw an error if it is not an error - which is what I would want to happen in this case. The column assigned this cast should always contain something that can be decoded to an array.

Copy link

@honzahana honzahana Feb 18, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is bad. Throws an exception web DB column is nullable (and item is null).

}

public function set($model, $key, $value, $attributes)
{
return [$key => json_encode($value)];
}

public function serialize($model, string $key, $value, array $attributes)
{
return $value->getArrayCopy();
}
};
}
}
31 changes: 31 additions & 0 deletions src/Illuminate/Database/Eloquent/Casts/AsCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Illuminate\Database\Eloquent\Casts;

use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Collection;

class AsCollection implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @param array $arguments
* @return object|string
*/
public static function castUsing(array $arguments)
{
return new class implements CastsAttributes {
public function get($model, $key, $value, $attributes)
{
return new Collection(json_decode($attributes[$key], true));
}

public function set($model, $key, $value, $attributes)
{
return [$key => json_encode($value)];
}
};
}
}
36 changes: 36 additions & 0 deletions src/Illuminate/Database/Eloquent/Casts/AsEncryptedArrayObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Illuminate\Database\Eloquent\Casts;

use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Facades\Crypt;

class AsEncryptedArrayObject implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @param array $arguments
* @return object|string
*/
public static function castUsing(array $arguments)
{
return new class implements CastsAttributes {
public function get($model, $key, $value, $attributes)
{
return new ArrayObject(json_decode(Crypt::decryptString($attributes[$key]), true));
}

public function set($model, $key, $value, $attributes)
{
return [$key => Crypt::encryptString(json_encode($value))];
}

public function serialize($model, string $key, $value, array $attributes)
{
return $value->getArrayCopy();
}
};
}
}
32 changes: 32 additions & 0 deletions src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Illuminate\Database\Eloquent\Casts;

use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Crypt;

class AsEncryptedCollection implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @param array $arguments
* @return object|string
*/
public static function castUsing(array $arguments)
{
return new class implements CastsAttributes {
public function get($model, $key, $value, $attributes)
{
return new Collection(json_decode(Crypt::decryptString($attributes[$key]), true));
}

public function set($model, $key, $value, $attributes)
{
return [$key => Crypt::encryptString(json_encode($value))];
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

namespace Illuminate\Tests\Integration\Database;

use Illuminate\Database\Eloquent\Casts\AsArrayObject;
use Illuminate\Database\Eloquent\Casts\AsCollection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

/**
* @group integration
*/
class DatabaseArrayObjectAndCollectionCustomCastTest extends DatabaseTestCase
{
protected function setUp(): void
{
parent::setUp();

Schema::create('test_eloquent_model_with_custom_array_object_casts', function (Blueprint $table) {
$table->increments('id');
$table->text('array_object');
$table->text('collection');
$table->timestamps();
});
}

public function test_array_object_and_collection_casting()
{
$model = new TestEloquentModelWithCustomArrayObjectCast;

$model->array_object = ['name' => 'Taylor'];
$model->collection = collect(['name' => 'Taylor']);

$model->save();

$model = $model->fresh();

$this->assertEquals(['name' => 'Taylor'], $model->array_object->toArray());
$this->assertEquals(['name' => 'Taylor'], $model->collection->toArray());

$model->array_object['age'] = 34;
$model->array_object['meta']['title'] = 'Developer';

$model->save();

$model = $model->fresh();

$this->assertEquals([
'name' => 'Taylor',
'age' => 34,
'meta' => ['title' => 'Developer'],
], $model->array_object->toArray());
}
}

class TestEloquentModelWithCustomArrayObjectCast extends Model
{
/**
* The attributes that aren't mass assignable.
*
* @var string[]
*/
protected $guarded = [];

/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'array_object' => AsArrayObject::class,
'collection' => AsCollection::class,
];
}