Reglas generales para Observers
Orden de eventos preferido: saving → creating/updating → created/updated
Si una lógica siempre se ejecuta en cambios del modelo, modifica datos relacionados y debe mantenerse centralizada → pertenece a un Observer.
Los jobs y efectos secundarios que siempre deben ejecutarse después de que se guarda un modelo (p. ej., sincronizar con un servicio externo) pertenecen al hook saved del observer, no al llamador. El componente Livewire o el controller nunca deben despachar jobs en nombre de un modelo, ni deben llamar efectos secundarios directamente sobre modelos relacionados. Las acciones de UI que eliminan o cambian datos relacionados solo deben actualizar el estado pendiente local — el observer se encarga de la persistencia al guardar.
// Correct — observer owns the side effect
public function saved(Plan $plan)
{
SyncWithExternalServiceJob::dispatch($plan)->afterCommit();
}
// Incorrect — caller dispatches the job
public function save()
{
$this->object->save();
SyncWithExternalServiceJob::dispatch($this->object)->afterCommit(); // wrong place
}
Cuando un observer tiene más de 4 métodos en total, organízalos con comentarios de sección usando el mismo estilo /* */ que en los modelos: Live Events (hooks que se ejecutan antes de la escritura en la BD: saving, creating, updating, deleting) → Events (hooks que se ejecutan después: saved, created, updated, deleted) → Methods (todos los helpers private). Incluye solo las secciones que estén presentes — no agregues una sección vacía. No agregues comentarios de sección cuando el observer tenga 4 métodos o menos.
Los métodos hook del observer (saving, updating, saved, etc.) deben delegar a métodos private con nombre cuando la lógica tenga 3 o más líneas, o cuando la misma lógica se llame desde múltiples hooks. Las operaciones de una sola línea deben quedarse inline — extraerlas a un método private agrega indirección sin aportar beneficio.
// Correct — 1 line stays inline
public function creating(BillingPrice $price): void
{
$price->stripe_price_id = app(StripeSyncService::class)->syncPrice($price);
}
// Correct — 3+ lines get a named private method
public function updating(BillingPrice $price): void
{
$this->preventAmountModification($price);
}
private function preventAmountModification(BillingPrice $price): void
{
if ($price->isDirty('amount')) {
throw CannotModifyCreatedPriceAmount::for($price);
}
}
// Incorrect — 1-line logic extracted unnecessarily
public function creating(BillingPrice $price): void
{
$this->assignStripePriceId($price);
}
private function assignStripePriceId(BillingPrice $price): void
{
$price->stripe_price_id = app(StripeSyncService::class)->syncPrice($price);
}