Laravel pass arguments in trait constructor

Stefan Bogdanescu

Founder & Senior Architect · 2026-06-29

Laravel Company

The Dilemma of Dependencies: Passing Arguments into Laravel Traits

As senior developers working within the Laravel ecosystem, we frequently deal with object-oriented patterns like Traits, where the goal is to share behavior across multiple classes. When we introduce Dependency Injection (DI) into this structure, a common hurdle arises: how do we pass dependencies into a trait if traits are not full-fledged classes that participate directly in the Service Container's binding resolution?

This post dives into a specific scenario encountered when trying to inject dependencies into a Laravel Trait and why the standard constructor injection method fails. We will explore the architectural implications and demonstrate the correct, idiomatic ways to manage dependencies across traits and models.

The Setup: Where the Problem Lies

The setup you described involves correctly configuring your Service Container bindings for repository resolution:

// In a Service Provider (e.g., AppServiceProvider)
public function register()
{
    $this->app->bind(UserRepositoryInterface::class, UserRepository::class);
}

public function provides()
{
    return [
        UserRepositoryInterface::class,
    ];
}

This setup is perfect for resolving dependencies when instantiating classes or services. However, when you attempt to inject this dependency directly into a trait's constructor, the result is null. This happens because traits are merely collections of methods and properties; they do not execute within the context of the main Application Container resolution pipeline in the same way a standard class does during object instantiation.

Your attempt:

// Inside TimezoneTrait.php
private $userRepository;

public function __construct(UserRepository $userRepository) // Fails here!
{
    $this->userRepository = $userRepository;
}

The core issue is that the mechanism responsible for resolving $userRepository during trait loading doesn't automatically look up the IoC container, leading to an uninitialized state.

Why Traits Resist Direct Injection

Traits are designed for code reuse and defining shared contracts (behavior), not for managing the lifecycle or direct instantiation of complex objects like repositories. When you define a constructor within a trait, you are essentially defining a method that will be called when any class uses that trait, but the mechanism to inject dependencies into this context is missing.

For robust dependency management in Laravel, especially with traits, we need to shift where the responsibility for dependency resolution lies. We should focus on injecting dependencies into the consuming class, not the trait itself.

The Correct Approach: Injecting via the Consuming Class

The most idiomatic and maintainable solution is to ensure that the dependency is injected directly into the model or service that uses the trait. The trait should define the methods and properties necessary for behavior, while the consuming class handles the actual instantiation of its dependencies.

Step 1: Refactor the Trait (Focus on Behavior)

The trait should only contain the logic and properties it needs, not the dependency resolution mechanism.

// TimezoneTrait.php
trait TimezoneTrait
{
    // Dependencies are assumed to be injected into the class that uses this trait.
    protected ?UserRepositoryInterface $userRepository = null;

    public function setUserRepository(UserRepositoryInterface $repository): void
    {
        $this->userRepository = $repository;
    }

    public function findUserAndTimezone(int $userId): ?object
    {
        if (!$this->userRepository) {
            throw new \Exception("Repository not set.");
        }
        // Actual logic using the injected repository
        return $this->userRepository->find($userId);
    }
}

Step 2: Inject in the Consuming Class (The Solution)

Now, when you use this trait in your User model or service, you handle the dependency injection there. This adheres to the principle that classes should manage their own construction dependencies, which aligns perfectly with Laravel's philosophy regarding Service Providers and binding resolution (https://laravelcompany.com).

// In app/Models/User.php
use App\Traits\TimezoneTrait;
use App\Repositories\UserRepositoryInterface;

class User extends Model
{
    use TimezoneTrait;

    protected UserRepositoryInterface $userRepository;

    // Constructor handles dependency injection correctly
    public function __construct(UserRepositoryInterface $userRepository)
    {
        $this->userRepository = $userRepository;
    }

    // Now, the trait methods can rely on the injected property being set.
    public function loadDetails(int $id)
    {
        $user = $this->userRepository->find($id); // Uses the dependency provided by the constructor
        // ... rest of the logic
    }
}

Conclusion

Traits are powerful tools for code reuse, but they operate primarily on behavior rather than state management or direct dependency resolution within the Laravel Service Container context. Trying to force full constructor injection directly into a trait results in the null value because traits lack the necessary lifecycle hooks.

By refactoring the responsibility: letting the consuming class manage the injection of services (like UserRepositoryInterface) via its own constructor, you maintain clean separation of concerns. This pattern ensures your code remains testable, adheres to SOLID principles, and leverages Laravel's powerful IoC container effectively. For deeper dives into service container mechanics, always refer back to the official documentation provided by Laravel.