| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449 |
- <?php
- declare(strict_types=1);
- /**
- * This file is part of Hyperf.
- *
- * @link https://www.hyperf.io
- * @document https://hyperf.wiki
- * @contact group@hyperf.io
- * @license https://github.com/hyperf/hyperf/blob/master/LICENSE
- */
- namespace Hyperf\Database\Model\Concerns;
- use BackedEnum;
- use Carbon\Carbon;
- use Carbon\CarbonInterface;
- use DateTimeInterface;
- use Hyperf\Collection\Arr;
- use Hyperf\Collection\Collection as BaseCollection;
- use Hyperf\Contract\Arrayable;
- use Hyperf\Contract\Castable;
- use Hyperf\Contract\CastsAttributes;
- use Hyperf\Contract\CastsInboundAttributes;
- use Hyperf\Contract\Synchronized;
- use Hyperf\Database\Exception\InvalidCastException;
- use Hyperf\Database\Model\EnumCollector;
- use Hyperf\Database\Model\JsonEncodingException;
- use Hyperf\Database\Model\Relations\Relation;
- use Hyperf\Database\Query\Expression;
- use Hyperf\Stringable\Str;
- use Hyperf\Stringable\StrCache;
- use LogicException;
- use UnitEnum;
- use function Hyperf\Collection\collect;
- use function Hyperf\Tappable\tap;
- trait HasAttributes
- {
- /**
- * Indicates whether attributes are snake cased on arrays.
- */
- public static bool $snakeAttributes = true;
- /**
- * The model's attributes.
- */
- protected array $attributes = [];
- /**
- * The model attribute's original state.
- */
- protected array $original = [];
- /**
- * The changed model attributes.
- */
- protected array $changes = [];
- /**
- * The attributes that should be cast.
- */
- protected array $casts = [];
- /**
- * The attributes that have been cast using custom classes.
- */
- protected array $classCastCache = [];
- /**
- * The built-in, primitive cast types supported.
- */
- protected static array $primitiveCastTypes = [
- 'array',
- 'bool',
- 'boolean',
- 'collection',
- 'custom_datetime',
- 'date',
- 'datetime',
- 'decimal',
- 'double',
- 'float',
- 'int',
- 'integer',
- 'json',
- 'object',
- 'real',
- 'string',
- 'timestamp',
- ];
- /**
- * The attributes that should be mutated to dates.
- */
- protected array $dates = [];
- /**
- * The storage format of the model's date columns.
- */
- protected ?string $dateFormat = null;
- /**
- * The accessors to append to the model's array form.
- */
- protected array $appends = [];
- /**
- * The cache of the mutated attributes for each class.
- */
- protected static array $mutatorCache = [];
- /**
- * Convert the model's attributes to an array.
- */
- public function attributesToArray(): array
- {
- // If an attribute is a date, we will cast it to a string after converting it
- // to a DateTime / Carbon instance. This is so we will get some consistent
- // formatting while accessing attributes vs. arraying / JSONing a model.
- $attributes = $this->addDateAttributesToArray(
- $attributes = $this->getArrayableAttributes()
- );
- $attributes = $this->addMutatedAttributesToArray(
- $attributes,
- $mutatedAttributes = $this->getMutatedAttributes()
- );
- // Next we will handle any casts that have been setup for this model and cast
- // the values to their appropriate type. If the attribute has a mutator we
- // will not perform the cast on those attributes to avoid any confusion.
- $attributes = $this->addCastAttributesToArray(
- $attributes,
- $mutatedAttributes
- );
- // Here we will grab all of the appended, calculated attributes to this model
- // as these attributes are not really in the attributes array, but are run
- // when we need to array or JSON the model for convenience to the coder.
- foreach ($this->getArrayableAppends() as $key) {
- $attributes[$key] = $this->mutateAttributeForArray($key, null);
- }
- return $attributes;
- }
- /**
- * Get the model's relationships in array form.
- */
- public function relationsToArray(): array
- {
- $attributes = [];
- foreach ($this->getArrayableRelations() as $key => $value) {
- // If the values implements the Arrayable interface we can just call this
- // toArray method on the instances which will convert both models and
- // collections to their proper array form and we'll set the values.
- if ($value instanceof Arrayable) {
- $relation = $value->toArray();
- }
- // If the value is null, we'll still go ahead and set it in this list of
- // attributes since null is used to represent empty relationships if
- // if it a has one or belongs to type relationships on the models.
- elseif (is_null($value)) {
- $relation = $value;
- }
- // If the relationships snake-casing is enabled, we will snake case this
- // key so that the relation attribute is snake cased in this returned
- // array to the developers, making this consistent with attributes.
- if (static::$snakeAttributes) {
- $key = StrCache::snake($key);
- }
- // If the relation value has been set, we will set it on this attributes
- // list for returning. If it was not arrayable or null, we'll not set
- // the value on the array because it is some type of invalid value.
- if (isset($relation) || is_null($value)) {
- $attributes[$key] = $relation;
- }
- unset($relation);
- }
- return $attributes;
- }
- /**
- * Get an attribute from the model.
- */
- public function getAttribute(string $key)
- {
- if (! $key) {
- return;
- }
- // If the attribute exists in the attribute array or has a "get" mutator we will
- // get the attribute's value. Otherwise, we will proceed as if the developers
- // are asking for a relationship's value. This covers both types of values.
- if (array_key_exists($key, $this->getAttributes())
- || $this->hasGetMutator($key)
- || $this->isClassCastable($key)) {
- return $this->getAttributeValue($key);
- }
- // Here we will determine if the model base class itself contains this given key
- // since we don't want to treat any of those methods as relationships because
- // they are all intended as helper methods and none of these are relations.
- if (method_exists(self::class, $key)) {
- return;
- }
- return $this->getRelationValue($key);
- }
- /**
- * Get a plain attribute (not a relationship).
- */
- public function getAttributeValue(string $key)
- {
- return $this->transformModelValue($key, $this->getAttributeFromArray($key));
- }
- /**
- * Get a relationship.
- */
- public function getRelationValue(string $key)
- {
- // If the key already exists in the relationships array, it just means the
- // relationship has already been loaded, so we'll just return it out of
- // here because there is no need to query within the relations twice.
- if ($this->relationLoaded($key)) {
- return $this->relations[$key];
- }
- // If the "attribute" exists as a method on the model, we will just assume
- // it is a relationship and will load and return results from the query
- // and hydrate the relationship's value on the "relationships" array.
- if ($this->isRelation($key)) {
- return $this->getRelationshipFromMethod($key);
- }
- }
- /**
- * Determine if the given key is a relationship method on the model.
- */
- public function isRelation(string $key): bool
- {
- return method_exists($this, $key) || $this->relationResolver(static::class, $key);
- }
- /**
- * Determine if a get mutator exists for an attribute.
- */
- public function hasGetMutator(string $key): bool
- {
- return method_exists($this, 'get' . StrCache::studly($key) . 'Attribute');
- }
- /**
- * Merge new casts with existing casts on the model.
- */
- public function mergeCasts(array $casts): void
- {
- $this->casts = array_merge($this->casts, $casts);
- }
- /**
- * Set a given attribute on the model.
- */
- public function setAttribute(string $key, mixed $value)
- {
- // First we will check for the presence of a mutator for the set operation
- // which simply lets the developers tweak the attribute as it is set on
- // the model, such as "json_encoding" an listing of data for storage.
- if ($this->hasSetMutator($key)) {
- return $this->setMutatedAttributeValue($key, $value);
- }
- // If an attribute is listed as a "date", we'll convert it from a DateTime
- // instance into a form proper for storage on the database tables using
- // the connection grammar's date format. We will auto set the values.
- if ($value && $this->isDateAttribute($key)) {
- $value = $this->fromDateTime($value);
- }
- if ($this->isEnumCastable($key)) {
- $this->setEnumCastableAttribute($key, $value);
- return $this;
- }
- if ($this->isClassCastable($key)) {
- $this->setClassCastableAttribute($key, $value);
- return $this;
- }
- if ($this->isJsonCastable($key) && ! is_null($value)) {
- $value = $this->castAttributeAsJson($key, $value);
- }
- // If this attribute contains a JSON ->, we'll set the proper value in the
- // attribute's underlying array. This takes care of properly nesting an
- // attribute in the array's value in the case of deeply nested items.
- if (Str::contains($key, '->')) {
- return $this->fillJsonAttribute($key, $value);
- }
- $this->attributes[$key] = $value;
- return $this;
- }
- /**
- * Determine if a set mutator exists for an attribute.
- */
- public function hasSetMutator(string $key): bool
- {
- return method_exists($this, 'set' . StrCache::studly($key) . 'Attribute');
- }
- /**
- * Set a given JSON attribute on the model.
- */
- public function fillJsonAttribute(string $key, mixed $value): static
- {
- [$key, $path] = explode('->', $key, 2);
- $this->attributes[$key] = $this->asJson($this->getArrayAttributeWithValue(
- $path,
- $key,
- $value
- ));
- return $this;
- }
- /**
- * Decode the given JSON back into an array or object.
- */
- public function fromJson(string $value, bool $asObject = false)
- {
- return json_decode($value, ! $asObject);
- }
- /**
- * Decode the given float.
- */
- public function fromFloat(mixed $value): float
- {
- return match ((string) $value) {
- 'Infinity' => INF,
- '-Infinity' => -INF,
- 'NaN' => NAN,
- default => (float) $value,
- };
- }
- /**
- * Convert a DateTime to a storable string.
- *
- * @return null|string
- */
- public function fromDateTime(mixed $value): mixed
- {
- return empty($value) ? $value : $this->asDateTime($value)->format(
- $this->getDateFormat()
- );
- }
- /**
- * Get the attributes that should be converted to dates.
- */
- public function getDates(): array
- {
- $defaults = [static::CREATED_AT, static::UPDATED_AT];
- return $this->usesTimestamps()
- ? array_unique(array_merge($this->dates, $defaults))
- : $this->dates;
- }
- /**
- * Get the format for database stored dates.
- */
- public function getDateFormat(): string
- {
- return $this->dateFormat ?: $this->getConnection()->getQueryGrammar()->getDateFormat();
- }
- /**
- * Set the date format used by the model.
- */
- public function setDateFormat(string $format): static
- {
- $this->dateFormat = $format;
- return $this;
- }
- /**
- * Determine whether an attribute should be cast to a native type.
- *
- * @param null|string|string[] $types
- */
- public function hasCast(string $key, mixed $types = null): bool
- {
- if (array_key_exists($key, $this->getCasts())) {
- return ! $types || in_array($this->getCastType($key), (array) $types, true);
- }
- return false;
- }
- /**
- * Get the casts array.
- */
- public function getCasts(): array
- {
- if ($this->getIncrementing()) {
- return array_merge([$this->getKeyName() => $this->getKeyType()], $this->casts);
- }
- return $this->casts;
- }
- /**
- * Get all the current attributes on the model.
- */
- public function getAttributes(): array
- {
- return $this->attributes;
- }
- public function syncAttributes(): static
- {
- $this->mergeAttributesFromClassCasts();
- return $this;
- }
- /**
- * Set the array of model attributes. No checking is done.
- */
- public function setRawAttributes(array $attributes, bool $sync = false): static
- {
- $this->attributes = $attributes;
- if ($sync) {
- $this->syncOriginal();
- }
- $this->classCastCache = [];
- return $this;
- }
- /**
- * Get the model's original attribute values.
- */
- public function getOriginal(?string $key = null, mixed $default = null): mixed
- {
- if ($key) {
- return $this->transformModelValue(
- $key,
- Arr::get($this->original, $key, $default)
- );
- }
- return collect($this->original)->mapWithKeys(function (mixed $value, string $key) {
- return [$key => $this->transformModelValue($key, $value)];
- })->all();
- }
- /**
- * Get the model's raw original attribute values.
- *
- * @return array|mixed
- */
- public function getRawOriginal(?string $key = null, mixed $default = null): mixed
- {
- return Arr::get($this->original, $key, $default);
- }
- /**
- * Get a subset of the model's attributes.
- *
- * @param array|mixed $attributes
- */
- public function only(mixed $attributes): array
- {
- $results = [];
- foreach (is_array($attributes) ? $attributes : func_get_args() as $attribute) {
- $results[$attribute] = $this->getAttribute($attribute);
- }
- return $results;
- }
- /**
- * Sync the original attributes with the current.
- */
- public function syncOriginal(): static
- {
- $this->original = $this->getAttributes();
- return $this;
- }
- /**
- * Sync a single original attribute with its current value.
- *
- * @param string $attribute
- */
- public function syncOriginalAttribute($attribute): static
- {
- return $this->syncOriginalAttributes($attribute);
- }
- /**
- * Sync multiple original attribute with their current values.
- *
- * @param array|string $attributes
- * @return $this
- */
- public function syncOriginalAttributes($attributes): static
- {
- $attributes = is_array($attributes) ? $attributes : func_get_args();
- $modelAttributes = $this->getAttributes();
- foreach ($attributes as $attribute) {
- $this->original[$attribute] = $modelAttributes[$attribute];
- }
- return $this;
- }
- /**
- * Sync the changed attributes.
- */
- public function syncChanges(?array $columns = null): static
- {
- $changes = $this->getDirty();
- $this->changes = is_array($columns) ? Arr::only($changes, $columns) : $changes;
- return $this;
- }
- /**
- * Determine if the model or given attribute(s) have been modified.
- *
- * @param null|array|string $attributes
- */
- public function isDirty($attributes = null): bool
- {
- return $this->hasChanges(
- $this->getDirty(),
- is_array($attributes) ? $attributes : func_get_args()
- );
- }
- /**
- * Determine if the model or given attribute(s) have remained the same.
- *
- * @param null|array|string $attributes
- */
- public function isClean($attributes = null): bool
- {
- return ! $this->isDirty(...func_get_args());
- }
- /**
- * Determine if the model or given attribute(s) have been modified.
- *
- * @param null|array|string $attributes
- */
- public function wasChanged($attributes = null): bool
- {
- return $this->hasChanges(
- $this->getChanges(),
- is_array($attributes) ? $attributes : func_get_args()
- );
- }
- /**
- * Get the attributes that have been changed since last sync.
- */
- public function getDirty(): array
- {
- $dirty = [];
- foreach ($this->getAttributes() as $key => $value) {
- if (! $this->originalIsEquivalent($key, $value)) {
- $dirty[$key] = $value;
- }
- }
- return $dirty;
- }
- /**
- * Get the attributes that were changed.
- */
- public function getChanges(): array
- {
- return $this->changes;
- }
- /**
- * Determine if the new and old values for a given key are equivalent.
- */
- public function originalIsEquivalent(string $key, mixed $current): bool
- {
- if (! array_key_exists($key, $this->original)) {
- return false;
- }
- $original = Arr::get($this->original, $key);
- if ($current === $original) {
- return true;
- }
- if (is_null($current)) {
- return false;
- }
- // The model parameters should not be set with an expression,
- // Because after saving the expression, the parameters of the model will not receive the latest results,
- // When the model be used again, It will cause some problems.
- // So you must do something by yourself, the framework shouldn't be modified in any way.
- if ($current instanceof Expression) {
- return false;
- }
- if ($this->isDateAttribute($key)) {
- return $this->fromDateTime($current) ===
- $this->fromDateTime($original);
- }
- if ($this->hasCast($key, static::$primitiveCastTypes)) {
- return $this->castAttribute($key, $current) ===
- $this->castAttribute($key, $original);
- }
- return is_numeric($current) && is_numeric($original)
- && strcmp((string) $current, (string) $original) === 0;
- }
- /**
- * Append attributes to query when building a query.
- *
- * @param array|string $attributes
- */
- public function append($attributes): static
- {
- $this->appends = array_unique(
- array_merge($this->appends, is_string($attributes) ? func_get_args() : $attributes)
- );
- return $this;
- }
- /**
- * Set the accessors to append to model arrays.
- */
- public function setAppends(array $appends): static
- {
- $this->appends = $appends;
- return $this;
- }
- /**
- * Get the mutated attributes for a given instance.
- */
- public function getMutatedAttributes(): array
- {
- $class = static::class;
- if (! isset(static::$mutatorCache[$class])) {
- static::cacheMutatedAttributes($class);
- }
- return static::$mutatorCache[$class];
- }
- /**
- * Extract and cache all the mutated attributes of a class.
- */
- public static function cacheMutatedAttributes(string $class): void
- {
- static::$mutatorCache[$class] = collect(static::getMutatorMethods($class))->map(function ($match) {
- return lcfirst(static::$snakeAttributes ? StrCache::snake($match) : $match);
- })->all();
- }
- /**
- * Add the date attributes to the attributes array.
- */
- protected function addDateAttributesToArray(array $attributes): array
- {
- foreach ($this->getDates() as $key) {
- if (! isset($attributes[$key])) {
- continue;
- }
- $attributes[$key] = $this->serializeDate(
- $this->asDateTime($attributes[$key])
- );
- }
- return $attributes;
- }
- /**
- * Add the mutated attributes to the attributes array.
- */
- protected function addMutatedAttributesToArray(array $attributes, array $mutatedAttributes): array
- {
- foreach ($mutatedAttributes as $key) {
- // We want to spin through all the mutated attributes for this model and call
- // the mutator for the attribute. We cache off every mutated attributes so
- // we don't have to constantly check on attributes that actually change.
- if (! array_key_exists($key, $attributes)) {
- continue;
- }
- // Next, we will call the mutator for this attribute so that we can get these
- // mutated attribute's actual values. After we finish mutating each of the
- // attributes we will return this final array of the mutated attributes.
- $attributes[$key] = $this->mutateAttributeForArray(
- $key,
- $attributes[$key]
- );
- }
- return $attributes;
- }
- /**
- * Add the casted attributes to the attributes array.
- */
- protected function addCastAttributesToArray(array $attributes, array $mutatedAttributes): array
- {
- foreach ($this->getCasts() as $key => $value) {
- if (! array_key_exists($key, $attributes) || in_array($key, $mutatedAttributes)) {
- continue;
- }
- // Here we will cast the attribute. Then, if the cast is a date or datetime cast
- // then we will serialize the date for the array. This will convert the dates
- // to strings based on the date format specified for these Model models.
- $attributes[$key] = $this->castAttribute(
- $key,
- $attributes[$key]
- );
- // If the attribute cast was a date or a datetime, we will serialize the date as
- // a string. This allows the developers to customize how dates are serialized
- // into an array without affecting how they are persisted into the storage.
- if ($attributes[$key]
- && ($value === 'date' || $value === 'datetime')) {
- $attributes[$key] = $this->serializeDate($attributes[$key]);
- }
- if ($attributes[$key] && $this->isCustomDateTimeCast($value)) {
- $attributes[$key] = $attributes[$key]->format(explode(':', $value, 2)[1]);
- }
- if ($this->isEnumCastable($key) && (! ($attributes[$key] ?? null) instanceof Arrayable)) {
- $attributes[$key] = isset($attributes[$key]) ? $this->getStorableEnumValue($attributes[$key]) : null;
- }
- if ($attributes[$key] instanceof Arrayable) {
- $attributes[$key] = $attributes[$key]->toArray();
- }
- }
- return $attributes;
- }
- /**
- * Get an attribute array of all arrayable attributes.
- */
- protected function getArrayableAttributes(): array
- {
- $this->syncAttributes();
- return $this->getArrayableItems($this->getAttributes());
- }
- /**
- * Get all of the appendable values that are arrayable.
- */
- protected function getArrayableAppends(): array
- {
- if (! count($this->appends)) {
- return [];
- }
- return $this->getArrayableItems(
- array_combine($this->appends, $this->appends)
- );
- }
- /**
- * Get an attribute array of all arrayable relations.
- */
- protected function getArrayableRelations(): array
- {
- return $this->getArrayableItems($this->relations);
- }
- /**
- * Get an attribute array of all arrayable values.
- */
- protected function getArrayableItems(array $values): array
- {
- if (count($this->getVisible()) > 0) {
- $values = array_intersect_key($values, array_flip($this->getVisible()));
- }
- if (count($this->getHidden()) > 0) {
- $values = array_diff_key($values, array_flip($this->getHidden()));
- }
- return $values;
- }
- /**
- * Get an attribute from the $attributes array.
- */
- protected function getAttributeFromArray(string $key): mixed
- {
- return $this->getAttributes()[$key] ?? null;
- }
- /**
- * Get a relationship value from a method.
- *
- * @throws LogicException
- */
- protected function getRelationshipFromMethod(string $method)
- {
- $relation = $this->{$method}();
- if (! $relation instanceof Relation) {
- if (is_null($relation)) {
- throw new LogicException(sprintf(
- '%s::%s must return a relationship instance, but "null" was returned. Was the "return" keyword used?',
- static::class,
- $method
- ));
- }
- throw new LogicException(sprintf(
- '%s::%s must return a relationship instance.',
- static::class,
- $method
- ));
- }
- return tap($relation->getResults(), function ($results) use ($method) {
- $this->setRelation($method, $results);
- });
- }
- /**
- * Get the value of an attribute using its mutator.
- */
- protected function mutateAttribute(string $key, mixed $value)
- {
- return $this->{'get' . StrCache::studly($key) . 'Attribute'}($value);
- }
- /**
- * Get the value of an attribute using its mutator for array conversion.
- */
- protected function mutateAttributeForArray(string $key, mixed $value)
- {
- $value = $this->isClassCastable($key)
- ? $this->getClassCastableAttributeValue($key, $value)
- : $this->mutateAttribute($key, $value);
- return $value instanceof Arrayable ? $value->toArray() : $value;
- }
- /**
- * Cast an attribute to a native PHP type.
- */
- protected function castAttribute(string $key, mixed $value): mixed
- {
- $castType = $this->getCastType($key);
- if (is_null($value) && in_array($castType, static::$primitiveCastTypes)) {
- return null;
- }
- switch ($castType) {
- case 'int':
- case 'integer':
- return (int) $value;
- case 'real':
- case 'float':
- case 'double':
- return $this->fromFloat($value);
- case 'decimal':
- return $this->asDecimal($value, explode(':', $this->getCasts()[$key], 2)[1]);
- case 'string':
- return (string) $value;
- case 'bool':
- case 'boolean':
- return (bool) $value;
- case 'object':
- return $this->fromJson($value, true);
- case 'array':
- case 'json':
- return $this->fromJson($value);
- case 'collection':
- return new BaseCollection($this->fromJson($value));
- case 'date':
- return $this->asDate($value);
- case 'datetime':
- case 'custom_datetime':
- return $this->asDateTime($value);
- case 'timestamp':
- return $this->asTimestamp($value);
- }
- if ($this->isEnumCastable($key)) {
- return $this->getEnumCastableAttributeValue($key, $value);
- }
- if ($this->isClassCastable($key)) {
- return $this->getClassCastableAttributeValue($key, $value);
- }
- return $value;
- }
- /**
- * Cast the given attribute using a custom cast class.
- */
- protected function getClassCastableAttributeValue(string $key, mixed $value): mixed
- {
- if (isset($this->classCastCache[$key])) {
- return $this->classCastCache[$key];
- }
- $caster = $this->resolveCasterClass($key);
- $value = $caster instanceof CastsInboundAttributes
- ? $value
- : $caster->get($this, $key, $value, $this->attributes);
- if ($caster instanceof CastsInboundAttributes || ! is_object($value)) {
- unset($this->classCastCache[$key]);
- } else {
- $this->classCastCache[$key] = $value;
- }
- return $value;
- }
- /**
- * Cast the given attribute to an enum.
- */
- protected function getEnumCastableAttributeValue(string $key, mixed $value): mixed
- {
- if (is_null($value)) {
- return null;
- }
- $castType = $this->getCasts()[$key];
- if ($value instanceof $castType) {
- return $value;
- }
- return $this->getEnumCaseFromValue($castType, $value);
- }
- /**
- * Get the type of cast for a model attribute.
- */
- protected function getCastType(string $key): string
- {
- if ($this->isCustomDateTimeCast($this->getCasts()[$key])) {
- return 'custom_datetime';
- }
- if ($this->isDecimalCast($this->getCasts()[$key])) {
- return 'decimal';
- }
- return trim(strtolower($this->getCasts()[$key]));
- }
- /**
- * Determine if the cast type is a custom date time cast.
- */
- protected function isCustomDateTimeCast(string $cast): bool
- {
- return strncmp($cast, 'date:', 5) === 0
- || strncmp($cast, 'datetime:', 9) === 0;
- }
- /**
- * Determine if the cast type is a decimal cast.
- */
- protected function isDecimalCast(string $cast): bool
- {
- return strncmp($cast, 'decimal:', 8) === 0;
- }
- /**
- * Set the value of an attribute using its mutator.
- */
- protected function setMutatedAttributeValue(string $key, mixed $value)
- {
- return $this->{'set' . StrCache::studly($key) . 'Attribute'}($value);
- }
- /**
- * Determine if the given attribute is a date or date castable.
- */
- protected function isDateAttribute(string $key): bool
- {
- return in_array($key, $this->getDates(), true)
- || $this->isDateCastable($key);
- }
- /**
- * Set the value of a class castable attribute.
- */
- protected function setClassCastableAttribute(string $key, mixed $value): void
- {
- $caster = $this->resolveCasterClass($key);
- if (is_null($value)) {
- $this->attributes = array_merge($this->attributes, array_map(
- function () {
- },
- $this->normalizeCastClassResponse($key, $caster->set(
- $this,
- $key,
- $this->{$key},
- $this->attributes
- ))
- ));
- } else {
- $this->attributes = array_merge(
- $this->attributes,
- $this->normalizeCastClassResponse($key, $caster->set(
- $this,
- $key,
- $value,
- $this->attributes
- ))
- );
- }
- if ($caster instanceof CastsInboundAttributes || ! is_object($value)) {
- unset($this->classCastCache[$key]);
- } else {
- $this->classCastCache[$key] = $value;
- }
- }
- /**
- * Set the value of an enum castable attribute.
- *
- * @param int|string|UnitEnum $value
- */
- protected function setEnumCastableAttribute(string $key, mixed $value): void
- {
- $enumClass = $this->getCasts()[$key];
- if (! isset($value)) {
- $this->attributes[$key] = null;
- } elseif (is_object($value)) {
- $this->attributes[$key] = $this->getStorableEnumValue($value);
- } else {
- $this->attributes[$key] = $this->getStorableEnumValue(
- $this->getEnumCaseFromValue($enumClass, $value)
- );
- }
- }
- /**
- * Get an enum case instance from a given class and value.
- */
- protected function getEnumCaseFromValue(string $enumClass, int|string $value): BackedEnum|UnitEnum
- {
- return EnumCollector::getEnumCaseFromValue($enumClass, $value);
- }
- /**
- * Get the storable value from the given enum.
- */
- protected function getStorableEnumValue(UnitEnum $value): int|string
- {
- return $value instanceof BackedEnum
- ? $value->value
- : $value->name;
- }
- /**
- * Get an array attribute with the given key and value set.
- *
- * @return $this
- */
- protected function getArrayAttributeWithValue(string $path, string $key, mixed $value)
- {
- return tap($this->getArrayAttributeByKey($key), function (&$array) use ($path, $value) {
- Arr::set($array, str_replace('->', '.', $path), $value);
- });
- }
- /**
- * Get an array attribute or return an empty array if it is not set.
- *
- * @return array
- */
- protected function getArrayAttributeByKey(string $key)
- {
- return isset($this->attributes[$key]) ?
- $this->fromJson($this->attributes[$key]) : [];
- }
- /**
- * Cast the given attribute to JSON.
- */
- protected function castAttributeAsJson(string $key, mixed $value): string
- {
- $value = $this->asJson($value);
- if ($value === false) {
- throw JsonEncodingException::forAttribute(
- $this,
- $key,
- json_last_error_msg()
- );
- }
- return $value;
- }
- /**
- * Encode the given value as JSON.
- */
- protected function asJson(mixed $value): false|string
- {
- return json_encode($value);
- }
- /**
- * Return a decimal as string.
- *
- * @param float $value
- * @param int $decimals
- */
- protected function asDecimal(mixed $value, mixed $decimals): string
- {
- return number_format((float) $value, (int) $decimals, '.', '');
- }
- /**
- * Return a timestamp as DateTime object with time set to 00:00:00.
- */
- protected function asDate(mixed $value): CarbonInterface
- {
- return $this->asDateTime($value)->startOfDay();
- }
- /**
- * Return a timestamp as DateTime object.
- */
- protected function asDateTime(mixed $value): CarbonInterface
- {
- // If this value is already a Carbon instance, we shall just return it as is.
- // This prevents us having to re-instantiate a Carbon instance when we know
- // it already is one, which wouldn't be fulfilled by the DateTime check.
- if ($value instanceof CarbonInterface) {
- return Carbon::instance($value);
- }
- // If the value is already a DateTime instance, we will just skip the rest of
- // these checks since they will be a waste of time, and hinder performance
- // when checking the field. We will just return the DateTime right away.
- if ($value instanceof DateTimeInterface) {
- return Carbon::parse(
- $value->format('Y-m-d H:i:s.u'),
- $value->getTimezone()
- );
- }
- // If this value is an integer, we will assume it is a UNIX timestamp's value
- // and format a Carbon object from this timestamp. This allows flexibility
- // when defining your date fields as they might be UNIX timestamps here.
- if (is_numeric($value)) {
- return Carbon::createFromTimestamp($value);
- }
- // If the value is in simply year, month, day format, we will instantiate the
- // Carbon instances from that format. Again, this provides for simple date
- // fields on the database, while still supporting Carbonized conversion.
- if ($this->isStandardDateFormat($value)) {
- return Carbon::instance(Carbon::createFromFormat('Y-m-d', $value)->startOfDay());
- }
- $format = $this->getDateFormat();
- // Finally, we will just assume this date is in the format used by default on
- // the database connection and use that format to create the Carbon object
- // that is returned back out to the developers after we convert it here.
- if (Carbon::hasFormat($value, $format)) {
- return Carbon::createFromFormat($format, $value);
- }
- return Carbon::parse($value);
- }
- /**
- * Determine if the given value is a standard date format.
- * @param mixed $value
- */
- protected function isStandardDateFormat($value)
- {
- return preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', (string) $value);
- }
- /**
- * Return a timestamp as unix timestamp.
- */
- protected function asTimestamp(mixed $value): false|int
- {
- return $this->asDateTime($value)->getTimestamp();
- }
- /**
- * Prepare a date for array / JSON serialization.
- *
- * @return string
- */
- protected function serializeDate(DateTimeInterface $date)
- {
- return $date->format($this->getDateFormat());
- }
- /**
- * Determine whether a value is Date / DateTime castable for inbound manipulation.
- */
- protected function isDateCastable(string $key): bool
- {
- return $this->hasCast($key, ['date', 'datetime']);
- }
- /**
- * Determine whether a value is JSON castable for inbound manipulation.
- */
- protected function isJsonCastable(string $key): bool
- {
- return $this->hasCast($key, ['array', 'json', 'object', 'collection']);
- }
- /**
- * Determine if the given key is cast using a custom class.
- */
- protected function isClassCastable(string $key): bool
- {
- $casts = $this->getCasts();
- if (! array_key_exists($key, $casts)) {
- return false;
- }
- $castType = $this->parseCasterClass($casts[$key]);
- if (in_array($castType, static::$primitiveCastTypes)) {
- return false;
- }
- if (class_exists($castType)) {
- return true;
- }
- throw new InvalidCastException($this::class, $key, $castType);
- }
- /**
- * Determine if the given key is cast using an enum.
- */
- protected function isEnumCastable(string $key): bool
- {
- $casts = $this->getCasts();
- if (! array_key_exists($key, $casts)) {
- return false;
- }
- $castType = $casts[$key];
- if (in_array($castType, static::$primitiveCastTypes)) {
- return false;
- }
- return enum_exists($castType);
- }
- /**
- * Resolve the custom caster class for a given key.
- */
- protected function resolveCasterClass(string $key): CastsAttributes|CastsInboundAttributes
- {
- $castType = $this->getCasts()[$key];
- $arguments = [];
- if (is_string($castType) && str_contains($castType, ':')) {
- $segments = explode(':', $castType, 2);
- $castType = $segments[0];
- $arguments = explode(',', $segments[1]);
- }
- if (is_subclass_of($castType, Castable::class)) {
- $castType = $castType::castUsing();
- }
- if (is_object($castType)) {
- return $castType;
- }
- return new $castType(...$arguments);
- }
- /**
- * Parse the given caster class, removing any arguments.
- */
- protected function parseCasterClass(string $class): string
- {
- return ! str_contains($class, ':') ? $class : explode(':', $class, 2)[0];
- }
- /**
- * Merge the cast class attributes back into the model.
- */
- protected function mergeAttributesFromClassCasts(): void
- {
- foreach ($this->classCastCache as $key => $value) {
- if ($value instanceof Synchronized && $value->isSynchronized()) {
- continue;
- }
- $caster = $this->resolveCasterClass($key);
- $this->attributes = array_merge(
- $this->attributes,
- $caster instanceof CastsInboundAttributes
- ? [$key => $value]
- : $this->normalizeCastClassResponse($key, $caster->set($this, $key, $value, $this->attributes))
- );
- }
- }
- /**
- * Normalize the response from a custom class caster.
- */
- protected function normalizeCastClassResponse(string $key, mixed $value): array
- {
- return is_array($value) ? $value : [$key => $value];
- }
- /**
- * Determine if any of the given attributes were changed.
- *
- * @param null|array|string $attributes
- */
- protected function hasChanges(array $changes, mixed $attributes = null): bool
- {
- // If no specific attributes were provided, we will just see if the dirty array
- // already contains any attributes. If it does we will just return that this
- // count is greater than zero. Else, we need to check specific attributes.
- if (empty($attributes)) {
- return count($changes) > 0;
- }
- // Here we will spin through every attribute and see if this is in the array of
- // dirty attributes. If it is, we will return true and if we make it through
- // all the attributes for the entire array we will return false at end.
- foreach (Arr::wrap($attributes) as $attribute) {
- if (array_key_exists($attribute, $changes)) {
- return true;
- }
- }
- return false;
- }
- /**
- * Transform a raw model value using mutators, casts, etc.
- */
- protected function transformModelValue(string $key, mixed $value): mixed
- {
- // If the attribute has a get mutator, we will call that then return what
- // it returns as the value, which is useful for transforming values on
- // retrieval from the model to a form that is more useful for usage.
- if ($this->hasGetMutator($key)) {
- return $this->mutateAttribute($key, $value);
- }
- // If the attribute exists within the cast array, we will convert it to
- // an appropriate native PHP type dependent upon the associated value
- // given with the key in the pair. Dayle made this comment line up.
- if ($this->hasCast($key)) {
- return $this->castAttribute($key, $value);
- }
- // If the attribute is listed as a date, we will convert it to a DateTime
- // instance on retrieval, which makes it quite convenient to work with
- // date fields without having to create a mutator for each property.
- if ($value !== null
- && \in_array($key, $this->getDates(), false)) {
- return $this->asDateTime($value);
- }
- return $value;
- }
- /**
- * Get all of the attribute mutator methods.
- *
- * @param mixed $class
- * @return array
- */
- protected static function getMutatorMethods(string $class)
- {
- preg_match_all('/(?<=^|;)get([^;]+?)Attribute(;|$)/', implode(';', get_class_methods($class)), $matches);
- return $matches[1];
- }
- }
|