Crafting maintainable Laravel applications

Main Thread Talks 10 min read

This is a write-up of my talk Crafting Maintainable Laravel Applications given at Laracon AU.

Being the author of BaseCode and creator of Shift has given me a unique insight into writing Laravel applications. I combined 20 years of writing code with supporting over 25,000 Laravel upgrades into 10 tips for crafting maintainable Laravel applications.

These may seem fundamental and as such quickly dismissed. But any lasting Laravel codebase practices at least some of these fundamental elements. Put simply, the more tips you follow the more maintainable your codebase will be.

Stay Current

Let's start with the most fundamental of all, stay up-to-date. Sure, this comes from the creator of Shift. But it's nonetheless valid.

Too many applications choose to remain out-of-date for various reasons. I'm here to say, LTS is a trap, forking the framework is naive, and committing the vendor folder is a disaster waiting to happen.

These actions might seem silly to some, but as applications become out-of-date, many choose these paths over upgrading. They are one-way tickets to a completely unmaintainable codebases.

Staying current also allows you to take advantage of the latest features and services, as well as help the community grow and evolve. So, you don't have to be bleeding edge, but you do want to be leading edge.

Adopt the standards

Laravel comes with all sorts of conventions. I'll revisit more in other tips. For this one, I want to focus on coding standards.

You may not agree with all of them. I know I don't like the ! (not operator) or . (string concat operator) spacing. But when writing Laravel, I do my best to follow these.

Many developers create their own standards. In fact, I occasionally receive support tickets claiming a Shift was unusable because it formatted their code. I realize code style is personal. As developers, we've made it our identifiable trait. But really, it's a separator. A distraction.

In the end, it's okay if you want use a custom code style, but please automate it. Ideally using PHP CS Fixer as Shift will respect any .php_cs file within your project and use it when formatting your code.

Vanity namespaces

Along these lines, don't customize the App namespace. Taylor and Jeffrey Way rejected this pattern a while back. Even the app:name artisan command was removed from Laravel. Despite all this, I still see projects renaming the App namespace.

By changing this, you commit yourself to changing it in multiple locations. This adds to the maintenance overhead and creates needless friction when coding.

Consider the HTTP Kernel. Out of the box, this file contains 23 references to the App namespace. That's 23 spots to maintain and manage.

I know it's just a search and replace, but it adds friction to common developer actions like copying and pasting code from StackOverflow, Laracasts, or the documentation.

Regardless of customizing the namespace or not, I definitely encourage using morphMap for any polymorphic or dynamic model relationships saved to the database. Doing so will decouple your code from your database.

Default the structure

Similar to adopting the standards, I encourage you to keep the Laravel folder structure as close to the defaults as possible. I see applications creating their own folder structures underneath the app folder. Often to modularize or separate the code by domain.

Within these folder is a recreation of the default folder structure. So, Controllers, Models, Events. This creates more overhead which eventually competes with the default structure. It also leads to parallel inheritance hierarchies, which is an original Code Smell.

Instead, you may collapse these into the default structure. If necessary, you can organize domains within the core folders.

I know separating your domain code from the app code seems like a good idea in the beginning. It may make work out for some. Yet many developers who choose this path eventually regret it. Remember, the app is your domain, so there's no need to reorganize it elsewhere.

Where things go

Another common questions related to structure is where do things go?

Again, Laravel provides many folders within its default structure to choose from. I challenge most classes could be organized within one of these folders. When something doesn't immediately fit, I suggest putting it in a Services folder.

You'll find classes within this folder self-organize over time. Once they reach a critical mass, restructure them into their own top-level folder, like: Facades, Clients, Contracts, Traits, etc.

This may seem lazy, but it's to avoid the wrong abstraction. Something I discuss in the Rule of Three from BaseCode. The premise is future you is always smarter than you are now. So if you can defer decisions a bit, you'll find a better abstraction.

Managing packages

Composer makes packages super easy to use and manage. So easy we don't think about the maintenance of these packages. But any package we bring into our application is code we have to manage. Code with which our application code is coupled.

This is something we should remember before adding a package. But first, as a quick tangent, always ensure your packages are registered appropriate. Metrics from Shift show many applications incorrectly require development dependencies. That is, code which is not "required" in production. For example, the barryvdh/laravel-debugbar package.

Going back to packages, many simple packages are bloated and quick to become outdated or even abandon. Consider the once popular laracasts/Commander package. Now long since abandoned and replaced by core behavior.

Nonetheless, we can still use this package as an example. It contains 7 files to do one simple thing - execute a handler for a command. All this code can be a single trait with a single execute method. Doing so removes the risk of future package management.

Of course this is not something to do for every package. But for simple functionality, assimilating the code into your project can improve maintainability.

Smooth bindings

Laravel provides a lot of dynamic behavior. As such, it can become confusing on where something is located. Bindings are the biggest offender.

To combat this, register all your bindings in one place. Ideally the AppServiceProvider. When possible, use its $bindings and $singletons properties to make these easily scannable.

There are other bindings within Laravel which can benefit from this practices, including: Events, Policies, Commands, Broadcasts, and Routes.

Expanding on routes a bit more, remember there are both API and web routes. Too often I see applications put everything under the web routes, even though they use the endpoint as an API.

There's a vast difference in middleware loaded for these two types of routes. For example, API endpoint using the web middleware loads session and cookie data. This can lead not only to bad habits when crafting these endpoints, but also incompatibilities in a future if these middleware change.

Configuring Configs

It's true the configuration files are there for you to update and customize. It's also true these files are the most changed file in Laravel. There are ways to manage the Laravel config files in a maintainable way. I talk about this in more detail in a previous post on Maintaining Laravel Config Files.

Avoid overwriting

Similar to formatting and structure, many applications inject their own abstractions for core components. For example, creating something like a BaseModel. This BaseModel creates some shared logic, such as unguarding attributes, or sometimes, completely overwriting core methods to add tiny bits of customization.

Again, doing so may decouples you from the framework. But it this case this decoupling is actually bad. All the same issues arise as if you forked Laravel itself. You no longer receive features or tweaks Laravel makes to this overwritten methods.

Instead of injecting an inheritance layer, consider using a trait instead. In this case, an Unguarded trait. I'll expand on this more in the next tip. For now, let's look at another example regarding overwriting core methods.

I often see developers overwrite core authentication methods. Again, doing so for simple reasons like customizing the response. If you look at the code, Laravel provides ways to do this without overwriting the entire method which allows us to be more surgical.

For example, we can simply overwrite the authenticated method. Within it we can perform additional authentication logic or redirect the user.

Laravel provides all sorts of these callback methods and properties. For example, a Form Request has properties to change the redirect. You can add a render or report methods to custom exceptions to better handle errors.

You can hook into core functionality through events. Laravel fires events for all sorts of core behavior. You can register an event listener to run custom code without having to overwrite low-level details. These events are fired for all sorts of components and behaviors, such as Auth, Jobs, Notifications, Commands, even Migrations.

Grok the framework

Said another way, this means to use what the framework has to offer. Mimic its patterns and practices. Be a team player. Don't go rogue.

Grokking the framework takes different forms. One of these easiest ways are Blade directives. Laravel provides dozens of expressive Blade directives. Unfortunately many applications only use the standard @if directive. But there's @isset, @empty, @auth, and @guest directives which can streamline your templates and better communicate intent.

Grokking the framework encourages us to learn the Laravel Way to write code, often making it feel more like "home", approachable and therefore maintainable.

Another example is leveraging facades. Taking that farther, real-time facades. Facades can seem heavy having to create not just the underlying class, but the accessory, register it through a provider, and maybe even create an alias.

Well not with real-time facades. We can actually use the underlying class as a facade simply by importing the class underneath the dynamic Facades namespace. Laravel resolves the class automatically. This means we can get all the benefits of a facade, including its testability.

Honor the MVC architecture

Laravel is an MVC framework. It's also a developer friendly framework. The combination can sometimes make it easy to do things which blur the lines between models, views, and controllers. However, it's important to remember the MVC architecture.

In general, under MVC when a request comes in the controller mediates between the model and view to respond. This means limited view logic. No cross communication between the model and the view or response.

Unfortunately, too often developers abuse facades or helpers to access requests from models or the data layer from views. Again, the framework makes this super easy. But in doing so we've coupled the model not only to a higher layer but also to the nuances of Laravel request handling or authentication.

Instead, we can honor MVC and Laravel by following very basic pattern - dependency injection. Laravel injects a request object into any controller method with a Request type hint. We can also type hint a form request object if we have object if we want validation.

Now we can pass the request object down to these model. In doing so, we reduce the coupling to only the request object. Now you might be thinking, this is six in one hand, half dozen in the other. Maybe, but this is where using a form request can bring it all together. By also type-hinting this for the model, it serves as a contract. This communicates the bits of data we can expect within this request object.

Write tests

Even if you ignore every other tip for writing maintainable Laravel applications, you can likely overcome these with a solid suite of tests.

Testing in Laravel makes testing very approachable and easy. No matter which style of tests you write, Laravel provides an option. There's HTTP Tests for quickly sending requests to your application and verifying the responses. There's Dusk to interact with your application through the browser. And everything is built on top PHPUnit, so writing unit tests is always an option.

If you're new to testing, I encourage you to watch the first lesson of Confident Laravel. It is available for free so everyone can gain confidence to start writing tests.

Find this interesting? Let's continue the conversation on Twitter.