How to refactor a Laravel application in multiple packages
Make your code and your mind cleaner by separating your application domains
As time goes by, commercial projects become bigger and bigger. This makes them difficult to maintain, especially if the time allocated for refactoring is low and the project structure isn’t well defined and established.
In this article, we will see a nice way to divide your project in multiple subprojects by creating packages that can assolve to an entire domain application or - more generally - to a specific part of your software.
The base idea is to have a common Laravel structure (it is okay to use another framework or another language), in which we will create a dedicated folder called packages
, with a folder for each subproject. In this way, we will obtain a logically separated code that makes the project more readable.
This suits well for cases in which there are standalone requirements (e.g. 3rd party integrations) that can be performed without using the rest of the code. For example, I built a client to manage domains using a protocol called EPP (the standard protocol for .it domains), which exposes some actions to register a domain, cancel it and so on. In this way, it will be sufficient to me, in my Laravel application, call something like RegisterDomain::run($domainName)
in order to perform the action, using an abstraction layer and avoiding a series of a subproject into the app
folder. The goal is to have only the core things inside the main project.
The rule of thumb
How to decide what should become a package? It is not so simple at the start, but it will be clearer with the experience. Let’s start with a simple rule of thumb.
Rule of thumb: if a part of the code is responsible for a specific domain and it doesn’t need to interact with the rest of the code (i.e. the
app
folder), probably it could become a package.
Let’s break down this rule to understand better what it means.
What is a domain? The technical definition in software engineering for a domain is “the targeted subject area of a computer program”1. This is the main concept behind the Domain-driven design, a technique in which the code matches the business domains, not only by terminology but also by how the code is structured (for example a class “CreateCustomer
” which handles the entire customer creation or “SendNewCustomerGift
” to dispatch an email with a one-time coupon). My idea of packages is not only to think on a level of business domains, but of using this concept to separate the logic when possible.
For example, if you are working on an e-commerce, you probably will have to deal with customers, orders, subscriptions and payment providers (Stripe, PayPal..). If you used Laravel for an e-commerce solution, probably you have seen Laravel Cashier, a really well done package to handle Stripe payments.
When using Cashier, you are not interested in the code that manages the communication with Stripe, but you are using the abstraction it provides, such as the function to redirect to the billing portal, the function to make a new payment and so on. But what happens if you add Laravel Cashier inside the app folder? It will make your folder full of logic which handles not the core application, but a collateral thing.
Now I’m talking in terms of folders, so something visual like the project structure, but it is not only related to what your eyes see when opening the project panel. I’m talking about the fact that a package is built to provide abstractions on what you have to do, containing all the necessary code that will handle the different use cases, without interfering with the application. It is standalone, you can take it, add to another project, and use when you want. Doesn’t it sounds familiar? This is an application of the DRY principle, because we don’t want to rebuild the wheel from scratch. You can also test this singular unit, and do it in a way you are secure it will works.
This is one important point: not only a separate package will make your product focused on the app logic, but it will make the package reliable and reusable in other applications. If I had to create a new application, for the company or for a specific customer that requires to register new domains, I’m able to take the package and add it in the new project. Zero effort. It’s tested, we know it works, the IT is happy, the business is happy. I could use a private repository management tool to synchronize the versions too (but we will not talk about that).
Concrete examples
Should I have to make everything a package? Clearly not. With the real world experience, I understood that only some things can be a package. For example, if the code has to be fully integrated with other models or domains, or if it is a main part of the custom business logic, probably it is better to have it inside the standard project (i.e. the app
folder). If you think there is a good chance that it will be deeply integrated in the main code, think carefully if it should be splitted.
Good examples of packages are third party integrations, in which the package manages the communication with the external provider. In fact, you don’t really need to have a folder “SaasX” in the main code, it is better to call only directly the function using a syntax like “new SaasX()→handleCaseY()
”.
Another good example is when the business requires a side project, for example an analytics system, a rewards system or something that is not part of the main product but that is not enough big to make a separate project.
As said before, the experience will help you to understand what should be separated. So let’s start to build a package from scratch. Online you will find well know skeletons such as the one provided by Spatie, but in this tutorial I want to start by zero.
Why not microservices?
Before building the packages, I have to clarify “why shouldn’t make directly a microservice?”. I think microservices aren’t so good as many people say. The first problem with microservices is that they are not so easy to add into an existing infrastructure without a DevOps team, and maintain it could be complicated because if you want to build a real product using microservices, you have to build something like 50 microservices. And you have to use a process communication system (gRPC, APIs, Kafka, RabbitMQ…), so you cannot directly use a function call without involving some advanced techniques. If you are interested in RabbitMQ for process communication, check this article.
The practice: How to build a package
So, let’s go in your Laravel application and create a packages
folder. Now, inside it, let’s create the first package. For example, let’s create a folder named our-first-package
.
So, enter the folder and start a new composer project by doing composer init
. You will see a tutorial that asks for basic informations, such as the package name.
Once made the tutorial, a composer.json
file and a src
folder will appear. The first will contain the composer details, such as the package name, the required dependencies and so on, and the second is where we will put our code. Remember to add the vendor
folder to the .gitignore
file at the same level of the src
folder and the composer.json
file. Create it if it doesn’t exists.
In the composer.json
of the package, you have to add the following line after the package name because you have to say what version the package is:
"version": "0.0.1",
You can add your dependencies by executing composer require x/y
from the package folder’s terminal, so you will be able to use external dependencies in your package classes.
Now, let’s make our first class! In the src
folder create a TestClass.php
file. The namespace should be something like “VendorName/OurFirstPackage
”.
<?php
namespace Alessandrodorazio\OurFirstPackage;
class TestClass {
public function sum($a, $b) {
return $a+$b;
}
}
We created the package, and we can try to import the test class to check if it’s working. Before doing that, we have to require the dependency into the main project. So, we have to import the package in the main composer.json
file by adding the repository path:
"repositories": [
{
"type": "path",
"url": "./packages/our-first-package"
}
]
Now, let’s run a composer require vendorName/our-first-package
from the Laravel main folder, and it will start to install it. If you don’t remember vendor and package name, you can see it in its composer.json
file at the value having key “name
”.
Now you can go in your app folder and use your new TestClass by importing it (auto imports should be already working!).
✅ Congratulations! You learned how to create your first PHP package, why and when you should use them.
If this helped you, consider to subscribe to this blog.
https://en.wikipedia.org/wiki/Domain_(software_engineering)