Streamlining Laravel
Main Thread • 7 min read
Lately more and more people have asked me, "What would I change in Laravel?"
Before answering, let me say, I love Laravel! Not only have I enjoyed using Laravel for all my projects over the last 6 years. But Laravel has also allowed me to work full-time on my own projects, as well as travel the world.
So this isn't one of those fractal of bad design. No, this is a proposal of ideas for future versions of the framework.
Of course, being the creator of Shift these suggestions are motivated by maintainability.
They're also motivated by someone who has created products. Specifically products which eventually failed because they became stale.
When consulting or attending conferences, I also listen to the questions developers asking. I pay attention to the aspects of Laravel they like or dislike.
Every so often a revolutionary change is required. This provides a chance to revisit goals. One of the primary goals of Laravel is developer experience. And maintainability, freshness, and approachability all improve developer experience.
So, with all this in mind here are the top five things I would change in Laravel.
Mass Assignment
This is likely one of the few (or only) lacking feature within Laravel.
The goal of mass assignment is to protect models from being injected with unexpected values. Often from request data.
From a security perspective, mass assignment is important. From a feature perspective, it works. Where it is lacking is more from an adoption perspective.
Most developers set all model columns as fillable. Or worse yet, completely disable mass assignment by unguarding their models.
Both of which defeat the purpose of mass assignment.
I'm not sure the solution. One approach would be to reimplement the feature. Rails did so by using strong parameters.
As it stands, I would prefer to see it removed as this would force developers to take responsibility for properly validating and assigning model data.
Less helpful
Laravel is exceptionally helpful. Early versions of Laravel 5 seemed to add more and more helpers. Even helpers which simply wrap underlying facades.
It's hard to argue with being helpful. After all, these helpers undoubtedly improve the developer experience. Yet being overly helpful can become debilitating.
I find many developers using helpers even when the underlying objects are readily available. Effectively using these helpers simply because, well, they're helpful.
With helpers, a developer can also reach for objects they might otherwise not have access to. This blurs important boundaries. Fundamental design aspects like coupling and cohesion and MVC architecture get lost.
Recent versions of Laravel have curbed the use of helpers. For example, all Arr
and Str
helpers were removed in favor of leveraging the underlying class instead.
I would push this farther.
A start might be the removal of the authentication and request helpers. These lead to some of the worst offenses of using helpers when alternative objects and patterns are readily available.
Using these alternatives often yield less complex, more readable code. Considering the following controller action:
1public function store()2{3 $user = User::createWithCheckout();4 $order = Order::createWithUser($user);5 6 return redirect()->route('order.show', $order->id);7}
On the surface, the code seems fine. The issue arises in lower levels of the code. In this case, the model heavily takes advantage of the auth()
and request()
helper (or Facade).
1public function createWithCheckout() 2{ 3 if (Auth::check()) { 4 return auth()->user(); 5 } 6 7 return User::create([ 8 'email' => request('email'), 9 'password' => Hash::make('...')10 ]);11}
This is likely beyond their original intent. But as developers we just can't help ourselves (pun).
If we think about this, we're actually reaching multiple layers in our application stack. All the way from the model to the request. From an MVC perspective, this crosses boundaries.
The alternative respects MVC and decouples the code. Laravel injects the request
object into all controller actions. This object also has access to the authenticated user.
1public function store(Request $request)2{3 $user = User::createWithCheckout($request);4 $order = Order::createWithUser($user);5 6 return redirect()->route('order.show', $order->id);7}
We may then pass this request object to lower levels of the code. Potentially even type-hinting with a form request object to communicate the available request data.
1public function createWithCheckout(Checkout $request) 2{ 3 if ($request->user()) { 4 return $request->user(); 5 } 6 7 return User::create([ 8 'email' => $request->input('email'), 9 'password' => Hash::make('...')10 ]);11}
So while the removal of these helpers might cause short term pain, there would be long term gains in the communities' code quality.
One thing well
Similar to how helpers and Facades offer more than one way to write things, Laravel offers many overlapping components.
While each of these have subtle differences which may make them better suited to various developers needs, it likely could be streamlined.
A few examples of this are Mailables versus Notifications, Events versus Listeners versus Observers, and Gates versus Policies.
The issue is the overlap causes bloat on both sides. From a developer's perspective it's unclear which to use in what scenarios. From a maintainer's perspective it's additional code to support.
Again, I don't think these should be removed. Each provide a distinct bit of functionality.
Instead, I am proposing a hard look at these components with an hard eye for consolidation.
Legacy Laravel
There are a few patterns and structures within Laravel that have been around a while. We've gotten used to them. But more modern frameworks expose these.
Now I'm not advocating for an empty directory structure. I actually appreciate the structure Laravel provides out of the box.
But it could be streamlined.
Specifically there are two patterns within a Laravel application I find a bit dated. Said another way, these are relatively low-level compared to the rest of Laravel. These are the kernel and service providers.
Analytics from Shift show these files are not often changed. When they are changed, the modifications are often simple. Such as binding a singleton or registering a named middleware.
I think these could be consolidated and moved to a different, more modern pattern that Laravel already follows.
If we look inside the routes
folder there are multiple files to register HTTP routes, console commands, and broadcast channels.
Similarly, Laravel could have a middleware.php
file for registering custom middleware and container.php
file for binding classes to the container.
I would then rename the routes
folder to something like bindings
to better communicate the new intent of the files within it.
By moving these responsibilities to simple configuration files, this would remove the need for the Kernel files as well as the service providers.
So this also provides a streamlined app
folder containing just the Http
folder and User
model.
Extended configuration
The most frequent change in Laravel are the configuration files. These change not only between releases, but also in the weekly patches.
As such it is nearly impossible to keep configuration files up-to-date. This is something I've learned first hand with Shift and have repeatedly rewritten code to making maintaining config files easier.
In the end, developers don't keep these files up-to-date or follow best practices which make them easier to maintain. So, while I'm glad to continue to help with Shift, I rather see a change in Laravel.
I think the solution is to remove the configuration files completely. Instead, document the configuration options within the Configuration section as well as reference them more contextually throughout the documentation.
Allow developers to customize these through the available ENV
files. This would cover a majority of the use cases and improve developer experience by removing the aspect of maintenance.
To offer finer grained customizations, introduce a single configuration.php
file (underneath the new bindings
folder).
Much like Laravel 4.2 or similar to configuration in Tailwind, developers could extend the core configuration as well as add their unique customizations.
This would have no impact on performance as it would still be a single array merge operation. In fact, it might even improve performance for an uncached configuration as there would be only one file to load instead of scanning the config
folder.
Closing thoughts
If you'd like to browse around a Laravel application structure adopting these proposed changes, I created a streamlined-laravel repository of a Laravel 6.5.2 application with these proposed changes.
All of the changes require modifying the framework. This includes an unknown amount of work and would definitely include breaking changes. As such, they're not something I expect to see anytime soon. Maybe Laravel 8…
In the meantime, please agree or disagree with me on Twitter, as well as propose your own changes.