Why Are Model Observers in Laravel a Bad Practice?

Dmitry Khorev
4 min readJul 7, 2022

--

Laravel provides an interesting way to automate common model events inside your app with dispatched events, closure events, and observers.

While it sounds cool to have this kind of plug-and-play solution — there are certain cases when this will backfire on your project if you tend to overload this feature with business logic.

TL;DR

  • I think observers and model events are fine for MVP and/or smaller projects.
  • When you have 2+ devs working and/or 100+ test cases — they can become an issue (not absolutely will).
  • For very large projects that will be an issue for sure. You would need to spend a lot of time refactoring, QAing, and regress-testing your app. So think ahead and refactor early.
  • Reason: model events create hidden side effects, sometimes unexpected and not required by the executed action.

The most common side effects can be observed when writing and running Unit tests and Feature tests for your Laravel app. This article will demonstrate this scenario.

Our example

Processing temperature measures from IoT devices, storing them in a database and doing some additional calculations after each sample is consumed.

Our business requirements:

  • store a sample consumed via exposed API
  • for each stored sample update and average temperature for the last 10 measures

This is our Sample model and migration:

Now every time we store a sample we want to store the average temperature for the last 10 samples in another model, Avg Temperature:

We can achieve this simply by attaching an event to the created state of the Sample model:

Now we add a listener with average recalculation logic:

Now, our naive controller implementation, **skipping validation and any good development patterns**, would look like this:

We can also write a feature test that confirms that our API route works as expected — sample is stored and avg sample is stored:

Test run results
Test run results

That looks perfectly fine, right?

Now when things go wrong

Imagine a second developer on your team is going to write a Unit test where he wants to check average temperature calculations.

He extracts a service from the listener class to do this job:

He has this unit test written where he wants to seed 100 samples at once at 1-minute intervals:

Test fails
The test fails on line 28

This is a pretty simple example and can be fixed by disabling the model event or faking the whole Event facade on an ad-hoc basis.

Event::fake();orSample::unsetEventDispatcher();

For any more or less large project such options are painful — you always need to remember your model creates side effects.

Imagine such an event creates side effects in another database or an external service via API call. Every time you create a sample with a factory you’d have to deal with mocking external calls.

What we have here is a combination of a bad development pattern of model events and not enough code decoupling.

Refactoring and decoupling our example

For a better visibility, we will create a second set of models in our project and a new route.

First, we remove the model event from our Sample model, now it looks like this:

Then we create a service that will be responsible for consuming new samples:

Notice our service is now responsible for firing an event in case of success.

Our new route handler will look like this:

Request class:

Now we replicate our second developer’s tests with the new route and can confirm it passes:

Passing unit test

Conclusion

What was improved:

  • We decoupled our controller from the database model.
  • We decoupled sample processing (business logic) from the framework.
  • Firing of SampleCreatedEvent is more controllable and will not trigger when not expected.

How this helps:

  • Developers are happier when working with your code.
  • You can now mock sample processing when testing the sample controller.
  • CI/CD runs faster and costs less as we don’t do unnecessary work (valid for large projects).

The repository with code can be found here: https://github.com/dkhorev/model-observers-bad-practice.

--

--

Dmitry Khorev

Sharing my experience in software engineering (NestJS, Laravel, System Design, DevOps)