Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Phaseolies/Auth/Security/Authenticate.php
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ public function logout(): void

if ($user && $user->getRememberToken()) {
$user->setRememberToken(null);
$user::withoutHook();
$user->withoutHook();
$user->save();
}

Expand Down
135 changes: 135 additions & 0 deletions src/Phaseolies/Database/Entity/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,16 @@ class Builder
*/
protected bool $takeWithoutEncryption = true;

/**
* @var bool
*/
protected bool $withoutHooks = false;

/**
* @var bool
*/
protected bool $withHooks = false;

/**
* @var bool
*/
Expand Down Expand Up @@ -163,6 +173,103 @@ public function __construct(PDO $pdo, string $table, string $modelClass, int $ro
$this->connectionName = $connectionName;
}

/**
* Skip model hooks for the next write on every model this builder
* creates or loads.
*
* @return self
*/
public function withoutHook(): self
{
$this->withoutHooks = true;

return $this;
}

/**
* Run model hooks for update(), delete(), restore() and forceDelete()
* by writing each matching model on its own.
*
* @return self
*/
public function withHooks(): self
{
$this->withHooks = true;

return $this;
}

/**
* Run the callback on each matching model, in chunks by primary key.
* Returns false if any callback returned false.
*
* @param \Closure(Model): bool $callback
* @return bool
*/
private function eachModelWithHooks(\Closure $callback): bool
{
$ok = true;

$this->chunkById(500, function ($models) use ($callback, &$ok) {
foreach ($models as $model) {
if ($callback($model) === false) {
$ok = false;
}
}
});

return $ok;
}

/**
* Update the first model matching the attributes, or create it.
*
* @param array $attributes
* @param array $values
* @return Model
*/
public function updateOrCreate(array $attributes, array $values = []): Model
{
$model = $this->firstMatching($attributes);

if ($model) {
$model->fill($values);
$model->save();

return $model;
}

return $this->create(array_merge($attributes, $values));
}

/**
* Retrieve the first model matching the attributes, or create it.
*
* @param array $attributes
* @param array $values
* @return Model
*/
public function firstOrCreate(array $attributes, array $values = []): Model
{
return $this->firstMatching($attributes)
?? $this->create(array_merge($attributes, $values));
}

/**
* @param array $attributes
* @return Model|null
*/
private function firstMatching(array $attributes): ?Model
{
$query = clone $this;

foreach ($attributes as $field => $value) {
$query->where($field, $value);
}

return $query->first();
}

/**
* Create a new model instance and save it to the database
*
Expand All @@ -174,6 +281,10 @@ public function create(array $attributes): Model
$modelClass = $this->modelClass;
$model = new $modelClass();

if ($this->withoutHooks) {
$model->withoutHook();
}

if ($this->connectionName !== null) {
$model->setConnectionName($this->connectionName);
}
Expand Down Expand Up @@ -984,6 +1095,10 @@ private function fetchLazy(): \Generator
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$model = app($this->modelClass, [$row]);

if ($this->withoutHooks) {
$model->withoutHook();
}

if ($this->connectionName !== null) {
$model->setConnectionName($this->connectionName);
}
Expand Down Expand Up @@ -2119,6 +2234,10 @@ public function first()

$model = new $this->modelClass($result);

if ($this->withoutHooks) {
$model->withoutHook();
}

if ($this->connectionName !== null) {
$model->setConnectionName($this->connectionName);
}
Expand Down Expand Up @@ -2582,6 +2701,12 @@ public function upsert(array $values, array|string $uniqueBy, ?array $updateColu
*/
public function update(array $attributes): bool
{
if ($this->withHooks) {
return $this->eachModelWithHooks(
fn (Model $model) => $model->update($attributes)
);
}

$class = $this->modelClass;

if (app($class)->usesTimestamps()) {
Expand Down Expand Up @@ -2641,6 +2766,12 @@ public function from(string $table): self
*/
public function delete(): bool
{
if ($this->withHooks) {
return $this->eachModelWithHooks(
fn (Model $model) => $model->delete()
);
}

if ($this->shouldSoftDelete()) {
$now = (string) now();

Expand Down Expand Up @@ -2692,6 +2823,10 @@ public function useRaw(string $sql, array $bindings = []): Collection
$models = array_map(function ($item) {
$model = new $this->modelClass($item);

if ($this->withoutHooks) {
$model->withoutHook();
}

if ($this->connectionName !== null) {
$model->setConnectionName($this->connectionName);
}
Expand Down
19 changes: 16 additions & 3 deletions src/Phaseolies/Database/Entity/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
use Phaseolies\Database\Contracts\Support\Jsonable;
use Phaseolies\Database\Entity\Attributes\Hook;

/**
* @method static \Phaseolies\Database\Entity\Builder withoutHook()
*/
abstract class Model implements Jsonable, \ArrayAccess, \JsonSerializable, \Stringable
{
use InteractsWithModelQueryProcessing, InteractsWithTemporal, InteractsWithCasting, InteractsWithWatches, InteractsWithComputedProperties, InteractsWithSoftDeletes;
Expand Down Expand Up @@ -770,11 +773,13 @@ public function offsetUnset($offset): void
public function delete(): bool
{
if (!isset($this->attributes[$this->primaryKey])) {
$this->hooksSuppressed = false;

return false;
}

try {
if (self::$isHookShouldBeCalled && $this->fireBeforeHooks('deleted') === false) {
if (! $this->hooksSuppressed && $this->fireBeforeHooks('deleted') === false) {
return false;
}

Expand All @@ -788,13 +793,13 @@ public function delete(): bool
$result = $this->usesSoftDeletes() ? $query->forceDelete() : $query->delete();
}

if ($result && self::$isHookShouldBeCalled) {
if ($result && ! $this->hooksSuppressed) {
$this->fireAfterHooks('deleted');
}

return $result;
} finally {
self::$isHookShouldBeCalled = true;
$this->hooksSuppressed = false;
}
}

Expand Down Expand Up @@ -1247,6 +1252,14 @@ public function decrement(string $column, int $amount = 1, array $extra = []): i
*/
public function __call($method, $parameters)
{
// withoutHook() skips hooks for the next write on this instance only
// and returns the instance, so $model->withoutHook()->save() is safe.
if ($method === 'withoutHook') {
$this->hooksSuppressed = true;

return $this;
}

return $this->newQuery()->$method(...$parameters);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
trait InteractsWithModelQueryProcessing
{
/**
* Whether hooks are skipped for the next write on this instance.
*
* @var bool
*/
protected static bool $isHookShouldBeCalled = true;
protected bool $hooksSuppressed = false;

/**
* Creates and returns a new query builder instance for the model.
Expand All @@ -35,18 +37,6 @@ public static function query(?string $connection = null): Builder
);
}

/**
* Disable the execution of model hooks for the current instance.
*
* @return \Phaseolies\Database\Entity\Builder
*/
public static function withoutHook(): Builder
{
self::$isHookShouldBeCalled = false;

return static::query();
}

/**
* Retrieves all records from the model's table.
*
Expand Down Expand Up @@ -167,7 +157,7 @@ public function save(): bool
if ($isUpdatable) {
$this->assertCreatableIsDefined();

if (self::$isHookShouldBeCalled && $this->fireBeforeHooks('updated') === false) {
if (! $this->hooksSuppressed && $this->fireBeforeHooks('updated') === false) {
return false;
}

Expand All @@ -191,7 +181,7 @@ public function save(): bool
->where($this->primaryKey, $this->attributes[$this->primaryKey])
->update($dirtyAttributes);

if (self::$isHookShouldBeCalled && $response) {
if (! $this->hooksSuppressed && $response) {
$this->pruneNonColumnDirtyAttributes($this->getDirtyAttributes());
$this->fireAfterHooks('updated');
$this->firePropertyWatches($dirtyAttributes);
Expand All @@ -201,7 +191,7 @@ public function save(): bool
return $response;
}

if (self::$isHookShouldBeCalled && $this->fireBeforeHooks('created') === false) {
if (! $this->hooksSuppressed && $this->fireBeforeHooks('created') === false) {
return false;
}

Expand All @@ -224,13 +214,13 @@ public function save(): bool
// model via getKey() instead of null.
$this->attributes[$this->primaryKey] = $id;

if (self::$isHookShouldBeCalled) {
if (! $this->hooksSuppressed) {
$this->fireAfterHooks('created');
}

return true;
} finally {
self::$isHookShouldBeCalled = true;
$this->hooksSuppressed = false;
}
}

Expand Down Expand Up @@ -478,6 +468,8 @@ public static function match(array|callable $filters): Builder
public function update(array $attributes): bool
{
if (!isset($this->attributes[$this->primaryKey])) {
$this->hooksSuppressed = false;

return false;
}

Expand All @@ -489,7 +481,7 @@ public function update(array $attributes): bool
$this->setAttribute($key, $value);
}

if (self::$isHookShouldBeCalled && $this->fireBeforeHooks('updated') === false) {
if (! $this->hooksSuppressed && $this->fireBeforeHooks('updated') === false) {
return false;
}

Expand All @@ -499,6 +491,8 @@ public function update(array $attributes): bool
$dirty = array_intersect_key($dirty, array_flip($this->creatable));

if (empty($dirty)) {
$this->hooksSuppressed = false;

return true;
}

Expand All @@ -514,14 +508,14 @@ public function update(array $attributes): bool

if ($result) {
$this->pruneNonColumnDirtyAttributes($this->getDirtyAttributes());
if (self::$isHookShouldBeCalled) {
if (! $this->hooksSuppressed) {
$this->fireAfterHooks('updated');
$this->firePropertyWatches($dirty);
}
$this->originalAttributes = $this->attributes;
}
} finally {
self::$isHookShouldBeCalled = true;
$this->hooksSuppressed = false;
}

return $result;
Expand Down
Loading
Loading