0

Dependency Injection — constructor injection, setter injection, method injection

Intermediate5 min read·php-08-010
interviewsolidframework

Concept

Dependency Injection (DI) is a design technique where a class's dependencies are provided from outside rather than created internally. DI makes classes testable (dependencies can be swapped for mocks), decoupled (classes don't know how dependencies are created), and explicit (requirements are visible in the constructor signature).

Constructor injection (preferred): Dependencies are passed as constructor parameters. The object is fully initialized with all dependencies at construction time. Type-hint against interfaces, not concrete classes — this is the Dependency Inversion Principle.

Setter injection: Dependencies are provided via setter methods after construction. Use when a dependency is optional or when you need to change it after construction. Less safe than constructor injection because the object may be used before the dependency is set.

Method injection: Dependency is passed directly to the method that needs it. Use when a dependency is only needed for one specific operation and isn't needed elsewhere in the class.

The DI Container (Service Container in Laravel): Automates DI. When you type-hint an interface, the container looks up which concrete class is bound to that interface and injects it. app(SomeInterface::class) or automatic resolution in controllers/commands/jobs. Without a container, you manually wire dependencies in application bootstrap code ("poor man's DI").

Code Example

php
<?php
declare(strict_types=1);

// Interface (the abstraction)
interface MailerInterface
{
    public function send(string $to, string $subject, string $body): void;
}

interface LoggerInterface
{
    public function info(string $message): void;
}

// BAD — internal creation (tightly coupled, hard to test)
class UserServiceBad
{
    private \Mailer $mailer;

    public function __construct()
    {
        $this->mailer = new \Mailer(); // hard dependency — can't be swapped
    }
}

// GOOD — constructor injection (loosely coupled, testable)
class UserService
{
    public function __construct(
        private readonly MailerInterface $mailer,
        private readonly LoggerInterface $logger,
    ) {}

    public function register(string $email, string $password): void
    {
        // ... create user ...
        $this->logger->info("User registered: $email");
        $this->mailer->send($email, 'Welcome!', 'Thanks for signing up');
    }
}

// Setter injection — optional dependency
class ReportGenerator
{
    private ?LoggerInterface $logger = null;

    public function setLogger(LoggerInterface $logger): void
    {
        $this->logger = $logger;
    }

    public function generate(): array
    {
        $this->logger?->info('Generating report');
        return [];
    }
}

// Method injection — one-off dependency
class DataProcessor
{
    public function process(array $data, LoggerInterface $logger): array
    {
        $logger->info('Processing ' . count($data) . ' items');
        return array_map(fn($item) => $item * 2, $data);
    }
}

// Manual wiring (no container)
$mailer  = new SmtpMailer($config['smtp']);
$logger  = new FileLogger('/var/log/app.log');
$service = new UserService($mailer, $logger);

// Laravel container wiring (in ServiceProvider)
// $this->app->bind(MailerInterface::class, SmtpMailer::class);
// $this->app->bind(LoggerInterface::class, FileLogger::class);
// Then UserService::class is auto-resolved from the constructor type-hints