Back to Course |
Design Patterns in Laravel 11

Service: Most Misunderstood Word in Laravel?

The word "service" is often used in the Laravel community, but is it a "design pattern"?

Yes and no.

It is a pattern in the sense that it's a typical approach to offload the logic from the Controller to "somewhere else", that "somewhere" being the Service class.

However, identically to the Action classes, Services are not a core Laravel function, so there's no php artisan make:service, and developers can name/structure them however they want.

I've seen two main use cases for Service classes:

  • Case 1. Something like UserService: A class with multiple methods around a certain Model or other subject (more "loose" pattern)
  • Case 2. Something like StripeService: A class that helps to work with external functionality or a 3rd party library that can be replaced with another library in the future (more "strict" pattern)

And let's see both in action.


"Loose" Services Example

A typical simple example is this:

app/Http/Controllers/UserController.php:

use App\Http\Requests\StoreUserRequest;
use App\Services\UserService;
 
class UserController extends Controller
{
public function store(StoreUserRequest $request, UserService $userService)
{
$userService->store($request->validated());
 
return redirect()->route('users.index');
}
 
// ...
}

Then, inside that UserService class, you have methods to store users, update users, and perform other operations related to the User model.

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
}
}

Wait, How Does That "Magic" Type-Hinting Work?

So, you've seen this code:

public function store(StoreUserRequest $request, UserService $userService)
{ // ...

So, how does that "method injection" work, and why don't we need to write new UserService()?

This is powered by a Laravel feature called Container, sometimes also called "Service Container," but it has nothing to do with the Service classes we're discussing here. Naming is hard, I know.

Without delving too deeply into how it is implemented, you need to know that some/most Laravel classes can auto-create class instances if you type-hint their classes in the parameter list.

The code above is actually a 2-in-1 example:

  • You're probably familiar with (Request $request) type-hinting in Controllers
  • But you may not know that you can type-hint any class this way in Controllers

Internally, Laravel just performs $request = new Request() for us. That's it. Not much more you need to know.

Similarly, you may type-hint dependencies in the handle() method of queued jobs.

This is called method injection, but you can also type-hint and inject classes in the constructor of a class that is resolved by the container, including Controllers, Event listeners, Middleware, and more. Then, there's no need to type-hint it in every method.

use App\Http\Requests\StoreUserRequest;
use App\Http\Requests\UpdateUserRequest;
use App\Models\User;
use App\Services\UserService;
 
class UserController extends Controller
{
public function __construct(
protected 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');
}
 
// ...
}

Final "notice": this is enabled only in Laravel classes processed in Container, but if you type-hint a Service class inside of another Service class method/constructor, for example, Laravel will not recognize it and will throw an error.

use app\Services\TwitterService;
 
class UserService {
 
public function store(array $userData, TwitterService $twitter): User
{
// THIS WILL NOT WORK
// You need to manually write $twitter = new TwitterService();

Services vs Jobs/Actions

The key point is that the Service method is a "black box":

  • It accepts the parameters from wherever (Controller, Job, Unit Test)
  • And it returns the results to be later used wherever (Controller will pass the results to the View, Unit Test will assert the results, etc)

It's a bit similar to Actions/Jobs from a previous chapter. The purpose of such separation is the same: offload the logic from the Controller to "somewhere else". But there are two differences between Services and Actions/Jobs:

  • Services contain multiple methods, not one handle() or execute()
  • Those methods usually return result for future processing, not void

However, the example above is still a "Wild West": if a developer sees a Service class used, they wouldn't immediately know what's inside, as every developer individually creates every Service, and there's no strict pattern or set of rules.

But if that's a problem, we can introduce the rules if needed!

And let's discuss a more "strict" type of Services in the next lesson.