Make Eloquent Faster And Safer With Laravel CRUD Wizard Free
BaseModel extra features
Even if you have a working CRUD for your project, here are some reasons to consider using laravel-crud-wizard-free lib v 5.1.0.
<?php
namespace MacropaySolutions\LaravelCrudWizard\Models;
use Carbon\Carbon;
use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use MacropaySolutions\LaravelCrudWizard\Eloquent\CustomRelations\Builders\CleverEloquentBuilder;
use MacropaySolutions\LaravelCrudWizard\Eloquent\CustomRelations\HasCleverRelationships;
use MacropaySolutions\LaravelCrudWizard\Helpers\GeneralHelper;
use MacropaySolutions\LaravelCrudWizard\Models\Attributes\BaseModelAttributesInterface;
use MacropaySolutions\LaravelCrudWizard\Models\Attributes\BaseModelLazyAttributes;
use MacropaySolutions\LaravelCrudWizard\Models\Attributes\BaseModelFrozenAttributes;
use MacropaySolutions\LaravelCrudWizard\Models\Attributes\BaseModelLazyRelations;
/**
* For autocompletion declare in the children classes (with @ property) ChildBaseModelAttributes $a
*/
abstract class BaseModel extends Model
{
use HasCleverRelationships;
Right from the start you can see this HasCleverRelationships trait that is being used to avoid/fix an eager load unsolved issue that Laravel considers a corner case but many others, including us, see it as a data leak vulnerability.
public const RESOURCE_NAME = null;
The resource name used for your CRUD routes can be guessed by the below resourceName method if not declared in your model.
public const WITH_RELATIONS = [];
This can contain the list of relations that will be exposed via API for this resource.
public const CREATED_AT_FORMAT = 'Y-m-d H:i:s';
public const UPDATED_AT_FORMAT = 'Y-m-d H:i:s';
This will be the format in which the created_at and updated_at columns are auto populated for DB storage by the lib.
public const COMPOSITE_PK_SEPARATOR = '_';
The lib supports composite primary keys as opposed to Eloquent. Change this separator if you need/want.
/**
* Setting this to true will not append the primary_key_identifier on response
* Leave it false if you use casts or any logic that alters the attributes of the model
*/
public const LIST_UN_HYDRATED_WHEN_POSSIBLE = false;
Eloquent will be used to translate the request into SQL and then the retrieval of the results is done as stdClass, skipping the hydration and improving speed, but this will happen only when the result is the same as the result of using Eloquent (except for the appended primary_key_identifier which can be added later by using laravel-crud-wizard-decorator-free lib).
public $timestamps = false;
As previously mentioned, the lib has its own logic for timestamps as datetime. You can fallback to Eloquent logic by setting this to true and overriding the boot method.
public static $snakeAttributes = false;
Our libs are following PSR12 coding style that means the relations are in camelCase format. To maintain that also on the responses and in the request, this needs to be false.
public BaseModelAttributesInterface $a;
public BaseModelAttributesInterface $r;
These 2 new properties allow segregating the model's PHP properties from its DB columns and relations to avoid clashes, allowing autocomplete, property hooks and casting via property hooks if you are using PHP >= 8.4.
protected bool $returnNullOnInvalidColumnAttributeAccess = true;
Set this to false if you want. It will mean that if you access a column or relation that does not exist and is not set in the model, you will get a PHP exception instead of null.
protected array $ignoreUpdateFor = [];
List of columns that should not be updated (see also allowNonExternalUpdatesFor).
protected array $ignoreExternalCreateFor = [];
List of columns that should not be used for creation via API (used in controller).
protected array $allowNonExternalUpdatesFor = [];
List of columns from ignoreUpdateFor that can be updated internally, so not via controller.
protected bool $indexRequiredOnFiltering = true;
If index is required on filtering, then at least one column from the filters must have an index in DB. This is to prevent queries that might timeout due to not using an index.
protected $hidden = [
'laravel_through_key'
];
laravel_through_key is used for eager loading so no need for it to be exposed via API.
/**
* Temporary cache to avoid multiple getDirty calls generating multiple set calls for
* sync/merge casted attributes to objects to persist the possible changes made to those objects
*/
protected ?array $tmpDirtyIfAttributesAreSyncedFromCashedCasts = null;
This property introduced in version 5.1.0 helps to improve eloquent speed when using casts on update operations, reducing the number of set calls discussed here that was not fixed in Laravel. This is used also for locking updates on the model when set to [].
/**
* Temporary original cache to prevent changes in created,updated,saved events from getting
* into $original without being saved into DB
*/
protected ?array $tmpOriginalBeforeAfterEvents = null;
This property introduced in version 5.1.0 fixes an unsolved issue from Laravel.
private array $incrementsToRefresh = [];
Eloquent increments the value from the model and then for DB storage it does not use that value when incrementing. That means that the value after increment can differ from the one stored in DB due to asynchronous queries. The lib refreshes the value (all from this list) from DB on its first access after the increment is done.
/**
* @inheritdoc
*/
public function __construct(array $attributes = [])
{
if ($this->incrementing) {
$this->casts[$this->getKeyName()] ??= $this->getKeyType();
}
This is to improve casts as stated in this unsolved issue from Laravel. Setting the primary key on construct is better that setting it on getCasts method because getCasts is called multiple times as described in the issue.
parent::__construct($attributes);
$this->initializeActiveRecordSegregationProperties();
$this->appends[] = 'primary_key_identifier';}
public static function resourceName(): string
{
return static::RESOURCE_NAME ?? Str::snake(Str::pluralStudly(\class_basename(static::class)), '-');
}
/**
* @inheritDoc
*/
public function newEloquentBuilder($query): CleverEloquentBuilder
{
return new CleverEloquentBuilder($query);
}
The custom Eloquent builder is needed for covering the eager load issue mentioned above.
public function getColumns(bool $includingPrimary = true): array
{
$columns = $includingPrimary ?
/** $this->primaryKey can be null or empty string so, it must not be included */
\array_merge(\array_filter([$this->primaryKey]), $this->fillable) :
/** $this->primaryKey may be in the fillable so, it must be included */
\array_diff($this->fillable, \array_diff([$this->primaryKey], $this->fillable));
if (
\function_exists('config')
&& \config('laravel_crud_wizard.LIVE_MODE') === false
&& [] !== $reservedUsed = \array_intersect(
['page', 'limit', 'cursor', 'simplePaginate', 'sort', 'sqlDebug', 'logError'],
$columns
)
) {
Log::warning('LaravelCrudWizard warning: the resource ' . $this::resourceName() .
' uses reserved words as columns: ' . \implode(',', $reservedUsed));
}
return \array_unique($columns);
}
/**
* called via $this->append(['index_required_on_filtering'])
*/
public function getIndexRequiredOnFilteringAttribute(): array
{
return $this->indexRequiredOnFiltering ? $this->retrieveFirstSeqIndexedColumns() : [];
}
/**
* called via $this->append(['primary_key_identifier'])
* @throws \Exception
*/
public function getPrimaryKeyIdentifierAttribute(): mixed
{
return $this->getKeyName() !== '' ?
$this->getAttributeValue($this->getKeyName()) :
\implode($this::COMPOSITE_PK_SEPARATOR, \array_map(
fn(mixed $value): mixed => (\is_array($value) ? \last($value) : $value),
$this->getPrimaryKeyFilter()
));
}
public function appendIndexRequiredOnFilteringAttribute(bool $appendIndex = true): static
{
return $appendIndex && $this->indexRequiredOnFiltering ? $this->append(['index_required_on_filtering']) : $this;
}
public function getIgnoreUpdateFor(): array
{
return $this->ignoreUpdateFor;
}
public function getIgnoreExternalCreateFor(): array
{
return (string)$this->getKeyName() === '' ?
$this->ignoreExternalCreateFor : \array_merge(
$this->ignoreExternalCreateFor,
\array_diff([$this->getKeyName()], $this->getFillable())
);
}
/**
* @inheritDoc
*/
public function setAttribute(mixed $key, mixed $value): mixed
{
if (!$this->exists) {
return parent::setAttribute($key, $value);
}
if (\in_array($key, \array_diff($this->ignoreUpdateFor, $this->allowNonExternalUpdatesFor), true)) {
Log::error(
'Development bug. Tried to update an ignored column ' . $key . ' on ' . \get_class($this) .
' with value: "' . $value . '" on ' . $this->getKeyName() . ' = ' . $this->getKey(
) . '. BACKTRACE: ' .
\json_encode(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3))
);
return $this;
}
if (isset($this->incrementsToRefresh[$key])) {
unset($this->incrementsToRefresh[$key]);
}
return parent::setAttribute($key, $value);
}
public static function boot(): void
{
parent::boot();
static::creating(function (BaseModel $baseModel): void {
$baseModel->setCreatedAt(Carbon::now()->format($baseModel::CREATED_AT_FORMAT));
});
static::updating(function (BaseModel $baseModel): void {
$updatedAtColumn = $baseModel->getUpdatedAtColumn();
if ('' === $baseModel->getAttribute($updatedAtColumn)) {
public function getColumns(bool $includingPrimary = true): array
{
$columns = $includingPrimary ?
/** $this->primaryKey can be null or empty string so, it must not be included */
\array_merge(\array_filter([$this->primaryKey]), $this->fillable) :
/** $this->primaryKey may be in the fillable so, it must be included */
\array_diff($this->fillable, \array_diff([$this->primaryKey], $this->fillable));
if (
\function_exists('config')
&& \config('laravel_crud_wizard.LIVE_MODE') === false
&& [] !== $reservedUsed = \array_intersect(
['page', 'limit', 'cursor', 'simplePaginate', 'sort', 'sqlDebug', 'logError'],
$columns
)
) {
Log::warning('LaravelCrudWizard warning: the resource ' . $this::resourceName() .
' uses reserved words as columns: ' . \implode(',', $reservedUsed));
}
return \array_unique($columns);
}
/**
* called via $this->append(['index_required_on_filtering'])
*/
public function getIndexRequiredOnFilteringAttribute(): array
{
return $this->indexRequiredOnFiltering ? $this->retrieveFirstSeqIndexedColumns() : [];
}
/**
* called via $this->append(['primary_key_identifier'])
* @throws \Exception
*/
public function getPrimaryKeyIdentifierAttribute(): mixed
{
return $this->getKeyName() !== '' ?
$this->getAttributeValue($this->getKeyName()) :
\implode($this::COMPOSITE_PK_SEPARATOR, \array_map(
fn(mixed $value): mixed => (\is_array($value) ? \last($value) : $value),
$this->getPrimaryKeyFilter()
));
}
public function appendIndexRequiredOnFilteringAttribute(bool $appendIndex = true): static
{
return $appendIndex && $this->indexRequiredOnFiltering ? $this->append(['index_required_on_filtering']) : $this;
}
public function getIgnoreUpdateFor(): array
{
return $this->ignoreUpdateFor;
}
public function getIgnoreExternalCreateFor(): array
{
return (string)$this->getKeyName() === '' ?
$this->ignoreExternalCreateFor : \array_merge(
$this->ignoreExternalCreateFor,
\array_diff([$this->getKeyName()], $this->getFillable())
);
}
/**
* @inheritDoc
*/
public function setAttribute(mixed $key, mixed $value): mixed
{
if (!$this->exists) {
return parent::setAttribute($key, $value);
}
if (\in_array($key, \array_diff($this->ignoreUpdateFor, $this->allowNonExternalUpdatesFor), true)) {
Log::error(
'Development bug. Tried to update an ignored column ' . $key . ' on ' . \get_class($this) .
' with value: "' . $value . '" on ' . $this->getKeyName() . ' = ' . $this->getKey(
) . '. BACKTRACE: ' .
\json_encode(debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3))
);
return $this;
}
if (isset($this->incrementsToRefresh[$key])) {
unset($this->incrementsToRefresh[$key]);
}
return parent::setAttribute($key, $value);
}
public static function boot(): void
{
parent::boot();
static::creating(function (BaseModel $baseModel): void {
$baseModel->setCreatedAt(Carbon::now()->format($baseModel::CREATED_AT_FORMAT));
});
static::updating(function (BaseModel $baseModel): void {
$updatedAtColumn = $baseModel->getUpdatedAtColumn();
if ('' === $baseModel->getAttribute($updatedAtColumn)) {
When setting updated_at to empty string, it being datetime or null, means that you will update the row without changing its value.
$baseModel->setUpdatedAt($baseModel->getOriginal($updatedAtColumn));
return;
}
$baseModel->setUpdatedAt(Carbon::now()->format($baseModel::UPDATED_AT_FORMAT));
});
}
/**
* @throws \Exception
*/
public function getPrimaryKeyFilter(): array
{
if ($this->getKeyName() === '') {
throw new \Exception('Development error. getPrimaryKeyFilter function not defined for this model.');
}
return [
[$this->getKeyName(), $this->getAttributeValue($this->getKeyName())],
];
}
/**
* @inheritdoc
*/
public function getKey()
{
return $this->getPrimaryKeyIdentifierAttribute();
}
/**
* Overwrite this for sqlite and sqlsrv db drivers if needed
* @throws \Throwable
*/
public function retrieveIndexesFromTable(): Collection
{
if (!\in_array($driver = $this->getConnection()->getDriverName(), ['mariadb', 'mysql', 'pgsql'], true)) {
throw new \Exception('Unsupported database driver ' . $driver . ' for retrieving indexes.');
}
static $result;
$callback =
/**
* @throws \Throwable
*/
function () use ($driver): array {
$tableName = $this->getConnection()->getTablePrefix() . $this->getTable();
return DB::connection($this->getConnectionName())->select([
'mariadb' => 'SHOW INDEX FROM ' . $tableName,
'mysql' => 'SHOW INDEX FROM ' . $tableName,
'pgsql' => "SELECT
array_position(ix.indkey, a.attnum) + 1 as Seq_in_index,
i.relname as Key_name,
a.attname as Column_names
from
pg_class t,
pg_class i,
pg_index ix,
pg_attribute a
where
t.oid = ix.indrelid
and i.oid = ix.indexrelid
and a.attrelid = t.oid
and a.attnum = any(ix.indkey)
and t.relkind = 'r'
and t.relname = " . $tableName,
][$driver]);
};
try {
return $result[$this->getConnectionName() . $this->getTable()] ??= \collect(Cache::remember(
$this::resourceName() . 'IndexesForFiltering',
Carbon::now()->addDay(),
$callback
));
} catch (\Throwable) {
return \collect($callback());
}
}
public function retrieveFirstSeqIndexedColumns(): array
{
static $indexes;
try {
$result = $indexes[$this->getConnectionName() . $this->getTable()] ??= \array_values(\array_unique(
$this->retrieveIndexesFromTable()->where('Seq_in_index', 1)->pluck('Column_name')->all()
));
} catch (\Throwable $e) {
if (GeneralHelper::isDebug()) {
Log::error($this::class . ' error getting indexes: ' . $e->getMessage());
}
}
if (($result ??= []) === [] && $this->indexRequiredOnFiltering) {
$this->indexRequiredOnFiltering = false;
}
return $result;
}
/**
* @throws \Throwable
*/
public function getColumnIndex(string $column, bool $asFirst = false): string
{
$collection = $this->retrieveIndexesFromTable()->where('Column_name', $column);
if ($asFirst) {
$collection->where('Seq_in_index', 1);
}
return $collection->firstOrFail()->Key_name;
}
public function getIndexedColumns(bool $includingPrimary = true): array
{
return $includingPrimary ?
$this->retrieveFirstSeqIndexedColumns() :
\array_diff($this->retrieveFirstSeqIndexedColumns(), [$this->primaryKey]);
}
public function isIndexRequiredOnFiltering(): bool
{
return $this->indexRequiredOnFiltering;
}
/**
* @inheritDoc
*/
protected function setKeysForSelectQuery($query)
{
return $this->setKeysForSaveQuery($query);
}
/**
* @inheritDoc
*/
protected function setKeysForSaveQuery($query)
{
return $query->where($this->getPrimaryKeyFilter());
}
/**
* @throws \Exception
*/
public function getFrozen(): BaseModelFrozenAttributes
{
$frozenAttributes =
\substr($class = static::class, 0, $l = (-1 * (\strlen($class) - \strrpos($class, '\\') - 1))) .
'Attributes\\' . \substr($class, $l) . 'FrozenAttributes';
if (\class_exists($frozenAttributes)) {
return new $frozenAttributes((clone $this)->forceFill($this->toArray()));
}
throw new \Exception('Class not found: ' . $frozenAttributes);
}
$baseModel->setUpdatedAt($baseModel->getOriginal($updatedAtColumn));
return;
}
$baseModel->setUpdatedAt(Carbon::now()->format($baseModel::UPDATED_AT_FORMAT));
});
}
/**
* @throws \Exception
*/
public function getPrimaryKeyFilter(): array
{
if ($this->getKeyName() === '') {
throw new \Exception('Development error. getPrimaryKeyFilter function not defined for this model.');
}
return [
[$this->getKeyName(), $this->getAttributeValue($this->getKeyName())],
];
}
/**
* @inheritdoc
*/
public function getKey()
{
return $this->getPrimaryKeyIdentifierAttribute();
}
/**
* Overwrite this for sqlite and sqlsrv db drivers if needed
* @throws \Throwable
*/
public function retrieveIndexesFromTable(): Collection
{
if (!\in_array($driver = $this->getConnection()->getDriverName(), ['mariadb', 'mysql', 'pgsql'], true)) {
throw new \Exception('Unsupported database driver ' . $driver . ' for retrieving indexes.');
}
static $result;
$callback =
/**
* @throws \Throwable
*/
function () use ($driver): array {
$tableName = $this->getConnection()->getTablePrefix() . $this->getTable();
return DB::connection($this->getConnectionName())->select([
'mariadb' => 'SHOW INDEX FROM ' . $tableName,
'mysql' => 'SHOW INDEX FROM ' . $tableName,
'pgsql' => "SELECT
array_position(ix.indkey, a.attnum) + 1 as Seq_in_index,
i.relname as Key_name,
a.attname as Column_names
from
pg_class t,
pg_class i,
pg_index ix,
pg_attribute a
where
t.oid = ix.indrelid
and i.oid = ix.indexrelid
and a.attrelid = t.oid
and a.attnum = any(ix.indkey)
and t.relkind = 'r'
and t.relname = " . $tableName,
][$driver]);
};
try {
return $result[$this->getConnectionName() . $this->getTable()] ??= \collect(Cache::remember(
$this::resourceName() . 'IndexesForFiltering',
Carbon::now()->addDay(),
$callback
));
} catch (\Throwable) {
return \collect($callback());
}
}
public function retrieveFirstSeqIndexedColumns(): array
{
static $indexes;
try {
$result = $indexes[$this->getConnectionName() . $this->getTable()] ??= \array_values(\array_unique(
$this->retrieveIndexesFromTable()->where('Seq_in_index', 1)->pluck('Column_name')->all()
));
} catch (\Throwable $e) {
if (GeneralHelper::isDebug()) {
Log::error($this::class . ' error getting indexes: ' . $e->getMessage());
}
}
if (($result ??= []) === [] && $this->indexRequiredOnFiltering) {
$this->indexRequiredOnFiltering = false;
}
return $result;
}
/**
* @throws \Throwable
*/
public function getColumnIndex(string $column, bool $asFirst = false): string
{
$collection = $this->retrieveIndexesFromTable()->where('Column_name', $column);
if ($asFirst) {
$collection->where('Seq_in_index', 1);
}
return $collection->firstOrFail()->Key_name;
}
public function getIndexedColumns(bool $includingPrimary = true): array
{
return $includingPrimary ?
$this->retrieveFirstSeqIndexedColumns() :
\array_diff($this->retrieveFirstSeqIndexedColumns(), [$this->primaryKey]);
}
public function isIndexRequiredOnFiltering(): bool
{
return $this->indexRequiredOnFiltering;
}
/**
* @inheritDoc
*/
protected function setKeysForSelectQuery($query)
{
return $this->setKeysForSaveQuery($query);
}
/**
* @inheritDoc
*/
protected function setKeysForSaveQuery($query)
{
return $query->where($this->getPrimaryKeyFilter());
}
/**
* @throws \Exception
*/
public function getFrozen(): BaseModelFrozenAttributes
{
$frozenAttributes =
\substr($class = static::class, 0, $l = (-1 * (\strlen($class) - \strrpos($class, '\\') - 1))) .
'Attributes\\' . \substr($class, $l) . 'FrozenAttributes';
if (\class_exists($frozenAttributes)) {
return new $frozenAttributes((clone $this)->forceFill($this->toArray()));
}
throw new \Exception('Class not found: ' . $frozenAttributes);
}
This method can be used to get the model as stdClass like read only DTO.
public function shouldReturnNullOnInvalidColumnAttributeAccess(): bool
{
return $this->returnNullOnInvalidColumnAttributeAccess;
}
/**
* @inheritdoc
* @throws \Exception
*/
public function getAttribute($key): mixed
{
if ((string)$key === '') {
return null;
}
if (\in_array($key, $this->getColumns(), true)) {
return $this->getAttributeValue($key);
}
return parent::getAttribute($key);
}
/**
* @inheritDoc
* @throws \Exception
* @see static::returnNullOnInvalidColumnAttributeAccess
*/
public function getAttributeValue($key): mixed
{
if ($this->exists && isset($this->incrementsToRefresh[$key])) {
$this->attributes = \array_merge(
$this->attributes,
(array)($this->setKeysForSelectQuery($this->newQueryWithoutScopes())
->useWritePdo()
->select($attributes = \array_keys($this->incrementsToRefresh))
->first()
?->toArray())
);
$this->syncOriginalAttributes($attributes);
$this->incrementsToRefresh = [];
}
$return = $this->transformModelValue($key, $this->getAttributeFromArray($key, true));
public function shouldReturnNullOnInvalidColumnAttributeAccess(): bool
{
return $this->returnNullOnInvalidColumnAttributeAccess;
}
/**
* @inheritdoc
* @throws \Exception
*/
public function getAttribute($key): mixed
{
if ((string)$key === '') {
return null;
}
if (\in_array($key, $this->getColumns(), true)) {
return $this->getAttributeValue($key);
}
return parent::getAttribute($key);
}
/**
* @inheritDoc
* @throws \Exception
* @see static::returnNullOnInvalidColumnAttributeAccess
*/
public function getAttributeValue($key): mixed
{
if ($this->exists && isset($this->incrementsToRefresh[$key])) {
$this->attributes = \array_merge(
$this->attributes,
(array)($this->setKeysForSelectQuery($this->newQueryWithoutScopes())
->useWritePdo()
->select($attributes = \array_keys($this->incrementsToRefresh))
->first()
?->toArray())
);
$this->syncOriginalAttributes($attributes);
$this->incrementsToRefresh = [];
}
$return = $this->transformModelValue($key, $this->getAttributeFromArray($key, true));
Not calling the parent here is part of the improvement in casting from this discussion.
if (
$return !== null
|| $this->returnNullOnInvalidColumnAttributeAccess
|| \in_array($key, $this->getColumns(), true)
) {
return $return;
}
/** @see static::transformModelValue() */
if (!$this->hasGetMutator($key) && !$this->hasAttributeGetMutator($key)) {
try {
return $this->attributes[$key];
} catch (\Throwable) {
throw new \Exception('Undefined attribute: ' . $key . ' in model: ' . static::class);
}
}
return null;
}
public function shouldEscapeWhenCastingToString(): bool
{
return $this->escapeWhenCastingToString;
}
public function attributeOffsetUnset(string $offset): void
{
unset($this->attributes[$offset]);
}
public function attributeOffsetExists(string $offset): bool
{
return isset($this->attributes[$offset]);
}
if (
$return !== null
|| $this->returnNullOnInvalidColumnAttributeAccess
|| \in_array($key, $this->getColumns(), true)
) {
return $return;
}
/** @see static::transformModelValue() */
if (!$this->hasGetMutator($key) && !$this->hasAttributeGetMutator($key)) {
try {
return $this->attributes[$key];
} catch (\Throwable) {
throw new \Exception('Undefined attribute: ' . $key . ' in model: ' . static::class);
}
}
return null;
}
public function shouldEscapeWhenCastingToString(): bool
{
return $this->escapeWhenCastingToString;
}
public function attributeOffsetUnset(string $offset): void
{
unset($this->attributes[$offset]);
}
public function attributeOffsetExists(string $offset): bool
{
return isset($this->attributes[$offset]);
}
The above offset methods are needed for segregating the active record properties from its DB columns.
/**
* @inheritDoc
* @throws \Exception
* @see static::returnNullOnInvalidColumnAttributeAccess
*/
public function getRelationValue($key): mixed
{
$value = parent::getRelationValue($key);
if (
$value === null
&& !$this->isRelation($key)
&& !$this->returnNullOnInvalidColumnAttributeAccess
) {
throw new \Exception('Undefined relation: ' . $key . ' in model: ' . static::class);
}
return $value;
}
/**
* @inheritdoc
*/
public function getCasts()
{
return $this->casts;
}
/**
* @inheritDoc
* @throws \Exception
* @see static::returnNullOnInvalidColumnAttributeAccess
*/
public function getRelationValue($key): mixed
{
$value = parent::getRelationValue($key);
if (
$value === null
&& !$this->isRelation($key)
&& !$this->returnNullOnInvalidColumnAttributeAccess
) {
throw new \Exception('Undefined relation: ' . $key . ' in model: ' . static::class);
}
return $value;
}
/**
* @inheritdoc
*/
public function getCasts()
{
return $this->casts;
}
As mentioned above, the override is needed for speeding up Eloquent. See the __construct method.
/**
* This will mass update the whole table if the model does not exist!
* @inheritDoc
* @throws \InvalidArgumentException
*/
protected function incrementOrDecrement($column, $amount, $extra, $method): int
{
if (!$this->exists) {
return $this->newQueryWithoutRelationships()->{$method}($column, $amount, $extra);
}
$this->{$column} = $this->isClassDeviable($column)
? $this->deviateClassCastableAttribute($method, $column, $amount)
: (\extension_loaded('bcmath') ? \bcadd(
$s1 = (string)$this->{$column},
$s2 = (string)($method === 'increment' ? $amount : $amount * -1),
\max(\strlen(\strrchr($s1, '.') ?: ''), \strlen(\strrchr($s2, '.') ?: ''))
) : $this->{$column} + ($method === 'increment' ? $amount : $amount * -1));
$this->forceFill($extra);
if (!$this->isDirty() || $this->fireModelEvent('updating') === false) {
return 0;
}
/**
* This will mass update the whole table if the model does not exist!
* @inheritDoc
* @throws \InvalidArgumentException
*/
protected function incrementOrDecrement($column, $amount, $extra, $method): int
{
if (!$this->exists) {
return $this->newQueryWithoutRelationships()->{$method}($column, $amount, $extra);
}
$this->{$column} = $this->isClassDeviable($column)
? $this->deviateClassCastableAttribute($method, $column, $amount)
: (\extension_loaded('bcmath') ? \bcadd(
$s1 = (string)$this->{$column},
$s2 = (string)($method === 'increment' ? $amount : $amount * -1),
\max(\strlen(\strrchr($s1, '.') ?: ''), \strlen(\strrchr($s2, '.') ?: ''))
) : $this->{$column} + ($method === 'increment' ? $amount : $amount * -1));
$this->forceFill($extra);
if (!$this->isDirty() || $this->fireModelEvent('updating') === false) {
return 0;
}
Calling isDirty here will merge the casted attributes into objects back from those objects and also will help in preventing the model update when it is locked. More details can be found in this discussion.
return (int)tap(
$this->setKeysForSaveQuery($this->newQueryWithoutScopes())->{$method}($column, $amount, $extra),
function () use ($column) {
$this->syncChanges();
$this->fireModelEvent('updated', false);
$this->syncOriginalAttributes(\array_keys($this->changes));
$this->incrementsToRefresh[$column] = true;
}
);
}
/**
* @see static::a
* @see static::p
*/
protected function initializeActiveRecordSegregationProperties(): void
{
$this->a = new BaseModelLazyAttributes($this);
$this->r = new BaseModelLazyRelations($this);
}
return (int)tap(
$this->setKeysForSaveQuery($this->newQueryWithoutScopes())->{$method}($column, $amount, $extra),
function () use ($column) {
$this->syncChanges();
$this->fireModelEvent('updated', false);
$this->syncOriginalAttributes(\array_keys($this->changes));
$this->incrementsToRefresh[$column] = true;
}
);
}
/**
* @see static::a
* @see static::p
*/
protected function initializeActiveRecordSegregationProperties(): void
{
$this->a = new BaseModelLazyAttributes($this);
$this->r = new BaseModelLazyRelations($this);
}
To improve speed, by default the target attribute/relation classes will not be dynamically sought and instantiated until their first usage. If you want (or if you want to place them in a different place in your folder structure than in an Attributes folder next to your model location) you can override this method and instantiate them directly.
/**
* @inheritdoc
* @throws \BadMethodCallException
*/
public function __call($method, $parameters)
{
$lowerMethod = \strtolower($method);
if ($lowerMethod === 'getattributefromarray') {
/**
* @inheritdoc
* @throws \BadMethodCallException
*/
public function __call($method, $parameters)
{
$lowerMethod = \strtolower($method);
if ($lowerMethod === 'getattributefromarray') {
This is needed for improving casts. More details can be found in this discussion.
return $this->$method(...$parameters);
}
if (\in_array($lowerMethod, ['incrementeach', 'decrementeach'], true)) {
/** \MacropaySolutions\MaravelRestWizard\Models\BaseModel::incrementBulk can be used instead */
throw new \BadMethodCallException(\sprintf(
'Call to unscoped method %s::%s(). Use $model->newQuery()->getQuery()->%s()' .
' for unscoped or $model->newQuery()->%s() for scoped behavior.',
static::class,
$method,
$method,
$method,
));
return $this->$method(...$parameters);
}
if (\in_array($lowerMethod, ['incrementeach', 'decrementeach'], true)) {
/** \MacropaySolutions\MaravelRestWizard\Models\BaseModel::incrementBulk can be used instead */
throw new \BadMethodCallException(\sprintf(
'Call to unscoped method %s::%s(). Use $model->newQuery()->getQuery()->%s()' .
' for unscoped or $model->newQuery()->%s() for scoped behavior.',
static::class,
$method,
$method,
$method,
));
This is to prevent yet again an uncovered corner case from Laravel reported in these: issue 57262, issue 49009, issue 48595.
}
return parent::__call($method, $parameters);
}
/**
* @inheritdoc
*/
public function newCollection(array $models = []): \Illuminate\Database\Eloquent\Collection
{
return \function_exists('arrayUniqueSortRegular')
&& \Composer\InstalledVersions::isInstalled('macropay-solutions/maravel-framework') ?
parent::newCollection($models) :
new class ($models) extends \Illuminate\Database\Eloquent\Collection {
/**
* @inheritdoc
*/
public function toBase(): Collection
{
return new class ($this) extends Collection {
/**
* Return only unique items from the collection array.
*
* @param mixed $key
* @param bool $strict
* @param int $flags [optional] <p>
* The optional second parameter sort_flags
* may be used to modify the sorting behavior using these values:
* </p>
* <p>
* Sorting type flags:
* </p><ul>
* <li>
* <b>SORT_REGULAR</b> - compare items normally
* (don't change types)
* </li>
* <li>
* <b>SORT_NUMERIC</b> - compare items numerically
* </li>
* <li>
* <b>SORT_STRING</b> - compare items as strings
* </li>
* <li>
* <b>SORT_LOCALE_STRING</b> - compare items as strings,
* based on the current locale
* </li>
* </ul>
* @return static
*/
public function unique($key = null, $strict = false): Collection
{
if ($key === null && $strict === false) {
$flags = \func_get_args()[2] ?? SORT_REGULAR;
return new static(
SORT_REGULAR === $flags ?
GeneralHelper::arrayUniqueSortRegular($this->items) :
\array_unique($this->items, $flags)
);
}
return parent::unique($key, $strict);
}
};
}
};
}
}
return parent::__call($method, $parameters);
}
/**
* @inheritdoc
*/
public function newCollection(array $models = []): \Illuminate\Database\Eloquent\Collection
{
return \function_exists('arrayUniqueSortRegular')
&& \Composer\InstalledVersions::isInstalled('macropay-solutions/maravel-framework') ?
parent::newCollection($models) :
new class ($models) extends \Illuminate\Database\Eloquent\Collection {
/**
* @inheritdoc
*/
public function toBase(): Collection
{
return new class ($this) extends Collection {
/**
* Return only unique items from the collection array.
*
* @param mixed $key
* @param bool $strict
* @param int $flags [optional] <p>
* The optional second parameter sort_flags
* may be used to modify the sorting behavior using these values:
* </p>
* <p>
* Sorting type flags:
* </p><ul>
* <li>
* <b>SORT_REGULAR</b> - compare items normally
* (don't change types)
* </li>
* <li>
* <b>SORT_NUMERIC</b> - compare items numerically
* </li>
* <li>
* <b>SORT_STRING</b> - compare items as strings
* </li>
* <li>
* <b>SORT_LOCALE_STRING</b> - compare items as strings,
* based on the current locale
* </li>
* </ul>
* @return static
*/
public function unique($key = null, $strict = false): Collection
{
if ($key === null && $strict === false) {
$flags = \func_get_args()[2] ?? SORT_REGULAR;
return new static(
SORT_REGULAR === $flags ?
GeneralHelper::arrayUniqueSortRegular($this->items) :
\array_unique($this->items, $flags)
);
}
return parent::unique($key, $strict);
}
};
}
};
}
Again fixing unsolved issues from PHP: php/php-src/issues/20262#issuecomment-3441217772 and Laravel laravel/framework/issues/57528.
/**
* Prevent updates
* Note that relations can be loaded and updated during the lock
*/
public function lockUpdates(bool $checkDirty = true): bool
{
if (
!$this->exists
|| $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts !== null
|| ($checkDirty && $this->isDirty())
) {
return false;
}
$this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = [];
return true;
}
/**
* Unlock updates
*
* To reset the model's $attributes and get the changes from dirty applied during the lock use:
*
* if ($this->unlockUpdates()) {
* $dirty = $this->getDirty();
* $this->attributes = $this->original;
* $this->classCastCache = [];
* $this->attributeCastCache = [];
* }
*
* Note that relations can be loaded during the lock
*/
public function unlockUpdates(): bool
{
if ($this->hasUnlockedUpdates()) {
return false;
}
$this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null;
return true;
}
public function hasUnlockedUpdates(): bool
{
return $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts !== [];
}
/**
* Prevent updates
* Note that relations can be loaded and updated during the lock
*/
public function lockUpdates(bool $checkDirty = true): bool
{
if (
!$this->exists
|| $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts !== null
|| ($checkDirty && $this->isDirty())
) {
return false;
}
$this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = [];
return true;
}
/**
* Unlock updates
*
* To reset the model's $attributes and get the changes from dirty applied during the lock use:
*
* if ($this->unlockUpdates()) {
* $dirty = $this->getDirty();
* $this->attributes = $this->original;
* $this->classCastCache = [];
* $this->attributeCastCache = [];
* }
*
* Note that relations can be loaded during the lock
*/
public function unlockUpdates(): bool
{
if ($this->hasUnlockedUpdates()) {
return false;
}
$this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null;
return true;
}
public function hasUnlockedUpdates(): bool
{
return $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts !== [];
}
The above 3 methods for preventing the model updates where introduced in version 5.1.0 and can be used when needed.
The below code was added in the same version for improving casts as detailed in this discussion.
The below code was added in the same version for improving casts as detailed in this discussion.
/**
* Get all of the current attributes on the model.
* @param bool $withoutMergeAttributesFromCachedCasts
* @return array
*/
public function getAttributes(): array
{
if (true !== (\func_get_args()[0] ?? null)) {
$this->mergeAttributesFromCachedCasts();
}
return $this->attributes;
}
/**
* @inheritdoc
*/
protected function getAttributesForInsert(): array
{
return $this->tmpOriginalBeforeAfterEvents = $this->getAttributes();
}
/**
* @inheritdoc
*/
public function syncOriginalAttributes($attributes): static
{
$attributes = is_array($attributes) ? $attributes : func_get_args();
foreach ($attributes as $attribute) {
$this->original[$attribute] = $this->getAttributeFromArray($attribute);
}
return $this;
}
/**
* @inheritdoc
*/
public function isDirty($attributes = null): bool
{
return [] !== $this->getDirty(\is_array($attributes) ? $attributes : \func_get_args());
}
/**
* Get the attributes that have been changed since the last sync.
* @param string|array $attributes
* @return array
*/
public function getDirty(): array
{
$args = \func_get_args();
$attributes = (array)($args[0] ?? []);
if (isset($this->tmpDirtyIfAttributesAreSyncedFromCashedCasts)) {
return [] !== $attributes ?
\array_intersect_key($this->tmpDirtyIfAttributesAreSyncedFromCashedCasts, \array_flip($attributes)) :
$this->tmpDirtyIfAttributesAreSyncedFromCashedCasts;
}
$dirty = [];
if ([] === $attributes) {
foreach ($this->getAttributes() as $key => $value) {
if (!$this->originalIsEquivalent($key)) {
$dirty[$key] = $value;
}
}
return $dirty;
}
foreach ($attributes as $key) {
/** This will call $this->mergeAttributesFromCachedCasts($key) before the if condition */
$value = $this->getAttributeFromArray($key);
if (!$this->originalIsEquivalent($key)) {
$dirty[$key] = $value;
}
}
return $dirty;
}
/**
* @inheritdoc
*/
public function save(array $options = []): bool
{
$query = $this->newModelQuery();
// If the "saving" event returns false we'll bail out of the save and return
// false, indicating that the save failed. This provides a chance for any
// listeners to cancel save operations if validations fail or whatever.
/** Saving event might change the model so, no point in calling $this->mergeAttributesFromCachedCasts() before */
if ($this->fireModelEvent('saving') === false) {
return false;
}
if ($this->exists) {
/** $this->getDirty() will merge/sync attributes from cached casts objects */
$isDirty = [] !== ($dirty = $this->getDirty());
if (!$isDirty) {
/** don't call saved event like in Laravel: https://github.com/laravel/framework/issues/56254 */
return true;
}
try {
/** We will try to optimize the execution by caching $dirty array BUT,
multiple set calls will be needed (https://github.com/laravel/framework/discussions/31778)
WHEN $this->usesTimestamps() or WHEN there are listeners for updating event because they can do changes
so, $this->getDirtyForUpdate() and $this->syncChanges() will call $this->getDirty() which will call
getAttributes() */
if (!$this->getEventDispatcher()->hasListeners('eloquent.updating: ' . $this::class)) {
$this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = $dirty;
unset($dirty);
}
if ($this->performUpdate($query)) {
$this->finishSave($options + ['touch' => $isDirty]);
return true;
}
return false;
} finally {
$this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null;
$this->tmpOriginalBeforeAfterEvents = null;
}
}
/** $this->isDirty() will merge/sync attributes from cached casts objects */
$isDirty = $this->isDirty();
/** Multiple set calls are needed (https://github.com/laravel/framework/discussions/31778) because:
- $this->performInsert can do changes,
- creating event can do changes so,
$this->getAttributesForInsert() and $this->syncOriginal() will call $this->getAttributes() */
try {
$saved = $this->performInsert($query);
if (
'' === (string)$this->getConnectionName() &&
($connection = $query->getConnection()) instanceof Connection
) {
$this->setConnection($connection->getName());
}
if ($saved) {
$this->finishSave($options + ['touch' => $isDirty]);
}
} finally {
$this->tmpOriginalBeforeAfterEvents = null;
}
return $saved;
}
/**
* @inheritdoc
*/
protected function finishSave(array $options): void
{
$this->fireModelEvent('saved', false);
if ($options['touch'] ?? true) {
$this->touchOwners();
}
if (isset($this->tmpOriginalBeforeAfterEvents)) {
$this->original = $this->tmpOriginalBeforeAfterEvents;
$this->tmpOriginalBeforeAfterEvents = null;
return;
}
$this->syncOriginal();
}
/**
* @inheritdoc
*/
protected function performUpdate(Builder $query): bool
{
// If the updating event returns false, we will cancel the update operation so
// developers can hook Validation systems into their models and cancel this
// operation if the model does not pass validation. Otherwise, we update.
if ($this->fireModelEvent('updating') === false) {
return false;
}
// First we need to create a fresh query instance and touch the creation and
// update timestamp on the model which are maintained by us for developer
// convenience. Then we will just continue saving the model instances.
if ($this->usesTimestamps()) {
$this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null;
$this->updateTimestamps();
}
// Once we have run the update operation, we will fire the "updated" event for
// this model instance. This will allow developers to hook into these after
// models are updated, giving them a chance to do any special processing.
if ([] === $dirty = $this->getDirtyForUpdate()) {
return false;
}
$this->setKeysForSaveQuery($query)->update($dirty);
$this->syncChanges();
$this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null;
$this->tmpOriginalBeforeAfterEvents = $this->attributes;
$this->fireModelEvent('updated', false);
return true;
}
/**
* @inheritdoc
*/
protected function insertAndSetId(Builder $query, $attributes): void
{
$id = $query->insertGetId($attributes, $keyName = $this->getKeyName());
$this->setAttribute($keyName, $id);
$this->tmpOriginalBeforeAfterEvents = $this->attributes;
}
/**
* Get the attributes that have been changed since the last sync for an update operation.
*
* @return array
*/
protected function getDirtyForUpdate(): array
{
return $this->getDirty();
}
/**
* Get an attribute from the $attributes array without transformation
* @see self::getAttributeValue
*
* @param string $key
* @param bool $mergeAllAttributesFromCachedCasts
* @return mixed
*/
protected function getAttributeFromArray($key): mixed
{
if (
true === (\func_get_args()[1] ?? false)
&& ($this->hasGetMutator($key) || $this->hasAttributeGetMutator($key))
) {
return $this->getAttributes()[$key] ?? null;
}
$this->mergeAttributesFromClassCasts($key);
$this->mergeAttributesFromAttributeCasts($key);
return $this->attributes[$key] ?? null;
}
/**
* Merge the cast class attributes back into the model.
* @param string|array $keys
* @return void
*/
protected function mergeAttributesFromClassCasts(): void
{
$k = \func_get_args()[0] ?? null;
$classCastCache = \is_string($k) || \is_array($k) ?
\array_intersect_key($this->classCastCache, \array_flip(\array_values((array)$k))) :
$this->classCastCache;
foreach ($classCastCache as $key => $value) {
$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))
);
}
}
/**
* Merge the cast class attributes back into the model.
* @param string|array $keys
* @return void
*/
protected function mergeAttributesFromAttributeCasts(): void
{
$k = \func_get_args()[0] ?? null;
$attributeCastCache = \is_string($k) || \is_array($k) ?
\array_intersect_key($this->attributeCastCache, \array_flip(\array_values((array)$k))) :
$this->attributeCastCache;
foreach ($attributeCastCache as $key => $value) {
$attribute = $this->{Str::camel($key)}();
if ($attribute->get && !$attribute->set) {
continue;
}
$callback = $attribute->set ?: function ($value) use ($key) {
$this->attributes[$key] = $value;
};
$this->attributes = array_merge(
$this->attributes,
$this->normalizeCastClassResponse(
$key,
$callback($value, $this->attributes)
)
);
}
}
}
/**
* Get all of the current attributes on the model.
* @param bool $withoutMergeAttributesFromCachedCasts
* @return array
*/
public function getAttributes(): array
{
if (true !== (\func_get_args()[0] ?? null)) {
$this->mergeAttributesFromCachedCasts();
}
return $this->attributes;
}
/**
* @inheritdoc
*/
protected function getAttributesForInsert(): array
{
return $this->tmpOriginalBeforeAfterEvents = $this->getAttributes();
}
/**
* @inheritdoc
*/
public function syncOriginalAttributes($attributes): static
{
$attributes = is_array($attributes) ? $attributes : func_get_args();
foreach ($attributes as $attribute) {
$this->original[$attribute] = $this->getAttributeFromArray($attribute);
}
return $this;
}
/**
* @inheritdoc
*/
public function isDirty($attributes = null): bool
{
return [] !== $this->getDirty(\is_array($attributes) ? $attributes : \func_get_args());
}
/**
* Get the attributes that have been changed since the last sync.
* @param string|array $attributes
* @return array
*/
public function getDirty(): array
{
$args = \func_get_args();
$attributes = (array)($args[0] ?? []);
if (isset($this->tmpDirtyIfAttributesAreSyncedFromCashedCasts)) {
return [] !== $attributes ?
\array_intersect_key($this->tmpDirtyIfAttributesAreSyncedFromCashedCasts, \array_flip($attributes)) :
$this->tmpDirtyIfAttributesAreSyncedFromCashedCasts;
}
$dirty = [];
if ([] === $attributes) {
foreach ($this->getAttributes() as $key => $value) {
if (!$this->originalIsEquivalent($key)) {
$dirty[$key] = $value;
}
}
return $dirty;
}
foreach ($attributes as $key) {
/** This will call $this->mergeAttributesFromCachedCasts($key) before the if condition */
$value = $this->getAttributeFromArray($key);
if (!$this->originalIsEquivalent($key)) {
$dirty[$key] = $value;
}
}
return $dirty;
}
/**
* @inheritdoc
*/
public function save(array $options = []): bool
{
$query = $this->newModelQuery();
// If the "saving" event returns false we'll bail out of the save and return
// false, indicating that the save failed. This provides a chance for any
// listeners to cancel save operations if validations fail or whatever.
/** Saving event might change the model so, no point in calling $this->mergeAttributesFromCachedCasts() before */
if ($this->fireModelEvent('saving') === false) {
return false;
}
if ($this->exists) {
/** $this->getDirty() will merge/sync attributes from cached casts objects */
$isDirty = [] !== ($dirty = $this->getDirty());
if (!$isDirty) {
/** don't call saved event like in Laravel: https://github.com/laravel/framework/issues/56254 */
return true;
}
try {
/** We will try to optimize the execution by caching $dirty array BUT,
multiple set calls will be needed (https://github.com/laravel/framework/discussions/31778)
WHEN $this->usesTimestamps() or WHEN there are listeners for updating event because they can do changes
so, $this->getDirtyForUpdate() and $this->syncChanges() will call $this->getDirty() which will call
getAttributes() */
if (!$this->getEventDispatcher()->hasListeners('eloquent.updating: ' . $this::class)) {
$this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = $dirty;
unset($dirty);
}
if ($this->performUpdate($query)) {
$this->finishSave($options + ['touch' => $isDirty]);
return true;
}
return false;
} finally {
$this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null;
$this->tmpOriginalBeforeAfterEvents = null;
}
}
/** $this->isDirty() will merge/sync attributes from cached casts objects */
$isDirty = $this->isDirty();
/** Multiple set calls are needed (https://github.com/laravel/framework/discussions/31778) because:
- $this->performInsert can do changes,
- creating event can do changes so,
$this->getAttributesForInsert() and $this->syncOriginal() will call $this->getAttributes() */
try {
$saved = $this->performInsert($query);
if (
'' === (string)$this->getConnectionName() &&
($connection = $query->getConnection()) instanceof Connection
) {
$this->setConnection($connection->getName());
}
if ($saved) {
$this->finishSave($options + ['touch' => $isDirty]);
}
} finally {
$this->tmpOriginalBeforeAfterEvents = null;
}
return $saved;
}
/**
* @inheritdoc
*/
protected function finishSave(array $options): void
{
$this->fireModelEvent('saved', false);
if ($options['touch'] ?? true) {
$this->touchOwners();
}
if (isset($this->tmpOriginalBeforeAfterEvents)) {
$this->original = $this->tmpOriginalBeforeAfterEvents;
$this->tmpOriginalBeforeAfterEvents = null;
return;
}
$this->syncOriginal();
}
/**
* @inheritdoc
*/
protected function performUpdate(Builder $query): bool
{
// If the updating event returns false, we will cancel the update operation so
// developers can hook Validation systems into their models and cancel this
// operation if the model does not pass validation. Otherwise, we update.
if ($this->fireModelEvent('updating') === false) {
return false;
}
// First we need to create a fresh query instance and touch the creation and
// update timestamp on the model which are maintained by us for developer
// convenience. Then we will just continue saving the model instances.
if ($this->usesTimestamps()) {
$this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null;
$this->updateTimestamps();
}
// Once we have run the update operation, we will fire the "updated" event for
// this model instance. This will allow developers to hook into these after
// models are updated, giving them a chance to do any special processing.
if ([] === $dirty = $this->getDirtyForUpdate()) {
return false;
}
$this->setKeysForSaveQuery($query)->update($dirty);
$this->syncChanges();
$this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null;
$this->tmpOriginalBeforeAfterEvents = $this->attributes;
$this->fireModelEvent('updated', false);
return true;
}
/**
* @inheritdoc
*/
protected function insertAndSetId(Builder $query, $attributes): void
{
$id = $query->insertGetId($attributes, $keyName = $this->getKeyName());
$this->setAttribute($keyName, $id);
$this->tmpOriginalBeforeAfterEvents = $this->attributes;
}
/**
* Get the attributes that have been changed since the last sync for an update operation.
*
* @return array
*/
protected function getDirtyForUpdate(): array
{
return $this->getDirty();
}
/**
* Get an attribute from the $attributes array without transformation
* @see self::getAttributeValue
*
* @param string $key
* @param bool $mergeAllAttributesFromCachedCasts
* @return mixed
*/
protected function getAttributeFromArray($key): mixed
{
if (
true === (\func_get_args()[1] ?? false)
&& ($this->hasGetMutator($key) || $this->hasAttributeGetMutator($key))
) {
return $this->getAttributes()[$key] ?? null;
}
$this->mergeAttributesFromClassCasts($key);
$this->mergeAttributesFromAttributeCasts($key);
return $this->attributes[$key] ?? null;
}
/**
* Merge the cast class attributes back into the model.
* @param string|array $keys
* @return void
*/
protected function mergeAttributesFromClassCasts(): void
{
$k = \func_get_args()[0] ?? null;
$classCastCache = \is_string($k) || \is_array($k) ?
\array_intersect_key($this->classCastCache, \array_flip(\array_values((array)$k))) :
$this->classCastCache;
foreach ($classCastCache as $key => $value) {
$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))
);
}
}
/**
* Merge the cast class attributes back into the model.
* @param string|array $keys
* @return void
*/
protected function mergeAttributesFromAttributeCasts(): void
{
$k = \func_get_args()[0] ?? null;
$attributeCastCache = \is_string($k) || \is_array($k) ?
\array_intersect_key($this->attributeCastCache, \array_flip(\array_values((array)$k))) :
$this->attributeCastCache;
foreach ($attributeCastCache as $key => $value) {
$attribute = $this->{Str::camel($key)}();
if ($attribute->get && !$attribute->set) {
continue;
}
$callback = $attribute->set ?: function ($value) use ($key) {
$this->attributes[$key] = $value;
};
$this->attributes = array_merge(
$this->attributes,
$this->normalizeCastClassResponse(
$key,
$callback($value, $this->attributes)
)
);
}
}
}
