Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

BluKoffee's avatar

What is the best approach to handle dependencies in Laravel services with user-specific functionality

Question:

I am building a Laravel application and using services to encapsulate the business logic. I have a UserService class that performs various operations related to a specific user, such as updating the user's profile or retrieving user-specific data.

The question is how to handle the dependency on the User instance within the UserService class. Currently, I have two approaches in mind:

Approach 1: Passing User Argument to the UserService Constructor

In this approach, the UserService constructor accepts a User instance, and the instance is passed to the methods that require user-specific functionality. Here's an example:

namespace App\Services;

use App\Models\User;

class UserService
{
    protected $user;

    public function __construct(User $user)
    {
         $this->user = $user;
     }

     // Methods that use the $user instance
     public function updateUserProfile($data)
     {
         // Perform operations using $this->user
     }

     public function getUserData()
     {
         // Retrieve and process user-specific data using $this->user
     }

     // Other methods...
}

Approach 2: Method Arguments for User Instance

In this approach, the UserService class does not store the User instance internally. Instead, each method expects the User instance as an argument. Here's an example:

namespace App\Services;

use App\Models\User;

class UserService
{
    public function updateUserProfile(User $user, $data)
    {
        // Perform operations using $user
    }

    public function getUserData(User $user)
    {
        // Retrieve and process user-specific data using $user
    }

    // Other methods...
}

Now, I'm seeking advice on the best approach to handle this situation. Here are my problems:

The problems of Approach 1 (Passing User Argument to the UserService Constructor):

(1). I can't use the Laravel service container (type hinting) anymore since the UserService class now expects a User $user argument so I can't do this anymore:

class UserController extends Controller
{
    protected $userService;

    //I can no more do this service container dependency injection  
    
    public function __construct(UserService $userService)
    {
        $this->userService = $userService;
    }

    //...
    //Rest of the controllers' methods
    //...

}

(2). it adds complexity to the Laravel controller: since a controller in Laravel is usually called to perform a decision based on a specific route, it makes sense to use the Request class or request() helper both provided by Laravel. And I thought it makes sense to retrieve the specific User $user that make the request in my laravel controller. But to do this in Laravel you have to interact with the middleware and my controller now looks like this:

class UserController extends Controller
{

    protected UserService $userService;

    public function __construct()
    {
        $this->middleware(function ($request, $next) {
            $this->userSettingsService = new UserService($request->user());
            return $next($request);
        });
    }

    //...
    //Rest of the controllers' methods
    //...
}

The problems of Approach 2 (Method Arguments for User Instance):

(1). it violates the Single Responsibility Principle: If the primary responsibility of the UserService is to perform operations specifically related to a single User, would it make sense to include the User argument in the constructor?

So Laravel experts what is your opinion on this?

0 likes
5 replies
LaryAI's avatar
Level 58

Both approaches have their pros and cons, and the best approach depends on the specific requirements of your application. Here are some considerations:

Approach 1 (Passing User Argument to the UserService Constructor):

Pros:

  • The UserService class is more flexible, as it can work with any User instance, not just the authenticated user.
  • The UserService class is easier to test, as you can pass a mock User instance to the constructor.

Cons:

  • The UserService class cannot be used with Laravel's automatic dependency injection, as it requires a User instance to be passed to the constructor.
  • The controller code becomes more complex, as you need to retrieve the User instance and pass it to the UserService constructor.

Approach 2 (Method Arguments for User Instance):

Pros:

  • The UserService class can be used with Laravel's automatic dependency injection, as it does not require a User instance to be passed to the constructor.
  • The controller code is simpler, as you can retrieve the User instance from the request and pass it to the UserService methods.

Cons:

  • The UserService class is less flexible, as it can only work with the User instance passed as an argument.
  • The UserService class is harder to test, as you need to create a User instance and pass it to each method.

Based on these considerations, if your UserService class is primarily focused on operations related to the authenticated user, and you don't need to use it with other User instances, Approach 2 may be a better fit. However, if you need more flexibility and testability, Approach 1 may be a better choice.

As for the violation of the Single Responsibility Principle in Approach 2, it's true that including the User argument in each method can make the class less cohesive. However, this is a trade-off between cohesion and flexibility, and it's up to you to decide which approach is more appropriate for your application.

Example code for Approach 1:

namespace App\Services;

use App\Models\User;

class UserService
{
    protected $user;

    public function __construct(User $user)
    {
         $this->user = $user;
     }

     // Methods that use the $user instance
     public function updateUserProfile($data)
     {
         // Perform operations using $this->user
     }

     public function getUserData()
     {
         // Retrieve and process user-specific data using $this->user
     }

     // Other methods...
}

Example code for Approach 2:

namespace App\Services;

use App\Models\User;

class UserService
{
    public function updateUserProfile(User $user, $data)
    {
        // Perform operations using $user
    }

    public function getUserData(User $user)
    {
        // Retrieve and process user-specific data using $user
    }

    // Other methods...
}
Snapey's avatar
Snapey
Best Answer
Level 122

a third approach, add a setUser() method to the class, omit the constructor.

1 like
BluKoffee's avatar

@Snapey Thank you so much for your suggestion. It actually makes a lot of sense as it mixes both pros of the approaches in terms of flexibility and not violating the single responsibility principle.

Since I am going to omit the constructor, as you suggested, and replace it with a setUser method this will not guarantee that the setUser method will be called after instantiating the UserService class and thus creating the problem of the rest of the UserService class methods not able to operate because the value of $this->user is null. That is why I preserved the $user argument in all methods as an optional argument that can still be used in this case.

Now my UserService class looks like the following:


namespace App\Services;

use App\Models\User;

class UserService
{
    protected $user;

    public function setUser(User $user)
    {
        $this->user = $user;
    }

    public function updateUserProfile($data, User $user = null)
    {
        $user = $user ?? $this->user;

        // Perform operations using $user
    }

    public function getUserData(User $user = null)
    {
        $user = $user ?? $this->user;

        // Retrieve and process user-specific data using $user
    }

    // Other methods...
}

So, Laravel experts what do you think? Please let me know if there are further improvements that I can do.

Snapey's avatar

@blukoffee You could throw an exception if the user has not been set

    public function updateUserProfile($data,)
    {
        throw_if(empty($this->user),  new Exception, 'User not set');

        // Perform operations using $user
    }

Doesn't really do anything except ensure consistency in your code.

1 like

Please or to participate in this conversation.