HasAttributes.php 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * This file is part of Hyperf.
  5. *
  6. * @link https://www.hyperf.io
  7. * @document https://hyperf.wiki
  8. * @contact group@hyperf.io
  9. * @license https://github.com/hyperf/hyperf/blob/master/LICENSE
  10. */
  11. namespace Hyperf\Database\Model\Concerns;
  12. use BackedEnum;
  13. use Carbon\Carbon;
  14. use Carbon\CarbonInterface;
  15. use DateTimeInterface;
  16. use Hyperf\Collection\Arr;
  17. use Hyperf\Collection\Collection as BaseCollection;
  18. use Hyperf\Contract\Arrayable;
  19. use Hyperf\Contract\Castable;
  20. use Hyperf\Contract\CastsAttributes;
  21. use Hyperf\Contract\CastsInboundAttributes;
  22. use Hyperf\Contract\Synchronized;
  23. use Hyperf\Database\Exception\InvalidCastException;
  24. use Hyperf\Database\Model\EnumCollector;
  25. use Hyperf\Database\Model\JsonEncodingException;
  26. use Hyperf\Database\Model\Relations\Relation;
  27. use Hyperf\Database\Query\Expression;
  28. use Hyperf\Stringable\Str;
  29. use Hyperf\Stringable\StrCache;
  30. use LogicException;
  31. use UnitEnum;
  32. use function Hyperf\Collection\collect;
  33. use function Hyperf\Tappable\tap;
  34. trait HasAttributes
  35. {
  36. /**
  37. * Indicates whether attributes are snake cased on arrays.
  38. */
  39. public static bool $snakeAttributes = true;
  40. /**
  41. * The model's attributes.
  42. */
  43. protected array $attributes = [];
  44. /**
  45. * The model attribute's original state.
  46. */
  47. protected array $original = [];
  48. /**
  49. * The changed model attributes.
  50. */
  51. protected array $changes = [];
  52. /**
  53. * The attributes that should be cast.
  54. */
  55. protected array $casts = [];
  56. /**
  57. * The attributes that have been cast using custom classes.
  58. */
  59. protected array $classCastCache = [];
  60. /**
  61. * The built-in, primitive cast types supported.
  62. */
  63. protected static array $primitiveCastTypes = [
  64. 'array',
  65. 'bool',
  66. 'boolean',
  67. 'collection',
  68. 'custom_datetime',
  69. 'date',
  70. 'datetime',
  71. 'decimal',
  72. 'double',
  73. 'float',
  74. 'int',
  75. 'integer',
  76. 'json',
  77. 'object',
  78. 'real',
  79. 'string',
  80. 'timestamp',
  81. ];
  82. /**
  83. * The attributes that should be mutated to dates.
  84. */
  85. protected array $dates = [];
  86. /**
  87. * The storage format of the model's date columns.
  88. */
  89. protected ?string $dateFormat = null;
  90. /**
  91. * The accessors to append to the model's array form.
  92. */
  93. protected array $appends = [];
  94. /**
  95. * The cache of the mutated attributes for each class.
  96. */
  97. protected static array $mutatorCache = [];
  98. /**
  99. * Convert the model's attributes to an array.
  100. */
  101. public function attributesToArray(): array
  102. {
  103. // If an attribute is a date, we will cast it to a string after converting it
  104. // to a DateTime / Carbon instance. This is so we will get some consistent
  105. // formatting while accessing attributes vs. arraying / JSONing a model.
  106. $attributes = $this->addDateAttributesToArray(
  107. $attributes = $this->getArrayableAttributes()
  108. );
  109. $attributes = $this->addMutatedAttributesToArray(
  110. $attributes,
  111. $mutatedAttributes = $this->getMutatedAttributes()
  112. );
  113. // Next we will handle any casts that have been setup for this model and cast
  114. // the values to their appropriate type. If the attribute has a mutator we
  115. // will not perform the cast on those attributes to avoid any confusion.
  116. $attributes = $this->addCastAttributesToArray(
  117. $attributes,
  118. $mutatedAttributes
  119. );
  120. // Here we will grab all of the appended, calculated attributes to this model
  121. // as these attributes are not really in the attributes array, but are run
  122. // when we need to array or JSON the model for convenience to the coder.
  123. foreach ($this->getArrayableAppends() as $key) {
  124. $attributes[$key] = $this->mutateAttributeForArray($key, null);
  125. }
  126. return $attributes;
  127. }
  128. /**
  129. * Get the model's relationships in array form.
  130. */
  131. public function relationsToArray(): array
  132. {
  133. $attributes = [];
  134. foreach ($this->getArrayableRelations() as $key => $value) {
  135. // If the values implements the Arrayable interface we can just call this
  136. // toArray method on the instances which will convert both models and
  137. // collections to their proper array form and we'll set the values.
  138. if ($value instanceof Arrayable) {
  139. $relation = $value->toArray();
  140. }
  141. // If the value is null, we'll still go ahead and set it in this list of
  142. // attributes since null is used to represent empty relationships if
  143. // if it a has one or belongs to type relationships on the models.
  144. elseif (is_null($value)) {
  145. $relation = $value;
  146. }
  147. // If the relationships snake-casing is enabled, we will snake case this
  148. // key so that the relation attribute is snake cased in this returned
  149. // array to the developers, making this consistent with attributes.
  150. if (static::$snakeAttributes) {
  151. $key = StrCache::snake($key);
  152. }
  153. // If the relation value has been set, we will set it on this attributes
  154. // list for returning. If it was not arrayable or null, we'll not set
  155. // the value on the array because it is some type of invalid value.
  156. if (isset($relation) || is_null($value)) {
  157. $attributes[$key] = $relation;
  158. }
  159. unset($relation);
  160. }
  161. return $attributes;
  162. }
  163. /**
  164. * Get an attribute from the model.
  165. */
  166. public function getAttribute(string $key)
  167. {
  168. if (! $key) {
  169. return;
  170. }
  171. // If the attribute exists in the attribute array or has a "get" mutator we will
  172. // get the attribute's value. Otherwise, we will proceed as if the developers
  173. // are asking for a relationship's value. This covers both types of values.
  174. if (array_key_exists($key, $this->getAttributes())
  175. || $this->hasGetMutator($key)
  176. || $this->isClassCastable($key)) {
  177. return $this->getAttributeValue($key);
  178. }
  179. // Here we will determine if the model base class itself contains this given key
  180. // since we don't want to treat any of those methods as relationships because
  181. // they are all intended as helper methods and none of these are relations.
  182. if (method_exists(self::class, $key)) {
  183. return;
  184. }
  185. return $this->getRelationValue($key);
  186. }
  187. /**
  188. * Get a plain attribute (not a relationship).
  189. */
  190. public function getAttributeValue(string $key)
  191. {
  192. return $this->transformModelValue($key, $this->getAttributeFromArray($key));
  193. }
  194. /**
  195. * Get a relationship.
  196. */
  197. public function getRelationValue(string $key)
  198. {
  199. // If the key already exists in the relationships array, it just means the
  200. // relationship has already been loaded, so we'll just return it out of
  201. // here because there is no need to query within the relations twice.
  202. if ($this->relationLoaded($key)) {
  203. return $this->relations[$key];
  204. }
  205. // If the "attribute" exists as a method on the model, we will just assume
  206. // it is a relationship and will load and return results from the query
  207. // and hydrate the relationship's value on the "relationships" array.
  208. if ($this->isRelation($key)) {
  209. return $this->getRelationshipFromMethod($key);
  210. }
  211. }
  212. /**
  213. * Determine if the given key is a relationship method on the model.
  214. */
  215. public function isRelation(string $key): bool
  216. {
  217. return method_exists($this, $key) || $this->relationResolver(static::class, $key);
  218. }
  219. /**
  220. * Determine if a get mutator exists for an attribute.
  221. */
  222. public function hasGetMutator(string $key): bool
  223. {
  224. return method_exists($this, 'get' . StrCache::studly($key) . 'Attribute');
  225. }
  226. /**
  227. * Merge new casts with existing casts on the model.
  228. */
  229. public function mergeCasts(array $casts): void
  230. {
  231. $this->casts = array_merge($this->casts, $casts);
  232. }
  233. /**
  234. * Set a given attribute on the model.
  235. */
  236. public function setAttribute(string $key, mixed $value)
  237. {
  238. // First we will check for the presence of a mutator for the set operation
  239. // which simply lets the developers tweak the attribute as it is set on
  240. // the model, such as "json_encoding" an listing of data for storage.
  241. if ($this->hasSetMutator($key)) {
  242. return $this->setMutatedAttributeValue($key, $value);
  243. }
  244. // If an attribute is listed as a "date", we'll convert it from a DateTime
  245. // instance into a form proper for storage on the database tables using
  246. // the connection grammar's date format. We will auto set the values.
  247. if ($value && $this->isDateAttribute($key)) {
  248. $value = $this->fromDateTime($value);
  249. }
  250. if ($this->isEnumCastable($key)) {
  251. $this->setEnumCastableAttribute($key, $value);
  252. return $this;
  253. }
  254. if ($this->isClassCastable($key)) {
  255. $this->setClassCastableAttribute($key, $value);
  256. return $this;
  257. }
  258. if ($this->isJsonCastable($key) && ! is_null($value)) {
  259. $value = $this->castAttributeAsJson($key, $value);
  260. }
  261. // If this attribute contains a JSON ->, we'll set the proper value in the
  262. // attribute's underlying array. This takes care of properly nesting an
  263. // attribute in the array's value in the case of deeply nested items.
  264. if (Str::contains($key, '->')) {
  265. return $this->fillJsonAttribute($key, $value);
  266. }
  267. $this->attributes[$key] = $value;
  268. return $this;
  269. }
  270. /**
  271. * Determine if a set mutator exists for an attribute.
  272. */
  273. public function hasSetMutator(string $key): bool
  274. {
  275. return method_exists($this, 'set' . StrCache::studly($key) . 'Attribute');
  276. }
  277. /**
  278. * Set a given JSON attribute on the model.
  279. */
  280. public function fillJsonAttribute(string $key, mixed $value): static
  281. {
  282. [$key, $path] = explode('->', $key, 2);
  283. $this->attributes[$key] = $this->asJson($this->getArrayAttributeWithValue(
  284. $path,
  285. $key,
  286. $value
  287. ));
  288. return $this;
  289. }
  290. /**
  291. * Decode the given JSON back into an array or object.
  292. */
  293. public function fromJson(string $value, bool $asObject = false)
  294. {
  295. return json_decode($value, ! $asObject);
  296. }
  297. /**
  298. * Decode the given float.
  299. */
  300. public function fromFloat(mixed $value): float
  301. {
  302. return match ((string) $value) {
  303. 'Infinity' => INF,
  304. '-Infinity' => -INF,
  305. 'NaN' => NAN,
  306. default => (float) $value,
  307. };
  308. }
  309. /**
  310. * Convert a DateTime to a storable string.
  311. *
  312. * @return null|string
  313. */
  314. public function fromDateTime(mixed $value): mixed
  315. {
  316. return empty($value) ? $value : $this->asDateTime($value)->format(
  317. $this->getDateFormat()
  318. );
  319. }
  320. /**
  321. * Get the attributes that should be converted to dates.
  322. */
  323. public function getDates(): array
  324. {
  325. $defaults = [static::CREATED_AT, static::UPDATED_AT];
  326. return $this->usesTimestamps()
  327. ? array_unique(array_merge($this->dates, $defaults))
  328. : $this->dates;
  329. }
  330. /**
  331. * Get the format for database stored dates.
  332. */
  333. public function getDateFormat(): string
  334. {
  335. return $this->dateFormat ?: $this->getConnection()->getQueryGrammar()->getDateFormat();
  336. }
  337. /**
  338. * Set the date format used by the model.
  339. */
  340. public function setDateFormat(string $format): static
  341. {
  342. $this->dateFormat = $format;
  343. return $this;
  344. }
  345. /**
  346. * Determine whether an attribute should be cast to a native type.
  347. *
  348. * @param null|string|string[] $types
  349. */
  350. public function hasCast(string $key, mixed $types = null): bool
  351. {
  352. if (array_key_exists($key, $this->getCasts())) {
  353. return ! $types || in_array($this->getCastType($key), (array) $types, true);
  354. }
  355. return false;
  356. }
  357. /**
  358. * Get the casts array.
  359. */
  360. public function getCasts(): array
  361. {
  362. if ($this->getIncrementing()) {
  363. return array_merge([$this->getKeyName() => $this->getKeyType()], $this->casts);
  364. }
  365. return $this->casts;
  366. }
  367. /**
  368. * Get all the current attributes on the model.
  369. */
  370. public function getAttributes(): array
  371. {
  372. return $this->attributes;
  373. }
  374. public function syncAttributes(): static
  375. {
  376. $this->mergeAttributesFromClassCasts();
  377. return $this;
  378. }
  379. /**
  380. * Set the array of model attributes. No checking is done.
  381. */
  382. public function setRawAttributes(array $attributes, bool $sync = false): static
  383. {
  384. $this->attributes = $attributes;
  385. if ($sync) {
  386. $this->syncOriginal();
  387. }
  388. $this->classCastCache = [];
  389. return $this;
  390. }
  391. /**
  392. * Get the model's original attribute values.
  393. */
  394. public function getOriginal(?string $key = null, mixed $default = null): mixed
  395. {
  396. if ($key) {
  397. return $this->transformModelValue(
  398. $key,
  399. Arr::get($this->original, $key, $default)
  400. );
  401. }
  402. return collect($this->original)->mapWithKeys(function (mixed $value, string $key) {
  403. return [$key => $this->transformModelValue($key, $value)];
  404. })->all();
  405. }
  406. /**
  407. * Get the model's raw original attribute values.
  408. *
  409. * @return array|mixed
  410. */
  411. public function getRawOriginal(?string $key = null, mixed $default = null): mixed
  412. {
  413. return Arr::get($this->original, $key, $default);
  414. }
  415. /**
  416. * Get a subset of the model's attributes.
  417. *
  418. * @param array|mixed $attributes
  419. */
  420. public function only(mixed $attributes): array
  421. {
  422. $results = [];
  423. foreach (is_array($attributes) ? $attributes : func_get_args() as $attribute) {
  424. $results[$attribute] = $this->getAttribute($attribute);
  425. }
  426. return $results;
  427. }
  428. /**
  429. * Sync the original attributes with the current.
  430. */
  431. public function syncOriginal(): static
  432. {
  433. $this->original = $this->getAttributes();
  434. return $this;
  435. }
  436. /**
  437. * Sync a single original attribute with its current value.
  438. *
  439. * @param string $attribute
  440. */
  441. public function syncOriginalAttribute($attribute): static
  442. {
  443. return $this->syncOriginalAttributes($attribute);
  444. }
  445. /**
  446. * Sync multiple original attribute with their current values.
  447. *
  448. * @param array|string $attributes
  449. * @return $this
  450. */
  451. public function syncOriginalAttributes($attributes): static
  452. {
  453. $attributes = is_array($attributes) ? $attributes : func_get_args();
  454. $modelAttributes = $this->getAttributes();
  455. foreach ($attributes as $attribute) {
  456. $this->original[$attribute] = $modelAttributes[$attribute];
  457. }
  458. return $this;
  459. }
  460. /**
  461. * Sync the changed attributes.
  462. */
  463. public function syncChanges(?array $columns = null): static
  464. {
  465. $changes = $this->getDirty();
  466. $this->changes = is_array($columns) ? Arr::only($changes, $columns) : $changes;
  467. return $this;
  468. }
  469. /**
  470. * Determine if the model or given attribute(s) have been modified.
  471. *
  472. * @param null|array|string $attributes
  473. */
  474. public function isDirty($attributes = null): bool
  475. {
  476. return $this->hasChanges(
  477. $this->getDirty(),
  478. is_array($attributes) ? $attributes : func_get_args()
  479. );
  480. }
  481. /**
  482. * Determine if the model or given attribute(s) have remained the same.
  483. *
  484. * @param null|array|string $attributes
  485. */
  486. public function isClean($attributes = null): bool
  487. {
  488. return ! $this->isDirty(...func_get_args());
  489. }
  490. /**
  491. * Determine if the model or given attribute(s) have been modified.
  492. *
  493. * @param null|array|string $attributes
  494. */
  495. public function wasChanged($attributes = null): bool
  496. {
  497. return $this->hasChanges(
  498. $this->getChanges(),
  499. is_array($attributes) ? $attributes : func_get_args()
  500. );
  501. }
  502. /**
  503. * Get the attributes that have been changed since last sync.
  504. */
  505. public function getDirty(): array
  506. {
  507. $dirty = [];
  508. foreach ($this->getAttributes() as $key => $value) {
  509. if (! $this->originalIsEquivalent($key, $value)) {
  510. $dirty[$key] = $value;
  511. }
  512. }
  513. return $dirty;
  514. }
  515. /**
  516. * Get the attributes that were changed.
  517. */
  518. public function getChanges(): array
  519. {
  520. return $this->changes;
  521. }
  522. /**
  523. * Determine if the new and old values for a given key are equivalent.
  524. */
  525. public function originalIsEquivalent(string $key, mixed $current): bool
  526. {
  527. if (! array_key_exists($key, $this->original)) {
  528. return false;
  529. }
  530. $original = Arr::get($this->original, $key);
  531. if ($current === $original) {
  532. return true;
  533. }
  534. if (is_null($current)) {
  535. return false;
  536. }
  537. // The model parameters should not be set with an expression,
  538. // Because after saving the expression, the parameters of the model will not receive the latest results,
  539. // When the model be used again, It will cause some problems.
  540. // So you must do something by yourself, the framework shouldn't be modified in any way.
  541. if ($current instanceof Expression) {
  542. return false;
  543. }
  544. if ($this->isDateAttribute($key)) {
  545. return $this->fromDateTime($current) ===
  546. $this->fromDateTime($original);
  547. }
  548. if ($this->hasCast($key, static::$primitiveCastTypes)) {
  549. return $this->castAttribute($key, $current) ===
  550. $this->castAttribute($key, $original);
  551. }
  552. return is_numeric($current) && is_numeric($original)
  553. && strcmp((string) $current, (string) $original) === 0;
  554. }
  555. /**
  556. * Append attributes to query when building a query.
  557. *
  558. * @param array|string $attributes
  559. */
  560. public function append($attributes): static
  561. {
  562. $this->appends = array_unique(
  563. array_merge($this->appends, is_string($attributes) ? func_get_args() : $attributes)
  564. );
  565. return $this;
  566. }
  567. /**
  568. * Set the accessors to append to model arrays.
  569. */
  570. public function setAppends(array $appends): static
  571. {
  572. $this->appends = $appends;
  573. return $this;
  574. }
  575. /**
  576. * Get the mutated attributes for a given instance.
  577. */
  578. public function getMutatedAttributes(): array
  579. {
  580. $class = static::class;
  581. if (! isset(static::$mutatorCache[$class])) {
  582. static::cacheMutatedAttributes($class);
  583. }
  584. return static::$mutatorCache[$class];
  585. }
  586. /**
  587. * Extract and cache all the mutated attributes of a class.
  588. */
  589. public static function cacheMutatedAttributes(string $class): void
  590. {
  591. static::$mutatorCache[$class] = collect(static::getMutatorMethods($class))->map(function ($match) {
  592. return lcfirst(static::$snakeAttributes ? StrCache::snake($match) : $match);
  593. })->all();
  594. }
  595. /**
  596. * Add the date attributes to the attributes array.
  597. */
  598. protected function addDateAttributesToArray(array $attributes): array
  599. {
  600. foreach ($this->getDates() as $key) {
  601. if (! isset($attributes[$key])) {
  602. continue;
  603. }
  604. $attributes[$key] = $this->serializeDate(
  605. $this->asDateTime($attributes[$key])
  606. );
  607. }
  608. return $attributes;
  609. }
  610. /**
  611. * Add the mutated attributes to the attributes array.
  612. */
  613. protected function addMutatedAttributesToArray(array $attributes, array $mutatedAttributes): array
  614. {
  615. foreach ($mutatedAttributes as $key) {
  616. // We want to spin through all the mutated attributes for this model and call
  617. // the mutator for the attribute. We cache off every mutated attributes so
  618. // we don't have to constantly check on attributes that actually change.
  619. if (! array_key_exists($key, $attributes)) {
  620. continue;
  621. }
  622. // Next, we will call the mutator for this attribute so that we can get these
  623. // mutated attribute's actual values. After we finish mutating each of the
  624. // attributes we will return this final array of the mutated attributes.
  625. $attributes[$key] = $this->mutateAttributeForArray(
  626. $key,
  627. $attributes[$key]
  628. );
  629. }
  630. return $attributes;
  631. }
  632. /**
  633. * Add the casted attributes to the attributes array.
  634. */
  635. protected function addCastAttributesToArray(array $attributes, array $mutatedAttributes): array
  636. {
  637. foreach ($this->getCasts() as $key => $value) {
  638. if (! array_key_exists($key, $attributes) || in_array($key, $mutatedAttributes)) {
  639. continue;
  640. }
  641. // Here we will cast the attribute. Then, if the cast is a date or datetime cast
  642. // then we will serialize the date for the array. This will convert the dates
  643. // to strings based on the date format specified for these Model models.
  644. $attributes[$key] = $this->castAttribute(
  645. $key,
  646. $attributes[$key]
  647. );
  648. // If the attribute cast was a date or a datetime, we will serialize the date as
  649. // a string. This allows the developers to customize how dates are serialized
  650. // into an array without affecting how they are persisted into the storage.
  651. if ($attributes[$key]
  652. && ($value === 'date' || $value === 'datetime')) {
  653. $attributes[$key] = $this->serializeDate($attributes[$key]);
  654. }
  655. if ($attributes[$key] && $this->isCustomDateTimeCast($value)) {
  656. $attributes[$key] = $attributes[$key]->format(explode(':', $value, 2)[1]);
  657. }
  658. if ($this->isEnumCastable($key) && (! ($attributes[$key] ?? null) instanceof Arrayable)) {
  659. $attributes[$key] = isset($attributes[$key]) ? $this->getStorableEnumValue($attributes[$key]) : null;
  660. }
  661. if ($attributes[$key] instanceof Arrayable) {
  662. $attributes[$key] = $attributes[$key]->toArray();
  663. }
  664. }
  665. return $attributes;
  666. }
  667. /**
  668. * Get an attribute array of all arrayable attributes.
  669. */
  670. protected function getArrayableAttributes(): array
  671. {
  672. $this->syncAttributes();
  673. return $this->getArrayableItems($this->getAttributes());
  674. }
  675. /**
  676. * Get all of the appendable values that are arrayable.
  677. */
  678. protected function getArrayableAppends(): array
  679. {
  680. if (! count($this->appends)) {
  681. return [];
  682. }
  683. return $this->getArrayableItems(
  684. array_combine($this->appends, $this->appends)
  685. );
  686. }
  687. /**
  688. * Get an attribute array of all arrayable relations.
  689. */
  690. protected function getArrayableRelations(): array
  691. {
  692. return $this->getArrayableItems($this->relations);
  693. }
  694. /**
  695. * Get an attribute array of all arrayable values.
  696. */
  697. protected function getArrayableItems(array $values): array
  698. {
  699. if (count($this->getVisible()) > 0) {
  700. $values = array_intersect_key($values, array_flip($this->getVisible()));
  701. }
  702. if (count($this->getHidden()) > 0) {
  703. $values = array_diff_key($values, array_flip($this->getHidden()));
  704. }
  705. return $values;
  706. }
  707. /**
  708. * Get an attribute from the $attributes array.
  709. */
  710. protected function getAttributeFromArray(string $key): mixed
  711. {
  712. return $this->getAttributes()[$key] ?? null;
  713. }
  714. /**
  715. * Get a relationship value from a method.
  716. *
  717. * @throws LogicException
  718. */
  719. protected function getRelationshipFromMethod(string $method)
  720. {
  721. $relation = $this->{$method}();
  722. if (! $relation instanceof Relation) {
  723. if (is_null($relation)) {
  724. throw new LogicException(sprintf(
  725. '%s::%s must return a relationship instance, but "null" was returned. Was the "return" keyword used?',
  726. static::class,
  727. $method
  728. ));
  729. }
  730. throw new LogicException(sprintf(
  731. '%s::%s must return a relationship instance.',
  732. static::class,
  733. $method
  734. ));
  735. }
  736. return tap($relation->getResults(), function ($results) use ($method) {
  737. $this->setRelation($method, $results);
  738. });
  739. }
  740. /**
  741. * Get the value of an attribute using its mutator.
  742. */
  743. protected function mutateAttribute(string $key, mixed $value)
  744. {
  745. return $this->{'get' . StrCache::studly($key) . 'Attribute'}($value);
  746. }
  747. /**
  748. * Get the value of an attribute using its mutator for array conversion.
  749. */
  750. protected function mutateAttributeForArray(string $key, mixed $value)
  751. {
  752. $value = $this->isClassCastable($key)
  753. ? $this->getClassCastableAttributeValue($key, $value)
  754. : $this->mutateAttribute($key, $value);
  755. return $value instanceof Arrayable ? $value->toArray() : $value;
  756. }
  757. /**
  758. * Cast an attribute to a native PHP type.
  759. */
  760. protected function castAttribute(string $key, mixed $value): mixed
  761. {
  762. $castType = $this->getCastType($key);
  763. if (is_null($value) && in_array($castType, static::$primitiveCastTypes)) {
  764. return null;
  765. }
  766. switch ($castType) {
  767. case 'int':
  768. case 'integer':
  769. return (int) $value;
  770. case 'real':
  771. case 'float':
  772. case 'double':
  773. return $this->fromFloat($value);
  774. case 'decimal':
  775. return $this->asDecimal($value, explode(':', $this->getCasts()[$key], 2)[1]);
  776. case 'string':
  777. return (string) $value;
  778. case 'bool':
  779. case 'boolean':
  780. return (bool) $value;
  781. case 'object':
  782. return $this->fromJson($value, true);
  783. case 'array':
  784. case 'json':
  785. return $this->fromJson($value);
  786. case 'collection':
  787. return new BaseCollection($this->fromJson($value));
  788. case 'date':
  789. return $this->asDate($value);
  790. case 'datetime':
  791. case 'custom_datetime':
  792. return $this->asDateTime($value);
  793. case 'timestamp':
  794. return $this->asTimestamp($value);
  795. }
  796. if ($this->isEnumCastable($key)) {
  797. return $this->getEnumCastableAttributeValue($key, $value);
  798. }
  799. if ($this->isClassCastable($key)) {
  800. return $this->getClassCastableAttributeValue($key, $value);
  801. }
  802. return $value;
  803. }
  804. /**
  805. * Cast the given attribute using a custom cast class.
  806. */
  807. protected function getClassCastableAttributeValue(string $key, mixed $value): mixed
  808. {
  809. if (isset($this->classCastCache[$key])) {
  810. return $this->classCastCache[$key];
  811. }
  812. $caster = $this->resolveCasterClass($key);
  813. $value = $caster instanceof CastsInboundAttributes
  814. ? $value
  815. : $caster->get($this, $key, $value, $this->attributes);
  816. if ($caster instanceof CastsInboundAttributes || ! is_object($value)) {
  817. unset($this->classCastCache[$key]);
  818. } else {
  819. $this->classCastCache[$key] = $value;
  820. }
  821. return $value;
  822. }
  823. /**
  824. * Cast the given attribute to an enum.
  825. */
  826. protected function getEnumCastableAttributeValue(string $key, mixed $value): mixed
  827. {
  828. if (is_null($value)) {
  829. return null;
  830. }
  831. $castType = $this->getCasts()[$key];
  832. if ($value instanceof $castType) {
  833. return $value;
  834. }
  835. return $this->getEnumCaseFromValue($castType, $value);
  836. }
  837. /**
  838. * Get the type of cast for a model attribute.
  839. */
  840. protected function getCastType(string $key): string
  841. {
  842. if ($this->isCustomDateTimeCast($this->getCasts()[$key])) {
  843. return 'custom_datetime';
  844. }
  845. if ($this->isDecimalCast($this->getCasts()[$key])) {
  846. return 'decimal';
  847. }
  848. return trim(strtolower($this->getCasts()[$key]));
  849. }
  850. /**
  851. * Determine if the cast type is a custom date time cast.
  852. */
  853. protected function isCustomDateTimeCast(string $cast): bool
  854. {
  855. return strncmp($cast, 'date:', 5) === 0
  856. || strncmp($cast, 'datetime:', 9) === 0;
  857. }
  858. /**
  859. * Determine if the cast type is a decimal cast.
  860. */
  861. protected function isDecimalCast(string $cast): bool
  862. {
  863. return strncmp($cast, 'decimal:', 8) === 0;
  864. }
  865. /**
  866. * Set the value of an attribute using its mutator.
  867. */
  868. protected function setMutatedAttributeValue(string $key, mixed $value)
  869. {
  870. return $this->{'set' . StrCache::studly($key) . 'Attribute'}($value);
  871. }
  872. /**
  873. * Determine if the given attribute is a date or date castable.
  874. */
  875. protected function isDateAttribute(string $key): bool
  876. {
  877. return in_array($key, $this->getDates(), true)
  878. || $this->isDateCastable($key);
  879. }
  880. /**
  881. * Set the value of a class castable attribute.
  882. */
  883. protected function setClassCastableAttribute(string $key, mixed $value): void
  884. {
  885. $caster = $this->resolveCasterClass($key);
  886. if (is_null($value)) {
  887. $this->attributes = array_merge($this->attributes, array_map(
  888. function () {
  889. },
  890. $this->normalizeCastClassResponse($key, $caster->set(
  891. $this,
  892. $key,
  893. $this->{$key},
  894. $this->attributes
  895. ))
  896. ));
  897. } else {
  898. $this->attributes = array_merge(
  899. $this->attributes,
  900. $this->normalizeCastClassResponse($key, $caster->set(
  901. $this,
  902. $key,
  903. $value,
  904. $this->attributes
  905. ))
  906. );
  907. }
  908. if ($caster instanceof CastsInboundAttributes || ! is_object($value)) {
  909. unset($this->classCastCache[$key]);
  910. } else {
  911. $this->classCastCache[$key] = $value;
  912. }
  913. }
  914. /**
  915. * Set the value of an enum castable attribute.
  916. *
  917. * @param int|string|UnitEnum $value
  918. */
  919. protected function setEnumCastableAttribute(string $key, mixed $value): void
  920. {
  921. $enumClass = $this->getCasts()[$key];
  922. if (! isset($value)) {
  923. $this->attributes[$key] = null;
  924. } elseif (is_object($value)) {
  925. $this->attributes[$key] = $this->getStorableEnumValue($value);
  926. } else {
  927. $this->attributes[$key] = $this->getStorableEnumValue(
  928. $this->getEnumCaseFromValue($enumClass, $value)
  929. );
  930. }
  931. }
  932. /**
  933. * Get an enum case instance from a given class and value.
  934. */
  935. protected function getEnumCaseFromValue(string $enumClass, int|string $value): BackedEnum|UnitEnum
  936. {
  937. return EnumCollector::getEnumCaseFromValue($enumClass, $value);
  938. }
  939. /**
  940. * Get the storable value from the given enum.
  941. */
  942. protected function getStorableEnumValue(UnitEnum $value): int|string
  943. {
  944. return $value instanceof BackedEnum
  945. ? $value->value
  946. : $value->name;
  947. }
  948. /**
  949. * Get an array attribute with the given key and value set.
  950. *
  951. * @return $this
  952. */
  953. protected function getArrayAttributeWithValue(string $path, string $key, mixed $value)
  954. {
  955. return tap($this->getArrayAttributeByKey($key), function (&$array) use ($path, $value) {
  956. Arr::set($array, str_replace('->', '.', $path), $value);
  957. });
  958. }
  959. /**
  960. * Get an array attribute or return an empty array if it is not set.
  961. *
  962. * @return array
  963. */
  964. protected function getArrayAttributeByKey(string $key)
  965. {
  966. return isset($this->attributes[$key]) ?
  967. $this->fromJson($this->attributes[$key]) : [];
  968. }
  969. /**
  970. * Cast the given attribute to JSON.
  971. */
  972. protected function castAttributeAsJson(string $key, mixed $value): string
  973. {
  974. $value = $this->asJson($value);
  975. if ($value === false) {
  976. throw JsonEncodingException::forAttribute(
  977. $this,
  978. $key,
  979. json_last_error_msg()
  980. );
  981. }
  982. return $value;
  983. }
  984. /**
  985. * Encode the given value as JSON.
  986. */
  987. protected function asJson(mixed $value): false|string
  988. {
  989. return json_encode($value);
  990. }
  991. /**
  992. * Return a decimal as string.
  993. *
  994. * @param float $value
  995. * @param int $decimals
  996. */
  997. protected function asDecimal(mixed $value, mixed $decimals): string
  998. {
  999. return number_format((float) $value, (int) $decimals, '.', '');
  1000. }
  1001. /**
  1002. * Return a timestamp as DateTime object with time set to 00:00:00.
  1003. */
  1004. protected function asDate(mixed $value): CarbonInterface
  1005. {
  1006. return $this->asDateTime($value)->startOfDay();
  1007. }
  1008. /**
  1009. * Return a timestamp as DateTime object.
  1010. */
  1011. protected function asDateTime(mixed $value): CarbonInterface
  1012. {
  1013. // If this value is already a Carbon instance, we shall just return it as is.
  1014. // This prevents us having to re-instantiate a Carbon instance when we know
  1015. // it already is one, which wouldn't be fulfilled by the DateTime check.
  1016. if ($value instanceof CarbonInterface) {
  1017. return Carbon::instance($value);
  1018. }
  1019. // If the value is already a DateTime instance, we will just skip the rest of
  1020. // these checks since they will be a waste of time, and hinder performance
  1021. // when checking the field. We will just return the DateTime right away.
  1022. if ($value instanceof DateTimeInterface) {
  1023. return Carbon::parse(
  1024. $value->format('Y-m-d H:i:s.u'),
  1025. $value->getTimezone()
  1026. );
  1027. }
  1028. // If this value is an integer, we will assume it is a UNIX timestamp's value
  1029. // and format a Carbon object from this timestamp. This allows flexibility
  1030. // when defining your date fields as they might be UNIX timestamps here.
  1031. if (is_numeric($value)) {
  1032. return Carbon::createFromTimestamp($value);
  1033. }
  1034. // If the value is in simply year, month, day format, we will instantiate the
  1035. // Carbon instances from that format. Again, this provides for simple date
  1036. // fields on the database, while still supporting Carbonized conversion.
  1037. if ($this->isStandardDateFormat($value)) {
  1038. return Carbon::instance(Carbon::createFromFormat('Y-m-d', $value)->startOfDay());
  1039. }
  1040. $format = $this->getDateFormat();
  1041. // Finally, we will just assume this date is in the format used by default on
  1042. // the database connection and use that format to create the Carbon object
  1043. // that is returned back out to the developers after we convert it here.
  1044. if (Carbon::hasFormat($value, $format)) {
  1045. return Carbon::createFromFormat($format, $value);
  1046. }
  1047. return Carbon::parse($value);
  1048. }
  1049. /**
  1050. * Determine if the given value is a standard date format.
  1051. * @param mixed $value
  1052. */
  1053. protected function isStandardDateFormat($value)
  1054. {
  1055. return preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', (string) $value);
  1056. }
  1057. /**
  1058. * Return a timestamp as unix timestamp.
  1059. */
  1060. protected function asTimestamp(mixed $value): false|int
  1061. {
  1062. return $this->asDateTime($value)->getTimestamp();
  1063. }
  1064. /**
  1065. * Prepare a date for array / JSON serialization.
  1066. *
  1067. * @return string
  1068. */
  1069. protected function serializeDate(DateTimeInterface $date)
  1070. {
  1071. return $date->format($this->getDateFormat());
  1072. }
  1073. /**
  1074. * Determine whether a value is Date / DateTime castable for inbound manipulation.
  1075. */
  1076. protected function isDateCastable(string $key): bool
  1077. {
  1078. return $this->hasCast($key, ['date', 'datetime']);
  1079. }
  1080. /**
  1081. * Determine whether a value is JSON castable for inbound manipulation.
  1082. */
  1083. protected function isJsonCastable(string $key): bool
  1084. {
  1085. return $this->hasCast($key, ['array', 'json', 'object', 'collection']);
  1086. }
  1087. /**
  1088. * Determine if the given key is cast using a custom class.
  1089. */
  1090. protected function isClassCastable(string $key): bool
  1091. {
  1092. $casts = $this->getCasts();
  1093. if (! array_key_exists($key, $casts)) {
  1094. return false;
  1095. }
  1096. $castType = $this->parseCasterClass($casts[$key]);
  1097. if (in_array($castType, static::$primitiveCastTypes)) {
  1098. return false;
  1099. }
  1100. if (class_exists($castType)) {
  1101. return true;
  1102. }
  1103. throw new InvalidCastException($this::class, $key, $castType);
  1104. }
  1105. /**
  1106. * Determine if the given key is cast using an enum.
  1107. */
  1108. protected function isEnumCastable(string $key): bool
  1109. {
  1110. $casts = $this->getCasts();
  1111. if (! array_key_exists($key, $casts)) {
  1112. return false;
  1113. }
  1114. $castType = $casts[$key];
  1115. if (in_array($castType, static::$primitiveCastTypes)) {
  1116. return false;
  1117. }
  1118. return enum_exists($castType);
  1119. }
  1120. /**
  1121. * Resolve the custom caster class for a given key.
  1122. */
  1123. protected function resolveCasterClass(string $key): CastsAttributes|CastsInboundAttributes
  1124. {
  1125. $castType = $this->getCasts()[$key];
  1126. $arguments = [];
  1127. if (is_string($castType) && str_contains($castType, ':')) {
  1128. $segments = explode(':', $castType, 2);
  1129. $castType = $segments[0];
  1130. $arguments = explode(',', $segments[1]);
  1131. }
  1132. if (is_subclass_of($castType, Castable::class)) {
  1133. $castType = $castType::castUsing();
  1134. }
  1135. if (is_object($castType)) {
  1136. return $castType;
  1137. }
  1138. return new $castType(...$arguments);
  1139. }
  1140. /**
  1141. * Parse the given caster class, removing any arguments.
  1142. */
  1143. protected function parseCasterClass(string $class): string
  1144. {
  1145. return ! str_contains($class, ':') ? $class : explode(':', $class, 2)[0];
  1146. }
  1147. /**
  1148. * Merge the cast class attributes back into the model.
  1149. */
  1150. protected function mergeAttributesFromClassCasts(): void
  1151. {
  1152. foreach ($this->classCastCache as $key => $value) {
  1153. if ($value instanceof Synchronized && $value->isSynchronized()) {
  1154. continue;
  1155. }
  1156. $caster = $this->resolveCasterClass($key);
  1157. $this->attributes = array_merge(
  1158. $this->attributes,
  1159. $caster instanceof CastsInboundAttributes
  1160. ? [$key => $value]
  1161. : $this->normalizeCastClassResponse($key, $caster->set($this, $key, $value, $this->attributes))
  1162. );
  1163. }
  1164. }
  1165. /**
  1166. * Normalize the response from a custom class caster.
  1167. */
  1168. protected function normalizeCastClassResponse(string $key, mixed $value): array
  1169. {
  1170. return is_array($value) ? $value : [$key => $value];
  1171. }
  1172. /**
  1173. * Determine if any of the given attributes were changed.
  1174. *
  1175. * @param null|array|string $attributes
  1176. */
  1177. protected function hasChanges(array $changes, mixed $attributes = null): bool
  1178. {
  1179. // If no specific attributes were provided, we will just see if the dirty array
  1180. // already contains any attributes. If it does we will just return that this
  1181. // count is greater than zero. Else, we need to check specific attributes.
  1182. if (empty($attributes)) {
  1183. return count($changes) > 0;
  1184. }
  1185. // Here we will spin through every attribute and see if this is in the array of
  1186. // dirty attributes. If it is, we will return true and if we make it through
  1187. // all the attributes for the entire array we will return false at end.
  1188. foreach (Arr::wrap($attributes) as $attribute) {
  1189. if (array_key_exists($attribute, $changes)) {
  1190. return true;
  1191. }
  1192. }
  1193. return false;
  1194. }
  1195. /**
  1196. * Transform a raw model value using mutators, casts, etc.
  1197. */
  1198. protected function transformModelValue(string $key, mixed $value): mixed
  1199. {
  1200. // If the attribute has a get mutator, we will call that then return what
  1201. // it returns as the value, which is useful for transforming values on
  1202. // retrieval from the model to a form that is more useful for usage.
  1203. if ($this->hasGetMutator($key)) {
  1204. return $this->mutateAttribute($key, $value);
  1205. }
  1206. // If the attribute exists within the cast array, we will convert it to
  1207. // an appropriate native PHP type dependent upon the associated value
  1208. // given with the key in the pair. Dayle made this comment line up.
  1209. if ($this->hasCast($key)) {
  1210. return $this->castAttribute($key, $value);
  1211. }
  1212. // If the attribute is listed as a date, we will convert it to a DateTime
  1213. // instance on retrieval, which makes it quite convenient to work with
  1214. // date fields without having to create a mutator for each property.
  1215. if ($value !== null
  1216. && \in_array($key, $this->getDates(), false)) {
  1217. return $this->asDateTime($value);
  1218. }
  1219. return $value;
  1220. }
  1221. /**
  1222. * Get all of the attribute mutator methods.
  1223. *
  1224. * @param mixed $class
  1225. * @return array
  1226. */
  1227. protected static function getMutatorMethods(string $class)
  1228. {
  1229. preg_match_all('/(?<=^|;)get([^;]+?)Attribute(;|$)/', implode(';', get_class_methods($class)), $matches);
  1230. return $matches[1];
  1231. }
  1232. }