123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510 |
- <?php
- namespace Illuminate\Tests\Integration\Database;
- use Illuminate\Contracts\Database\Eloquent\Castable;
- use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
- use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;
- use Illuminate\Contracts\Database\Eloquent\DeviatesCastableAttributes;
- use Illuminate\Contracts\Database\Eloquent\SerializesCastableAttributes;
- use Illuminate\Database\Eloquent\InvalidCastException;
- use Illuminate\Database\Eloquent\Model;
- use Illuminate\Database\Schema\Blueprint;
- use Illuminate\Support\Carbon;
- use Illuminate\Support\Facades\Schema;
- class DatabaseEloquentModelCustomCastingTest extends DatabaseTestCase
- {
- protected function defineDatabaseMigrationsAfterDatabaseRefreshed()
- {
- Schema::create('test_eloquent_model_with_custom_casts', function (Blueprint $table) {
- $table->increments('id');
- $table->timestamps();
- $table->decimal('price');
- });
- }
- public function testBasicCustomCasting()
- {
- $model = new TestEloquentModelWithCustomCast;
- $model->uppercase = 'taylor';
- $this->assertSame('TAYLOR', $model->uppercase);
- $this->assertSame('TAYLOR', $model->getAttributes()['uppercase']);
- $this->assertSame('TAYLOR', $model->toArray()['uppercase']);
- $unserializedModel = unserialize(serialize($model));
- $this->assertSame('TAYLOR', $unserializedModel->uppercase);
- $this->assertSame('TAYLOR', $unserializedModel->getAttributes()['uppercase']);
- $this->assertSame('TAYLOR', $unserializedModel->toArray()['uppercase']);
- $model->syncOriginal();
- $model->uppercase = 'dries';
- $this->assertSame('TAYLOR', $model->getOriginal('uppercase'));
- $model = new TestEloquentModelWithCustomCast;
- $model->uppercase = 'taylor';
- $model->syncOriginal();
- $model->uppercase = 'dries';
- $model->getOriginal();
- $this->assertSame('DRIES', $model->uppercase);
- $model = new TestEloquentModelWithCustomCast;
- $model->address = $address = new Address('110 Kingsbrook St.', 'My Childhood House');
- $address->lineOne = '117 Spencer St.';
- $this->assertSame('117 Spencer St.', $model->getAttributes()['address_line_one']);
- $model = new TestEloquentModelWithCustomCast;
- $model->setRawAttributes([
- 'address_line_one' => '110 Kingsbrook St.',
- 'address_line_two' => 'My Childhood House',
- ]);
- $this->assertSame('110 Kingsbrook St.', $model->address->lineOne);
- $this->assertSame('My Childhood House', $model->address->lineTwo);
- $this->assertSame('110 Kingsbrook St.', $model->toArray()['address_line_one']);
- $this->assertSame('My Childhood House', $model->toArray()['address_line_two']);
- $model->address->lineOne = '117 Spencer St.';
- $this->assertFalse(isset($model->toArray()['address']));
- $this->assertSame('117 Spencer St.', $model->toArray()['address_line_one']);
- $this->assertSame('My Childhood House', $model->toArray()['address_line_two']);
- $this->assertSame('117 Spencer St.', json_decode($model->toJson(), true)['address_line_one']);
- $this->assertSame('My Childhood House', json_decode($model->toJson(), true)['address_line_two']);
- $model->address = null;
- $this->assertNull($model->toArray()['address_line_one']);
- $this->assertNull($model->toArray()['address_line_two']);
- $model->options = ['foo' => 'bar'];
- $this->assertEquals(['foo' => 'bar'], $model->options);
- $this->assertEquals(['foo' => 'bar'], $model->options);
- $model->options = ['foo' => 'bar'];
- $model->options = ['foo' => 'bar'];
- $this->assertEquals(['foo' => 'bar'], $model->options);
- $this->assertEquals(['foo' => 'bar'], $model->options);
- $this->assertSame(json_encode(['foo' => 'bar']), $model->getAttributes()['options']);
- $model = new TestEloquentModelWithCustomCast(['options' => []]);
- $model->syncOriginal();
- $model->options = ['foo' => 'bar'];
- $this->assertTrue($model->isDirty('options'));
- $model = new TestEloquentModelWithCustomCast;
- $model->birthday_at = now();
- $this->assertIsString($model->toArray()['birthday_at']);
- }
- public function testGetOriginalWithCastValueObjects()
- {
- $model = new TestEloquentModelWithCustomCast([
- 'address' => new Address('110 Kingsbrook St.', 'My Childhood House'),
- ]);
- $model->syncOriginal();
- $model->address = new Address('117 Spencer St.', 'Another house.');
- $this->assertSame('117 Spencer St.', $model->address->lineOne);
- $this->assertSame('110 Kingsbrook St.', $model->getOriginal('address')->lineOne);
- $this->assertSame('117 Spencer St.', $model->address->lineOne);
- $model = new TestEloquentModelWithCustomCast([
- 'address' => new Address('110 Kingsbrook St.', 'My Childhood House'),
- ]);
- $model->syncOriginal();
- $model->address = new Address('117 Spencer St.', 'Another house.');
- $this->assertSame('117 Spencer St.', $model->address->lineOne);
- $this->assertSame('110 Kingsbrook St.', $model->getOriginal()['address_line_one']);
- $this->assertSame('117 Spencer St.', $model->address->lineOne);
- $this->assertSame('110 Kingsbrook St.', $model->getOriginal()['address_line_one']);
- $model = new TestEloquentModelWithCustomCast([
- 'address' => new Address('110 Kingsbrook St.', 'My Childhood House'),
- ]);
- $model->syncOriginal();
- $model->address = null;
- $this->assertNull($model->address);
- $this->assertInstanceOf(Address::class, $model->getOriginal('address'));
- $this->assertNull($model->address);
- }
- public function testDeviableCasts()
- {
- $model = new TestEloquentModelWithCustomCast;
- $model->price = '123.456';
- $model->save();
- $model->increment('price', '530.865');
- $this->assertSame((new Decimal('654.321'))->getValue(), $model->price->getValue());
- $model->decrement('price', '333.333');
- $this->assertSame((new Decimal('320.988'))->getValue(), $model->price->getValue());
- }
- public function testSerializableCasts()
- {
- $model = new TestEloquentModelWithCustomCast;
- $model->price = '123.456';
- $expectedValue = (new Decimal('123.456'))->getValue();
- $this->assertSame($expectedValue, $model->price->getValue());
- $this->assertSame('123.456', $model->getAttributes()['price']);
- $this->assertSame('123.456', $model->toArray()['price']);
- $unserializedModel = unserialize(serialize($model));
- $this->assertSame($expectedValue, $unserializedModel->price->getValue());
- $this->assertSame('123.456', $unserializedModel->getAttributes()['price']);
- $this->assertSame('123.456', $unserializedModel->toArray()['price']);
- }
- public function testOneWayCasting()
- {
- // CastsInboundAttributes is used for casting that is unidirectional... only use case I can think of is one-way hashing...
- $model = new TestEloquentModelWithCustomCast;
- $model->password = 'secret';
- $this->assertEquals(hash('sha256', 'secret'), $model->password);
- $this->assertEquals(hash('sha256', 'secret'), $model->getAttributes()['password']);
- $this->assertEquals(hash('sha256', 'secret'), $model->getAttributes()['password']);
- $this->assertEquals(hash('sha256', 'secret'), $model->password);
- $model->password = 'secret2';
- $this->assertEquals(hash('sha256', 'secret2'), $model->password);
- $this->assertEquals(hash('sha256', 'secret2'), $model->getAttributes()['password']);
- $this->assertEquals(hash('sha256', 'secret2'), $model->getAttributes()['password']);
- $this->assertEquals(hash('sha256', 'secret2'), $model->password);
- }
- public function testSettingRawAttributesClearsTheCastCache()
- {
- $model = new TestEloquentModelWithCustomCast;
- $model->setRawAttributes([
- 'address_line_one' => '110 Kingsbrook St.',
- 'address_line_two' => 'My Childhood House',
- ]);
- $this->assertSame('110 Kingsbrook St.', $model->address->lineOne);
- $model->setRawAttributes([
- 'address_line_one' => '117 Spencer St.',
- 'address_line_two' => 'My Childhood House',
- ]);
- $this->assertSame('117 Spencer St.', $model->address->lineOne);
- }
- public function testWithCastableInterface()
- {
- $model = new TestEloquentModelWithCustomCast;
- $model->setRawAttributes([
- 'value_object_with_caster' => serialize(new ValueObject('hello')),
- ]);
- $this->assertInstanceOf(ValueObject::class, $model->value_object_with_caster);
- $this->assertSame(serialize(new ValueObject('hello')), $model->toArray()['value_object_with_caster']);
- $model->setRawAttributes([
- 'value_object_caster_with_argument' => null,
- ]);
- $this->assertSame('argument', $model->value_object_caster_with_argument);
- $model->setRawAttributes([
- 'value_object_caster_with_caster_instance' => serialize(new ValueObject('hello')),
- ]);
- $this->assertInstanceOf(ValueObject::class, $model->value_object_caster_with_caster_instance);
- }
- public function testGetFromUndefinedCast()
- {
- $this->expectException(InvalidCastException::class);
- $model = new TestEloquentModelWithCustomCast;
- $model->undefined_cast_column;
- }
- public function testSetToUndefinedCast()
- {
- $this->expectException(InvalidCastException::class);
- $model = new TestEloquentModelWithCustomCast;
- $this->assertTrue($model->hasCast('undefined_cast_column'));
- $model->undefined_cast_column = 'Glāžšķūņu rūķīši';
- }
- }
- class TestEloquentModelWithCustomCast 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 = [
- 'address' => AddressCaster::class,
- 'price' => DecimalCaster::class,
- 'password' => HashCaster::class,
- 'other_password' => HashCaster::class.':md5',
- 'uppercase' => UppercaseCaster::class,
- 'options' => JsonCaster::class,
- 'value_object_with_caster' => ValueObject::class,
- 'value_object_caster_with_argument' => ValueObject::class.':argument',
- 'value_object_caster_with_caster_instance' => ValueObjectWithCasterInstance::class,
- 'undefined_cast_column' => UndefinedCast::class,
- 'birthday_at' => DateObjectCaster::class,
- ];
- }
- class HashCaster implements CastsInboundAttributes
- {
- public function __construct($algorithm = 'sha256')
- {
- $this->algorithm = $algorithm;
- }
- public function set($model, $key, $value, $attributes)
- {
- return [$key => hash($this->algorithm, $value)];
- }
- }
- class UppercaseCaster implements CastsAttributes
- {
- public function get($model, $key, $value, $attributes)
- {
- return strtoupper($value);
- }
- public function set($model, $key, $value, $attributes)
- {
- return [$key => strtoupper($value)];
- }
- }
- class AddressCaster implements CastsAttributes
- {
- public function get($model, $key, $value, $attributes)
- {
- if (is_null($attributes['address_line_one'])) {
- return;
- }
- return new Address($attributes['address_line_one'], $attributes['address_line_two']);
- }
- public function set($model, $key, $value, $attributes)
- {
- if (is_null($value)) {
- return [
- 'address_line_one' => null,
- 'address_line_two' => null,
- ];
- }
- return ['address_line_one' => $value->lineOne, 'address_line_two' => $value->lineTwo];
- }
- }
- class JsonCaster implements CastsAttributes
- {
- public function get($model, $key, $value, $attributes)
- {
- return json_decode($value, true);
- }
- public function set($model, $key, $value, $attributes)
- {
- return json_encode($value);
- }
- }
- class DecimalCaster implements CastsAttributes, DeviatesCastableAttributes, SerializesCastableAttributes
- {
- public function get($model, $key, $value, $attributes)
- {
- return new Decimal($value);
- }
- public function set($model, $key, $value, $attributes)
- {
- return (string) $value;
- }
- public function increment($model, $key, $value, $attributes)
- {
- return new Decimal($attributes[$key] + $value);
- }
- public function decrement($model, $key, $value, $attributes)
- {
- return new Decimal($attributes[$key] - $value);
- }
- public function serialize($model, $key, $value, $attributes)
- {
- return (string) $value;
- }
- }
- class ValueObjectCaster implements CastsAttributes
- {
- private $argument;
- public function __construct($argument = null)
- {
- $this->argument = $argument;
- }
- public function get($model, $key, $value, $attributes)
- {
- if ($this->argument) {
- return $this->argument;
- }
- return unserialize($value);
- }
- public function set($model, $key, $value, $attributes)
- {
- return serialize($value);
- }
- }
- class ValueObject implements Castable
- {
- public $name;
- public function __construct(string $name)
- {
- $this->name = $name;
- }
- public static function castUsing(array $arguments)
- {
- return new class(...$arguments) implements CastsAttributes, SerializesCastableAttributes
- {
- private $argument;
- public function __construct($argument = null)
- {
- $this->argument = $argument;
- }
- public function get($model, $key, $value, $attributes)
- {
- if ($this->argument) {
- return $this->argument;
- }
- return unserialize($value);
- }
- public function set($model, $key, $value, $attributes)
- {
- return serialize($value);
- }
- public function serialize($model, $key, $value, $attributes)
- {
- return serialize($value);
- }
- };
- }
- }
- class ValueObjectWithCasterInstance extends ValueObject
- {
- public static function castUsing(array $arguments)
- {
- return new ValueObjectCaster;
- }
- }
- class Address
- {
- public $lineOne;
- public $lineTwo;
- public function __construct($lineOne, $lineTwo)
- {
- $this->lineOne = $lineOne;
- $this->lineTwo = $lineTwo;
- }
- }
- final class Decimal
- {
- private $value;
- private $scale;
- public function __construct($value)
- {
- $parts = explode('.', (string) $value);
- $this->scale = strlen($parts[1]);
- $this->value = (int) str_replace('.', '', $value);
- }
- public function getValue()
- {
- return $this->value;
- }
- public function __toString()
- {
- return substr_replace($this->value, '.', -$this->scale, 0);
- }
- }
- class DateObjectCaster implements CastsAttributes
- {
- private $argument;
- public function __construct($argument = null)
- {
- $this->argument = $argument;
- }
- public function get($model, $key, $value, $attributes)
- {
- return Carbon::parse($value);
- }
- public function set($model, $key, $value, $attributes)
- {
- return $value->format('Y-m-d');
- }
- }
|