DatabaseEloquentModelCustomCastingTest.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. <?php
  2. namespace Illuminate\Tests\Integration\Database;
  3. use Illuminate\Contracts\Database\Eloquent\Castable;
  4. use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
  5. use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;
  6. use Illuminate\Contracts\Database\Eloquent\DeviatesCastableAttributes;
  7. use Illuminate\Contracts\Database\Eloquent\SerializesCastableAttributes;
  8. use Illuminate\Database\Eloquent\InvalidCastException;
  9. use Illuminate\Database\Eloquent\Model;
  10. use Illuminate\Database\Schema\Blueprint;
  11. use Illuminate\Support\Carbon;
  12. use Illuminate\Support\Facades\Schema;
  13. class DatabaseEloquentModelCustomCastingTest extends DatabaseTestCase
  14. {
  15. protected function defineDatabaseMigrationsAfterDatabaseRefreshed()
  16. {
  17. Schema::create('test_eloquent_model_with_custom_casts', function (Blueprint $table) {
  18. $table->increments('id');
  19. $table->timestamps();
  20. $table->decimal('price');
  21. });
  22. }
  23. public function testBasicCustomCasting()
  24. {
  25. $model = new TestEloquentModelWithCustomCast;
  26. $model->uppercase = 'taylor';
  27. $this->assertSame('TAYLOR', $model->uppercase);
  28. $this->assertSame('TAYLOR', $model->getAttributes()['uppercase']);
  29. $this->assertSame('TAYLOR', $model->toArray()['uppercase']);
  30. $unserializedModel = unserialize(serialize($model));
  31. $this->assertSame('TAYLOR', $unserializedModel->uppercase);
  32. $this->assertSame('TAYLOR', $unserializedModel->getAttributes()['uppercase']);
  33. $this->assertSame('TAYLOR', $unserializedModel->toArray()['uppercase']);
  34. $model->syncOriginal();
  35. $model->uppercase = 'dries';
  36. $this->assertSame('TAYLOR', $model->getOriginal('uppercase'));
  37. $model = new TestEloquentModelWithCustomCast;
  38. $model->uppercase = 'taylor';
  39. $model->syncOriginal();
  40. $model->uppercase = 'dries';
  41. $model->getOriginal();
  42. $this->assertSame('DRIES', $model->uppercase);
  43. $model = new TestEloquentModelWithCustomCast;
  44. $model->address = $address = new Address('110 Kingsbrook St.', 'My Childhood House');
  45. $address->lineOne = '117 Spencer St.';
  46. $this->assertSame('117 Spencer St.', $model->getAttributes()['address_line_one']);
  47. $model = new TestEloquentModelWithCustomCast;
  48. $model->setRawAttributes([
  49. 'address_line_one' => '110 Kingsbrook St.',
  50. 'address_line_two' => 'My Childhood House',
  51. ]);
  52. $this->assertSame('110 Kingsbrook St.', $model->address->lineOne);
  53. $this->assertSame('My Childhood House', $model->address->lineTwo);
  54. $this->assertSame('110 Kingsbrook St.', $model->toArray()['address_line_one']);
  55. $this->assertSame('My Childhood House', $model->toArray()['address_line_two']);
  56. $model->address->lineOne = '117 Spencer St.';
  57. $this->assertFalse(isset($model->toArray()['address']));
  58. $this->assertSame('117 Spencer St.', $model->toArray()['address_line_one']);
  59. $this->assertSame('My Childhood House', $model->toArray()['address_line_two']);
  60. $this->assertSame('117 Spencer St.', json_decode($model->toJson(), true)['address_line_one']);
  61. $this->assertSame('My Childhood House', json_decode($model->toJson(), true)['address_line_two']);
  62. $model->address = null;
  63. $this->assertNull($model->toArray()['address_line_one']);
  64. $this->assertNull($model->toArray()['address_line_two']);
  65. $model->options = ['foo' => 'bar'];
  66. $this->assertEquals(['foo' => 'bar'], $model->options);
  67. $this->assertEquals(['foo' => 'bar'], $model->options);
  68. $model->options = ['foo' => 'bar'];
  69. $model->options = ['foo' => 'bar'];
  70. $this->assertEquals(['foo' => 'bar'], $model->options);
  71. $this->assertEquals(['foo' => 'bar'], $model->options);
  72. $this->assertSame(json_encode(['foo' => 'bar']), $model->getAttributes()['options']);
  73. $model = new TestEloquentModelWithCustomCast(['options' => []]);
  74. $model->syncOriginal();
  75. $model->options = ['foo' => 'bar'];
  76. $this->assertTrue($model->isDirty('options'));
  77. $model = new TestEloquentModelWithCustomCast;
  78. $model->birthday_at = now();
  79. $this->assertIsString($model->toArray()['birthday_at']);
  80. }
  81. public function testGetOriginalWithCastValueObjects()
  82. {
  83. $model = new TestEloquentModelWithCustomCast([
  84. 'address' => new Address('110 Kingsbrook St.', 'My Childhood House'),
  85. ]);
  86. $model->syncOriginal();
  87. $model->address = new Address('117 Spencer St.', 'Another house.');
  88. $this->assertSame('117 Spencer St.', $model->address->lineOne);
  89. $this->assertSame('110 Kingsbrook St.', $model->getOriginal('address')->lineOne);
  90. $this->assertSame('117 Spencer St.', $model->address->lineOne);
  91. $model = new TestEloquentModelWithCustomCast([
  92. 'address' => new Address('110 Kingsbrook St.', 'My Childhood House'),
  93. ]);
  94. $model->syncOriginal();
  95. $model->address = new Address('117 Spencer St.', 'Another house.');
  96. $this->assertSame('117 Spencer St.', $model->address->lineOne);
  97. $this->assertSame('110 Kingsbrook St.', $model->getOriginal()['address_line_one']);
  98. $this->assertSame('117 Spencer St.', $model->address->lineOne);
  99. $this->assertSame('110 Kingsbrook St.', $model->getOriginal()['address_line_one']);
  100. $model = new TestEloquentModelWithCustomCast([
  101. 'address' => new Address('110 Kingsbrook St.', 'My Childhood House'),
  102. ]);
  103. $model->syncOriginal();
  104. $model->address = null;
  105. $this->assertNull($model->address);
  106. $this->assertInstanceOf(Address::class, $model->getOriginal('address'));
  107. $this->assertNull($model->address);
  108. }
  109. public function testDeviableCasts()
  110. {
  111. $model = new TestEloquentModelWithCustomCast;
  112. $model->price = '123.456';
  113. $model->save();
  114. $model->increment('price', '530.865');
  115. $this->assertSame((new Decimal('654.321'))->getValue(), $model->price->getValue());
  116. $model->decrement('price', '333.333');
  117. $this->assertSame((new Decimal('320.988'))->getValue(), $model->price->getValue());
  118. }
  119. public function testSerializableCasts()
  120. {
  121. $model = new TestEloquentModelWithCustomCast;
  122. $model->price = '123.456';
  123. $expectedValue = (new Decimal('123.456'))->getValue();
  124. $this->assertSame($expectedValue, $model->price->getValue());
  125. $this->assertSame('123.456', $model->getAttributes()['price']);
  126. $this->assertSame('123.456', $model->toArray()['price']);
  127. $unserializedModel = unserialize(serialize($model));
  128. $this->assertSame($expectedValue, $unserializedModel->price->getValue());
  129. $this->assertSame('123.456', $unserializedModel->getAttributes()['price']);
  130. $this->assertSame('123.456', $unserializedModel->toArray()['price']);
  131. }
  132. public function testOneWayCasting()
  133. {
  134. // CastsInboundAttributes is used for casting that is unidirectional... only use case I can think of is one-way hashing...
  135. $model = new TestEloquentModelWithCustomCast;
  136. $model->password = 'secret';
  137. $this->assertEquals(hash('sha256', 'secret'), $model->password);
  138. $this->assertEquals(hash('sha256', 'secret'), $model->getAttributes()['password']);
  139. $this->assertEquals(hash('sha256', 'secret'), $model->getAttributes()['password']);
  140. $this->assertEquals(hash('sha256', 'secret'), $model->password);
  141. $model->password = 'secret2';
  142. $this->assertEquals(hash('sha256', 'secret2'), $model->password);
  143. $this->assertEquals(hash('sha256', 'secret2'), $model->getAttributes()['password']);
  144. $this->assertEquals(hash('sha256', 'secret2'), $model->getAttributes()['password']);
  145. $this->assertEquals(hash('sha256', 'secret2'), $model->password);
  146. }
  147. public function testSettingRawAttributesClearsTheCastCache()
  148. {
  149. $model = new TestEloquentModelWithCustomCast;
  150. $model->setRawAttributes([
  151. 'address_line_one' => '110 Kingsbrook St.',
  152. 'address_line_two' => 'My Childhood House',
  153. ]);
  154. $this->assertSame('110 Kingsbrook St.', $model->address->lineOne);
  155. $model->setRawAttributes([
  156. 'address_line_one' => '117 Spencer St.',
  157. 'address_line_two' => 'My Childhood House',
  158. ]);
  159. $this->assertSame('117 Spencer St.', $model->address->lineOne);
  160. }
  161. public function testWithCastableInterface()
  162. {
  163. $model = new TestEloquentModelWithCustomCast;
  164. $model->setRawAttributes([
  165. 'value_object_with_caster' => serialize(new ValueObject('hello')),
  166. ]);
  167. $this->assertInstanceOf(ValueObject::class, $model->value_object_with_caster);
  168. $this->assertSame(serialize(new ValueObject('hello')), $model->toArray()['value_object_with_caster']);
  169. $model->setRawAttributes([
  170. 'value_object_caster_with_argument' => null,
  171. ]);
  172. $this->assertSame('argument', $model->value_object_caster_with_argument);
  173. $model->setRawAttributes([
  174. 'value_object_caster_with_caster_instance' => serialize(new ValueObject('hello')),
  175. ]);
  176. $this->assertInstanceOf(ValueObject::class, $model->value_object_caster_with_caster_instance);
  177. }
  178. public function testGetFromUndefinedCast()
  179. {
  180. $this->expectException(InvalidCastException::class);
  181. $model = new TestEloquentModelWithCustomCast;
  182. $model->undefined_cast_column;
  183. }
  184. public function testSetToUndefinedCast()
  185. {
  186. $this->expectException(InvalidCastException::class);
  187. $model = new TestEloquentModelWithCustomCast;
  188. $this->assertTrue($model->hasCast('undefined_cast_column'));
  189. $model->undefined_cast_column = 'Glāžšķūņu rūķīši';
  190. }
  191. }
  192. class TestEloquentModelWithCustomCast extends Model
  193. {
  194. /**
  195. * The attributes that aren't mass assignable.
  196. *
  197. * @var string[]
  198. */
  199. protected $guarded = [];
  200. /**
  201. * The attributes that should be cast to native types.
  202. *
  203. * @var array
  204. */
  205. protected $casts = [
  206. 'address' => AddressCaster::class,
  207. 'price' => DecimalCaster::class,
  208. 'password' => HashCaster::class,
  209. 'other_password' => HashCaster::class.':md5',
  210. 'uppercase' => UppercaseCaster::class,
  211. 'options' => JsonCaster::class,
  212. 'value_object_with_caster' => ValueObject::class,
  213. 'value_object_caster_with_argument' => ValueObject::class.':argument',
  214. 'value_object_caster_with_caster_instance' => ValueObjectWithCasterInstance::class,
  215. 'undefined_cast_column' => UndefinedCast::class,
  216. 'birthday_at' => DateObjectCaster::class,
  217. ];
  218. }
  219. class HashCaster implements CastsInboundAttributes
  220. {
  221. public function __construct($algorithm = 'sha256')
  222. {
  223. $this->algorithm = $algorithm;
  224. }
  225. public function set($model, $key, $value, $attributes)
  226. {
  227. return [$key => hash($this->algorithm, $value)];
  228. }
  229. }
  230. class UppercaseCaster implements CastsAttributes
  231. {
  232. public function get($model, $key, $value, $attributes)
  233. {
  234. return strtoupper($value);
  235. }
  236. public function set($model, $key, $value, $attributes)
  237. {
  238. return [$key => strtoupper($value)];
  239. }
  240. }
  241. class AddressCaster implements CastsAttributes
  242. {
  243. public function get($model, $key, $value, $attributes)
  244. {
  245. if (is_null($attributes['address_line_one'])) {
  246. return;
  247. }
  248. return new Address($attributes['address_line_one'], $attributes['address_line_two']);
  249. }
  250. public function set($model, $key, $value, $attributes)
  251. {
  252. if (is_null($value)) {
  253. return [
  254. 'address_line_one' => null,
  255. 'address_line_two' => null,
  256. ];
  257. }
  258. return ['address_line_one' => $value->lineOne, 'address_line_two' => $value->lineTwo];
  259. }
  260. }
  261. class JsonCaster implements CastsAttributes
  262. {
  263. public function get($model, $key, $value, $attributes)
  264. {
  265. return json_decode($value, true);
  266. }
  267. public function set($model, $key, $value, $attributes)
  268. {
  269. return json_encode($value);
  270. }
  271. }
  272. class DecimalCaster implements CastsAttributes, DeviatesCastableAttributes, SerializesCastableAttributes
  273. {
  274. public function get($model, $key, $value, $attributes)
  275. {
  276. return new Decimal($value);
  277. }
  278. public function set($model, $key, $value, $attributes)
  279. {
  280. return (string) $value;
  281. }
  282. public function increment($model, $key, $value, $attributes)
  283. {
  284. return new Decimal($attributes[$key] + $value);
  285. }
  286. public function decrement($model, $key, $value, $attributes)
  287. {
  288. return new Decimal($attributes[$key] - $value);
  289. }
  290. public function serialize($model, $key, $value, $attributes)
  291. {
  292. return (string) $value;
  293. }
  294. }
  295. class ValueObjectCaster implements CastsAttributes
  296. {
  297. private $argument;
  298. public function __construct($argument = null)
  299. {
  300. $this->argument = $argument;
  301. }
  302. public function get($model, $key, $value, $attributes)
  303. {
  304. if ($this->argument) {
  305. return $this->argument;
  306. }
  307. return unserialize($value);
  308. }
  309. public function set($model, $key, $value, $attributes)
  310. {
  311. return serialize($value);
  312. }
  313. }
  314. class ValueObject implements Castable
  315. {
  316. public $name;
  317. public function __construct(string $name)
  318. {
  319. $this->name = $name;
  320. }
  321. public static function castUsing(array $arguments)
  322. {
  323. return new class(...$arguments) implements CastsAttributes, SerializesCastableAttributes
  324. {
  325. private $argument;
  326. public function __construct($argument = null)
  327. {
  328. $this->argument = $argument;
  329. }
  330. public function get($model, $key, $value, $attributes)
  331. {
  332. if ($this->argument) {
  333. return $this->argument;
  334. }
  335. return unserialize($value);
  336. }
  337. public function set($model, $key, $value, $attributes)
  338. {
  339. return serialize($value);
  340. }
  341. public function serialize($model, $key, $value, $attributes)
  342. {
  343. return serialize($value);
  344. }
  345. };
  346. }
  347. }
  348. class ValueObjectWithCasterInstance extends ValueObject
  349. {
  350. public static function castUsing(array $arguments)
  351. {
  352. return new ValueObjectCaster;
  353. }
  354. }
  355. class Address
  356. {
  357. public $lineOne;
  358. public $lineTwo;
  359. public function __construct($lineOne, $lineTwo)
  360. {
  361. $this->lineOne = $lineOne;
  362. $this->lineTwo = $lineTwo;
  363. }
  364. }
  365. final class Decimal
  366. {
  367. private $value;
  368. private $scale;
  369. public function __construct($value)
  370. {
  371. $parts = explode('.', (string) $value);
  372. $this->scale = strlen($parts[1]);
  373. $this->value = (int) str_replace('.', '', $value);
  374. }
  375. public function getValue()
  376. {
  377. return $this->value;
  378. }
  379. public function __toString()
  380. {
  381. return substr_replace($this->value, '.', -$this->scale, 0);
  382. }
  383. }
  384. class DateObjectCaster implements CastsAttributes
  385. {
  386. private $argument;
  387. public function __construct($argument = null)
  388. {
  389. $this->argument = $argument;
  390. }
  391. public function get($model, $key, $value, $attributes)
  392. {
  393. return Carbon::parse($value);
  394. }
  395. public function set($model, $key, $value, $attributes)
  396. {
  397. return $value->format('Y-m-d');
  398. }
  399. }