In this article, I will show you how to shorten Controllers by using Service classes, and different ways to initialize or inject that Service.
First, the "before" situation - you have a Controller with two methods: store()
and update()
:
class UserController extends Controller{ public function store(StoreUserRequest $request) { $user = User::create($request->validated()); $user->roles()->sync($request->input('roles', [])); // More actions with that user: let's say, 5+ more lines of code // - Upload avatar // - Email to the user // - Notify admins about new user // - Create some data for that user // - and more... return redirect()->route('users.index'); } public function update(UpdateUserRequest $request, User $user) { $user->update($request->validated()); $user->roles()->sync($request->input('roles', [])); // Also, more actions with that user return redirect()->route('users.index'); }}
This Controller is too long - the logic should be somewhere else.
Refactoring - Step 1: Service Class
One of the ways to refactor it is to create a specific Service class for everything related to the User, with methods like store()
and update()
.
Service can be created using the make:class
artisan command:
php artisan make:class Services/UserService
And then, we move that code from Controller, into a Service:
app/Services/UserService.php:
namespace App\Services; class UserService { public function store(array $userData): User { $user = User::create($userData); $user->roles()->sync($userData['roles']); // More actions with that user: let's say, 5+ more lines of code // - Upload avatar // - Email to the user // - Notify admins about new user // - Create some data for that user // - and more... return $user; } public function update(array $userData, User $user): User { $user->update($userData); $user->roles()->sync($userData['roles']); // Also, more actions with that user }}
Then, our Controller becomes much shorter - we're just calling the Service methods.
There are a few ways to do this. The most straightforward one is to create a Service class instance whenever you need it, like this:
use App\Services\UserService; class UserController extends Controller{ public function store(StoreUserRequest $request) { (new UserService())->store($request->validated()); return redirect()->route('users.index'); } public function update(UpdateUserRequest $request, User $user) { (new UserService())->update($request->validated(), $user); return redirect()->route('users.index'); }}
Refactoring - Step 2: Inject Service
Instead of doing new UserService()
every time we need it, we can just insert it as a dependency in the methods where we need it.
Laravel will auto-initialize it, if we provide the type-hint inside the Controller methods:
use App\Services\UserService; class UserController extends Controller{ public function store(StoreUserRequest $request, UserService $userService) { $userService->store($request->validated()); return redirect()->route('users.index'); } public function update(UpdateUserRequest $request, User $user, UserService $userService) { $userService->update($request->validated(), $user); return redirect()->route('users.index'); }}
We can go even further and inject the Service class into a constructor of the Controller. Then, we have access to the service in whatever Controller methods we need.
use App\Services\UserService; class UserController extends Controller{ private UserService $userService; public function __construct(UserService $userService) { $this->userService = $userService; } public function store(StoreUserRequest $request) { $this->userService->store($request->validated()); return redirect()->route('users.index'); } public function update(UpdateUserRequest $request, User $user) { $this->userService->update($request->validated(), $user); return redirect()->route('users.index'); }}
Finally, we can use the PHP 8 relatively new syntax called constructor property promotion, so we don't even need to declare a private variable, or assign something in the constructor:
use App\Services\UserService; class UserController extends Controller{ public function __construct(private UserService $userService) { } // We can still use $this->userService anywhere in the Controller}
I have a separate video, specifically on that PHP 8 feature:
That's it for this article. Of course, there are other ways to separate the code from the Controller - Action classes, Repositories and other patterns - so choose whichever you like, the logic of injecting or initializing that class would be the same.
We also have a PREMIUM course on Design Patterns in Laravel
Nice step, but i think we must use a service if each method of the controller more than 20-30 lines maybe..
cause, it will make u move from a file to another just to look at one method.
isn't it?
There's no strict rule for how many lines of code. It's whenever the method becomes harder to read and understand immediately what it does. I would go lower to 10+ lines of code, not even 20-30, but that's a personal preference.
Heya,
Building on this idea, how would you feel about invokable service classes dedicated to just the one thing, something like :
I've written something of the sort today at work and I'm not really sure what to think about it. Thanks !
For services with just one method they came up with a separate term called Action classes.
see examples here
Great, thanks ! I'll give that a look :)
I have a service that uses PHP 8.0^
constructor property promotion
in itCould you please ellaborate, how may I inject it in my controller's constructor? Because whenever I initiate it in any method, I have to provide params. Is it needed to be bind in Service Providers?
I am trying to figure out how service providers resolve that. I wanna clear my concept. Thanks.
Huge Respect from Paksitan
cc: @PovilasKorop
Yes, if you need parameters in your service, then you need to bind it in the Service Provider.
But, on the other hand, if you don't like Service Providers approach, I would re-think the logic and think if you really need to know those parameters? In other words, does service really need to know the company/user upfront for all action? Could those be parameters to one specific method? Or have a specific method to set them, not in the constructor?