Don't use eloquent!

... everywhere...

I know, this is a bit clikbaity title, but what the hack, at least I have your attention.

So I am a really big fan of Laravel. I work with the framework for almost 10 years now, where I use it not only for my hobby projects, but during my 9-5 job for complex high-availability corporate web applications.

I aim to use well-known design patterns and follow Laravel defaults whereever possible. This practice allows me to create predictable solutions for common problems, that are easy to communicate with peers, and with my feature self when I pick up a project or feature months or even years later.

When working on (new) features my standard operation procedure is something like this:

  1. Collect the requirements and write out some details in text;
  2. Note down some domain specific terms, processes and actors;
  3. Write the test cases that describe the required behaviour;

Now from here my approach is a bit changing. Most of the time my nex step would be to create the Eloquent models and the database migrations. This would not only lay the basic foundation for my database structure, but also to define the domain models.

If you are like me and have some decent experience with Laraven, followed a lot of tutorials, and spend significant time on the documenation, you are probably familiar with this command:

$ php artisan laraval make:model MyModel -a

In Laravel 10, this will generate the following:

  • a database migration
  • a model seeder
  • a model factory
  • a policy class,
  • a resource controller
  • form request classes

This is super convenient and helps us to stub out a lot of boilerplate code while maintaining strict naming conventions and using well known patterns for common problems.

We are able to create json api's and CRUD's in no time and in a few hours we can deliver a working goodlooking prototype.

Each controller follows the predictable REST structure for resources: 1 model per controller with predictable GET, POST, PUT, DELETE endpoints to manage a model.

And this works really really well, specially for simple CRUD based application, but also for most of of my more complex projects. Laravels route model binding makes it even possible that you might not need to write a single query, but just use Eloquent methods like User::create(), $user->update() or $user->delete(). When using Laravel's API resources or Blade templates, you can pass the models and use its attributes directly: $user->name, $user->email. And this is all super convenient and works very well.

Eloquent is super powerful. It can layout relations, like $post->comments() or $post->author() in code and you would still not need to write a single query to use this data in your application.

You can use $post->author->name, $user->posts->count() and $posts->comments->each(), to get related data throughout your whole application.

And this what most of us probably do:

  • We create our resources;
  • We stub out our database and model relations;
  • We TDD our features (You do TDD right?);
  • And we will use our Eloquent models as our domain models everywhere.

And it all works damn good!

Out client is super happy, as we where able to delivery a beautiful application. All requirements are implemented, client makes money, we make money and our invoices are paid on the same day of sending.

Because of our wizardry our client makes tons of money and they wisely decide to spend it on more features. Which makes us happy as well, as we can build more awesome stuff and even get paid for it.

Feature on feature gets delivered with lightspeed. New models and migrations are created with ease, we TDD our application to tens - or even hundred - of new resources and eloquent relations. Model relations get more and more complex, but we don't blink an eye as we know our tech stack and follow our lessons and examples in the documentation.

Fast forward a few years.

Client is driving a huge car, has hired a big team, praise you daily with new work and takes you to dinner whenever he can. As the project grew, we have coupled various third-party services like payment providers, accounting software, email providers and various external API's. We behaved like real professionals and decoupled our code as much as possible, grouped code in new modules and wrote some clever classes dat follow Laravel documentation and we even applied our SOLID knowledge whereever we can.

But.

Where every we need to display or work with database data, we used Eloquent everywhere.

Our blade files shows listings of eager loaded collections like:

@foreach($user->orders as $order)
{{ $order->created_at->format('Y-m-d') }}
@foreach($order->products as $product)
{{ $product->name }} {{ $product->price }}
@foreach($product->variations as $variation)
{{ $variation->color }}
@endforeach
@foreach($product->discounts as $discount)
{{ $discount->percentage }}
@endforeach

@endforeach
@endforeach

We do the same in our "Service" classes.

When we need to create some more complex features that does not fit in a request/controller combination, we write some nice classes, where we can pass our models like:

class PaymentHandler
{
public function createPayment(Order $order)
{
return PaymentProvider::createPayment($order->id, $order->total);
}
}

We create nice emails and PDF invoices with all the models that are involved in an invoice:

  • Order
  • OrderLines
  • Customer
  • InvoiceAddress
  • ShippingAddress
  • Discounts
  • ShippingCosts
  • PaymentStatus
  • SalesTax

We trust our application because we can easily find our source data if something goes wrong and we need to debug something:

$product->price is not correct? We quickly check the price column in our products table.

The shipping address postal code is wrong? Let's do a quick search in the addresses table and check the postal_code column.

It is impossible to get lost and every Laravel developer we needed to hire to get the done understands what is happening.

Change is the only constant in life

Things change...

Requirements change, tax laws change, our skills improve, team size grows, framework updates, new techniques evolve and the horror: supplier change and api's change.

At some point you WILL have to make changes. You WILL have to reuse features. And you WILL have to change new API connections as clients swap accounting software, ditch their underperforming fullfilment partners and change their payment providers for a cheaper one.

Your database tables and column will change. As your team grows, new ways of working are agreed upon. As we create new features, technical debt is a fact of live and we do need to refactor code.

As our knowledge of the business emerge we find out the names we choose for you project where not ideal. When we talk with our client we speak about Items instead of Products. Customers are not alway only Customers, but also Suppliers. Our client refers to them as Partners and Relations.

In the niche it is accustomed to talk about Zipcode instead of Postalcode. All this causes confusion in the development team but also in the communication with the client and his team.

As complexity grew and we had some changes in the team, we reached to a point where we have to refactor some large part of the code, change some confusing naming and swap out some obsolete implementations.

As a project grows, all of this will happen at some point.