Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Model Observers Laravel Package

rdehnhardt/model-observers

Laravel package that streamlines Eloquent model observers: register observers directly on a model with minimal boilerplate instead of wiring them in service providers, keeping boot logic clean and event handling easy to discover.

View on GitHub
Deep Wiki
Context7

Getting Started

This package is a thin convenience layer over Laravel's built-in observer registration (Model::observe()). Instead of wiring every observer into AppServiceProvider::boot(), you declare observers alongside the model they observe. The win is locality: the model and its side effects live in one file.

1. Install

composer require rdehnhardt/model-observers

There is no config file to publish and no service provider registration to worry about for a typical install (Laravel's package auto-discovery handles it). Confirm the exact trait/interface name in the README before writing code — with a 16-star package the surface area is small and the readme is the documentation.

2. Look here first

  • README.md — the entire public API is likely one trait plus one static property or attribute.
  • The package's src/ directory — it's small enough to read end-to-end in a few minutes. You want to see where the registration happens (service provider boot() vs. trait boot-style hook) because that determines when it runs.
  • Your model's booted() / boot() methods — understand how this package's registration interleaves with Laravel's own model booting.

3. First use case

Take an existing observer you already registered manually in AppServiceProvider and move it onto the model:

// app/Models/User.php
use Rdehnhardt\ModelObservers\...; // exact trait from README

class User extends Model
{
    // typically something like:
    protected static $observers = [
        UserObserver::class,
    ];

    // and the trait that reads that property
}

If the package supports PHP 8 attributes, the equivalent is a class-level attribute on the model. Remove the User::observe(UserObserver::class) line from your service provider and verify events still fire (see Gotchas — they may fire twice if you forget this step).

The right first use case is a genuinely side-effect-heavy model: sending a welcome email on created, writing an audit row on updated, purging a cache on deleted. Don't start with an observer that only logs.

Implementation Patterns

Keep observers single-purpose. One observer per concern, not one god-observer per model. UserObserver for notifications, UserAuditObserver for history, UserCacheObserver for invalidation. The package's registration style (list of classes) makes this cheap — you list several classes instead of one. It also makes it trivial to disable one concern in a test.

Wrap registration behind a base model. If you're adopting this package across many models, put the trait on a shared BaseModel (or a HasObservedEvents trait of your own that composes the package's trait). Then every model opts in by declaring the property, and the registration mechanism stays swappable if you later migrate to Laravel's native #[ObservedBy] attribute.

Prefer queued observers for anything slow. The package only changes registration, not execution — observers still run synchronously inside the model event. Implement ShouldQueue on the observer (or use ObservedBy-style queued observers where supported) so an email or HTTP call doesn't sit inside your create() transaction and hold row locks. Configure $afterCommit = true (or the queue connection's after_commit) so queued observers see committed data.

Test observers in isolation from the model. Because registration is declarative, you can assert the wiring in a cheap unit test:

public function test_user_model_registers_expected_observers(): void
{
    // Assert the property/attribute, or assert the event dispatcher
    // has listeners after booting the model.
    $this->assertContains(UserObserver::class, User::observers());
}

Then test the observer class directly by instantiating it and passing a model — no HTTP, no database factory needed for most cases. Keep a small number of full integration tests that assert "creating a user actually sends the mail", because those catch registration breakage the unit test can't.

Use Event::fake() carefully with observers. Observers are dispatched through the event dispatcher, so Event::fake() will suppress them. If you need to fake one event but keep observers running, prefer Event::fake([SomeOtherEvent::class]) (partial faking) over a blank Event::fake().

Integration tip — legacy migration. On an existing codebase, move observers one model at a time. Keep the AppServiceProvider registrations in place, add the property to the model, and confirm the observer does not fire twice (Laravel will happily register duplicate listeners). Once confirmed, delete the provider line. This ordering prevents a window where nothing is registered.

Gotchas and Tips

Double registration is the #1 failure mode. If you add the observer to the model and leave User::observe(UserObserver::class) in a service provider, every event fires the observer twice. Symptoms: duplicate emails, duplicate audit rows, duplicate queue jobs. Search your providers for the observer class name during code review. This package does not deduplicate against Laravel's own registration.

Mass operations bypass observers entirely. User::where(...)->update([...]) and ->delete() fire no model events, so no observers. This is a Laravel behaviour, not a package bug, but it's the most common "why didn't my observer run?" question. If you need to react to bulk changes, either chunk through models (->each->update()) or use database triggers / an explicit service method. Document this in the observer's docblock.

Timing and transactions. Observers run inside the transaction that triggered them. If the transaction rolls back after your observer sent an email, you've sent an email for a row that doesn't exist. Use DB::afterCommit() or queued observers with afterCommit for irreversible side effects. Conversely, an observer that reads the just-created model is safe — the row exists within the transaction.

Booting order vs. traits. If the package registers observers in a service provider's boot(), everything is registered before any HTTP request handling begins — fine. If it registers lazily via a trait boot hook, watch for models that are resolved before the provider boots (rare, but possible with package service providers that touch models in their own register()). Symptom: an observer silently not firing on the very first model interaction of a request, typically in an artisan command or a package boot script.

retrieved and N+1. Registering a retrieved observer is tempting for hydration-time logic, but it fires on every row fetched. A single query returning 5,000 rows means 5,000 observer invocations. If you must, keep the handler to a property check with no I/O and no relation loading.

Extending the package. Since it's small and MIT-licensed, the escape hatch is to fork or copy the registration trait into app/Support/ if you need different semantics (e.g., observer priority, conditional registration based on config). Reading src/ first will tell you whether that's a 30-line file — it almost certainly is for a package of this size. Don't fight the package with elaborate workarounds; vendoring a small trait is cheaper.

Debugging checklist when an observer "doesn't run": (1) Is the model actually using the trait / carrying the property? (2) Did you forget to remove the old provider registration, or conversely forget to add it if this package relies on the provider? (3) Is the operation a mass update/delete? (4) Is Event::fake() active in the test? (5) Did the observer throw an exception that was swallowed by a try/catch higher up — wrap the observer body in a log line temporarily to confirm entry.

Version pinning. At 16 stars and a 0.21 relevance score, this is not a high-traffic package. Pin the version in composer.json and skim the changelog before composer update. If you're on Laravel 9+ you have the native #[ObservedBy] attribute, which does the same job with no third-party dependency — treat this package as a migration stepping stone and keep the observer classes framework-agnostic so swapping the registration mechanism is a one-line change per model.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator