Entity lifecycle hooks¶
Small Swoole Entity Manager provides optional, interface-based lifecycle callbacks. An entity opts in by implementing one or more interfaces from Small\SwooleEntityManager\Entity\Contract. The manager invokes the corresponding methods automatically; application code does not need to call them, register listeners or dispatch events.
Version availability
Automatic OnNewInterface and OnLoadInterface dispatch was added to Core master in this change. Confirm that the version installed through Composer contains this change. Earlier published Core 3 versions may expose the interfaces without automatically invoking these two hooks. The persistence callbacks below already existed before that change.
All supported callbacks¶
Each interface is independent. Implement only the callbacks your entity needs. All callback methods have a void return type.
| Interface | Method | Automatically invoked |
|---|---|---|
OnNewInterface |
onNew(): void |
Once during a normal $manager->newEntity(), after manager defaults have been applied |
OnLoadInterface |
onLoad(): void |
After database-backed entity hydration (dbValue: true), including the original-primary-key snapshot |
BeforePersistInterface |
beforePersist(): void |
Before either an insert or an update initiated by persist() |
BeforeInsertInterface |
beforeInsert(): void |
Immediately before the insert |
AfterInsertInterface |
afterInsert(): void |
After a successful insert, once the entity has been marked as persisted |
BeforeUpdateInterface |
beforeUpdate(): void |
Immediately before updating an existing entity |
AfterUpdateInterface |
afterUpdate(): void |
After a successful update |
AfterPersistInterface |
afterPersist(): void |
After the insert/update flow completes |
BeforeDeleteInterface |
beforeDelete(): void |
Immediately before deleting the entity |
AfterDeleteInterface |
afterDelete(): void |
After a successful delete |
Execution order¶
newEntity()
-> apply manager field defaults
-> onNew() [only if implemented]
persist() on a new entity
-> beforePersist()
-> beforeInsert()
-> INSERT
-> afterInsert()
-> afterPersist()
Load from the database
-> create entity without onNew()
-> hydrate database fields/relations
-> record original primary keys
-> onLoad() [only if implemented]
persist() on an existing entity
-> beforePersist()
-> beforeUpdate()
-> UPDATE
-> afterUpdate()
-> afterPersist()
delete()
-> beforeDelete()
-> DELETE
-> afterDelete()
The insert or update path is chosen using the entity's persisted state (fromDb). The beforePersist() / afterPersist() pair surrounds both paths. Delete does not execute persist hooks.
Callbacks are synchronous in the manager's execution path. An exception in a callback propagates; it does not automatically provide a database transaction or rollback.
Complete example: initialization, hydration and persistence¶
This example uses an application-owned identifier so the initialization hook can assign it automatically. The transient loadedFromDatabase property is intentionally not a database field.
<?php
declare(strict_types=1);
namespace App\Entity;
use Small\SwooleEntityManager\Entity\AbstractEntity;
use Small\SwooleEntityManager\Entity\Attribute\Field;
use Small\SwooleEntityManager\Entity\Attribute\NoLastInsertId;
use Small\SwooleEntityManager\Entity\Attribute\OrmEntity;
use Small\SwooleEntityManager\Entity\Attribute\PrimaryKey;
use Small\SwooleEntityManager\Entity\Contract\BeforePersistInterface;
use Small\SwooleEntityManager\Entity\Contract\OnLoadInterface;
use Small\SwooleEntityManager\Entity\Contract\OnNewInterface;
use Small\SwooleEntityManager\Entity\Enum\FieldValueType;
#[OrmEntity]
#[NoLastInsertId]
final class Note extends AbstractEntity implements
OnNewInterface,
OnLoadInterface,
BeforePersistInterface
{
#[PrimaryKey]
private ?string $id = null;
#[Field(type: FieldValueType::string)]
private ?string $title = null;
#[Field(type: FieldValueType::dateTime)]
private ?\DateTime $createdAt = null;
#[Field(type: FieldValueType::dateTime)]
private ?\DateTime $updatedAt = null;
private bool $loadedFromDatabase = false;
public function onNew(): void
{
$this->id = bin2hex(random_bytes(16));
$this->createdAt = new \DateTime();
}
public function onLoad(): void
{
$this->loadedFromDatabase = true;
}
public function beforePersist(): void
{
$this->updatedAt = new \DateTime();
}
public function getId(): ?string
{
return $this->id;
}
public function wasLoadedFromDatabase(): bool
{
return $this->loadedFromDatabase;
}
public function setTitle(string $title): static
{
$this->title = $title;
return $this;
}
}
For example, with a NoteManager configured for Note::class:
/** @var \App\Entity\Note $note */
$note = $noteManager->newEntity(); // onNew() runs once
$note->setTitle('Example')->persist(); // beforePersist(), then INSERT
/** @var \App\Entity\Note $loaded */
$loaded = $noteManager->findOneBy(['id' => $note->getId()]);
// Database hydration has called onLoad() on $loaded.
assert($loaded->wasLoadedFromDatabase());
Do not call $note->onNew() manually. The callback's signature must remain void, as required by OnNewInterface; fluent setters can still be chained on the entity returned by newEntity().
Creation versus hydration¶
The manager deliberately uses different paths to avoid resetting identifiers and creation timestamps when reading existing rows.
newEntity(): applies the manager's default field values, then invokesonNew()if implemented.makeEntityFromStdClass($data)ormakeEntityFromArray($data)with the defaultdbValue: false: maps input data without invokingonNew()oronLoad().makeEntityFromStdClass($data, dbValue: true)and its array counterpart: create the entity withoutonNew(), convert database values, hydrate fields/relations and finally invokeonLoad().- Standard database reads through manager query results use the database hydration path, so an entity implementing
OnLoadInterfaceis notified without an explicit controller call.
newEntity(initialize: false) is available for internal hydration paths. Normal application code should use newEntity() instead.
Keep lifecycle methods focused
onNew() is suitable for application-generated IDs, initial timestamps and initial field values. onLoad() is suitable for transient, non-persisted state derived from a loaded entity. Avoid unbounded side effects, database writes or explicitly calling other lifecycle methods from these hooks. In particular, calling onNew() on a loaded object can overwrite existing values.
Persistence hooks¶
Implement any combination of BeforePersistInterface, BeforeInsertInterface, BeforeUpdateInterface, AfterInsertInterface, AfterUpdateInterface and AfterPersistInterface. No registration is necessary.
use Small\SwooleEntityManager\Entity\Contract\BeforeInsertInterface;
use Small\SwooleEntityManager\Entity\Contract\BeforeUpdateInterface;
final class ExampleEntity extends AbstractEntity implements
BeforeInsertInterface,
BeforeUpdateInterface
{
// ORM fields and the other entity members are omitted for brevity.
public function beforeInsert(): void
{
// Prepare values required only on the first insert.
}
public function beforeUpdate(): void
{
// Prepare values required only before an update.
}
}
The Before* methods execute before their database operation; After* methods execute after it completes normally. Exceptions are not silently swallowed. Use explicit transaction handling when an operation requires atomicity across multiple database actions.
For deletion, BeforeDeleteInterface and AfterDeleteInterface are invoked by the entity deletion path, independently of insert/update and persist hooks.
Bulk update and delete operations¶
Set-based builders execute directly against the database by default. They do not hydrate entities or dispatch per-entity callbacks.
$manager->executeUpdate($updateBuilder); // Bulk SQL, hooks skipped
$manager->executeDelete($deleteBuilder); // Bulk SQL, hooks skipped
To invoke callbacks for matching entities, opt into the entity-by-entity execution path:
$manager->executeUpdate($updateBuilder, skipTriggers: false);
$manager->executeDelete($deleteBuilder, skipTriggers: false);
This can be substantially slower for large result sets because the manager selects and processes matching entities individually. Use it when hook behavior is required; otherwise use the native bulk path. See Update builder and Delete builder.