Why Design Patterns Matter
If you’ve worked with Laravel, you know it’s a fantastic framework that helps you build applications quickly. But as your project grows, things can start to get messy. Controllers can become huge, queries are scattered everywhere, and testing your code can feel impossible. That’s where design patterns come in handy. They give you better ways to organize your code so it stays clean and easy to manage.
In this article, we’ll look at three useful design patterns in Laravel: the Action Pattern, Repository Pattern, and Query Service. Each one has pros and cons, and knowing when to use them is crucial. As Povilas Korop says: “Any use of patterns DEPENDS on the team, the project, and the developer.”
For my current big project, we chose the Action Pattern and Query Service, but I’ve used the Repository Pattern in the past too.
Quick Overview of My Current Project Setup
For our latest project Dental software, we opted for the Action Pattern to streamline our business logic and the Query Service Pattern to handle complex queries. We’ve used the Repository Pattern in previous projects, but it just didn’t make sense here. Let’s unpack why.
The Action Pattern
The Action Pattern is all about organizing your code so it’s easier to understand and maintain. Instead of putting a bunch of logic into your controllers or service classes, you create small classes called Actions. Each Action does one thing only, which makes your code simpler to test and reuse.
For example, imagine you need to create an order, send emails to the customer and admin, and log that the order was created. Instead of cramming all this into your controller, you can use an Action class like this:
class CreateOrderAction
{
public function execute(CreateOrderDto $data): Order
{
// Create the order
$order = new Order();
$order->user_id = $data->user_id;
$order->total_amount = $data->total_amount;
$order->status = OrderStatusEnum::PENDING;
$order->save();
// Event Handling next steps like notifications which are sent async
OrderCreated::dispatch($order);
return $order;
}
}
Then, in your controller, you just call this Action:
public function store(CreateOrderRequest $request, CreateOrderAction $action)
{
$order = $action->execute($request->getCreateOrderDto());
return response()->json(['message' => 'Order created successfully!', 'order' => $order]);
}
We chose this pattern because it keeps our controllers simple, making them easier to manage. Plus, testing is straightforward since each Action is its own class. The downside? If your project is small, making a separate class for everything can feel like too much. Also, if you have too many Actions, it can be a pain to organize them.
The Repository Pattern
The Repository Pattern creates a layer between your controllers and models, making your code more flexible. Instead of directly working with Eloquent models, you use a repository to handle data access. This keeps your code clean and makes it easier to change how data is stored later on.
Here’s a basic UserRepository:
use App\Models\User;
class UserRepository
{
public function findById(int $id): ?User
{
return User::find($id);
}
public function findByEmail(string $email): ?User
{
return User::where('email', $email)->first();
}
public function create(array $data): User
{
return User::create($data);
}
public function update(int $id, array $data): bool
{
$user = $this->findById($id);
return $user ? $user->update($data) : false;
}
}
In a controller, you can use this repository to keep things organized:
public function show(int $id, UserRepository $userRepository)
{
$user = $userRepository->findById($id);
return response()->json($user);
}
public function update(UpdateUserRequest $request, int $id, UserRepository $userRepository)
{
$success = $userRepository->update($id, $request->getUpdateUserDto());
return response()->json(['success' => $success]);
}
In our current project, we decided not to use the Repository Pattern. Our data needs were simple, and using Eloquent directly was good enough. Adding a repository layer would have made things more complicated. However, this pattern is great if you have complex or changing data sources and need more flexibility.
The Query Service
The Query Service is perfect for handling complex data retrieval. It focuses on read-only operations and helps you keep your models and controllers clean. If you have a lot of complicated queries, this pattern can make your code easier to maintain.
For example, suppose you need to fetch users with different filters and sorting options. You could use a UserQueryService
:
\
use App\Models\User;
class UserQueryService
{
public static function model(): Builder|CompanyModel
{
return User::query();
}
public static function getActiveUsers(int $limit = 10)
{
return self::model()->where('active', true)->limit($limit)->get();
}
public static function getUsersByRole(string $role)
{
return self::model()->where('role', $role)->get();
}
public static function searchUsers(string $query, int $perPage = 15)
{
return self::model()->('name', 'like', '%' . $query . '%')
->orWhere('email', 'like', '%' . $query . '%')
->paginate($perPage);
}
}
You can use this service in your controller like so:
public function index(SearchUserRequest $request, UserQueryService $userQueryService)
{
$dto = $request->getSearchDto();
$users = $userQueryService::searchUsers($dto->query);
return view('users.index', compact('users'));
}
For our project, this pattern made perfect sense. It kept our models focused and our query logic organized. It also made performance optimizations easier since all our queries were in one place. The downside is that it’s only for reading data, so you’ll need other solutions for writing or updating data.
Putting It All Together
Choosing the right design patterns isn’t just about following best practices, it’s about making your life easier as a developer. Think about the problems you’re trying to solve and how each pattern can simplify your code.
The Action Pattern is a game-changer when you want to keep your business logic organized and reusable. If you’ve ever felt overwhelmed by bulky controllers or hard-to-test code, Actions are your new best friend. Imagine having small, focused classes that handle individual tasks and make your project feel manageable again.
When it comes to data retrieval, the Query Service is like having a cheat sheet for complex queries. Instead of cluttering your models or copying query logic everywhere, you have one place to manage all your data-fetching needs. It keeps things clean, optimized, and easy to maintain.
On the flip side, if your project feels like it’s begging for a flexible way to handle data access, the Repository Pattern might be worth considering. Even if it didn’t make sense for our current project, it has proven valuable in the past, especially for handling changing data sources or when more abstraction is required.
Remember, no one knows your project better than you do. Experiment, adapt, and don’t be afraid to change your approach as you learn what works best. Patterns are tools in your developer toolkit, so use them where they make the most impact.
Have you tried implementing these patterns in your projects? What worked, what didn’t, and what surprised you? Share your experiences, I’d love to hear how you’re structuring your Laravel applications!