Laravel Controller into Service Class with Injection

Tutorial last revisioned on March 05, 2024 with Laravel 11

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.

avatar

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?

avatar

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.

avatar

Heya,

Building on this idea, how would you feel about invokable service classes dedicated to just the one thing, something like :

public function store(StoreThingyService $storeService, StoreThingyRequest $request)
{
	$thingy = $storeService($request->validated());			
	
	return to_route('thingy.show', compact('thingy'));
}

I've written something of the sort today at work and I'm not really sure what to think about it. Thanks !

avatar

For services with just one method they came up with a separate term called Action classes.

see examples here

avatar

Great, thanks ! I'll give that a look :)

avatar
Nabeel Yousaf Pasha

I have a service that uses PHP 8.0^ constructor property promotion in it

class UserInvitationService
{

    use ViaModelTrait;
		
		/**
     * @param Company $company
     * @param User $user
     */
    public function __construct(
        protected Company $company,
        protected User $user,
    )
    {
        //
    } 

Could 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?

$userInvitationService = (new userInvitationService(
            fromCompany: getAuthCompany(),
            fromUser: $authUser,
        ))

I am trying to figure out how service providers resolve that. I wanna clear my concept. Thanks.

Huge Respect from Paksitan

cc: @PovilasKorop

👍 1
avatar

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?

Like our articles?

Become a Premium Member for $129/year or $29/month
What else you will get:
  • 59 courses (1057 lessons, total 42 h 44 min)
  • 79 long-form tutorials (one new every week)
  • access to project repositories
  • access to private Discord

Recent Premium Tutorials