Dependency Injection & Conciseness in Modern PHP
A0mineTV

A0mineTV @blamsa0mine

About: 💻 Freelance Web Developer specializing in PHP, Laravel, and Vue.js. 🎯 Passionate about building elegant and efficient solutions. 🚀 "Code with passion, share with purpose."

Location:
France
Joined:
Nov 19, 2024

Dependency Injection & Conciseness in Modern PHP

Publish Date: Jun 6
0 1

TL;DR — Constructor Property Promotion (PHP 8), typed properties, and PSR-11
DI containers let you inject services in one line per dependency.

Less boilerplate ⇢ more readable code ⇢ happier developers.


1- Why Dependency Injection at all?

  • Loose coupling – swap an implementation (e.g. a logger) without touching business logic.
  • Testability – pass a fake or mock object and run fast unit tests.
  • Single Responsibility – classes specify what they need, not where to find it.

Classic PHP achieved this via manual setters or verbose constructors.

Modern PHP gives us language features that turn DI into a one-liner.


2- Constructor Property Promotion (PHP 8)

Before PHP 8 you needed three steps per dependency:

class ReportController
{
    private LoggerInterface $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }
}
Enter fullscreen mode Exit fullscreen mode

Since PHP 8 you can declare + type + assign in the constructor signature:

class ReportController
{
    public function __construct(private LoggerInterface $logger) {}
}
Enter fullscreen mode Exit fullscreen mode

That’s one line instead of four, and the property is ready everywhere in the class.

 Visibility & immutability

class ExportService
{
    public function __construct(
        public  readonly CsvWriter $csvWriter,
        private readonly StorageAdapter $storage
    ) {}
}
Enter fullscreen mode Exit fullscreen mode
  • public|protected|private is required.
  • readonly (PHP 8.1) forbids re-assignment after construction → safer objects.

3- Typed Properties (PHP 7.4+)

Typed properties eliminate PHPDoc duplication:

private CacheItemPoolInterface $cache; // real type, enforced by engine
Enter fullscreen mode Exit fullscreen mode

Combine with promotion for concise & type-safe DI.


4- Using a PSR-11 Container

A container builds objects and resolves their dependencies.
Most frameworks come with one (Illuminate\Container, Symfony DI, Laminas DI), but you can stay framework-agnostic with league/container:

composer require league/container
Enter fullscreen mode Exit fullscreen mode
use League\Container\Container;
use Psr\Log\LoggerInterface;
use Monolog\Logger;
use Monolog\Handler\StreamHandler;

$container = new Container();

/* 1. register definitions */
$container->add(LoggerInterface::class, function () {
    $log = new Logger('app');
    $log->pushHandler(new StreamHandler('php://stdout'));
    return $log;
});

/* 2. define a service using constructor promotion */
class Greeter
{
    public function __construct(private LoggerInterface $logger) {}

    public function greet(string $name): void
    {
        $this->logger->info("Hello {$name}");
    }
}

/* 3. resolve & use */
$greeter = $container->get(Greeter::class);
$greeter->greet('DEV Community');
Enter fullscreen mode Exit fullscreen mode

The container inspects the Greeter constructor, sees a LoggerInterface
parameter, and supplies the registered instance.


5- “Plain Old PHP” Without a Container

For micro-scripts a full container can feel heavy.
You can still reap promotion benefits by wiring dependencies by hand:

$logger  = new Logger('cli');
$cliTool = new CliTool($logger);   // promotion assigns $logger
Enter fullscreen mode Exit fullscreen mode

Small project? Acceptable. Bigger code-base? Prefer a container or framework
so wiring logic isn’t scattered everywhere.


6- Best Practices

✅ Do ❌ Avoid
Type-hint every promoted property. mixed properties (missing type).
Limit constructor to ~5 deps – else refactor. God objects with 10+ services.
Use readonly for immutable deps. Reassigning promoted properties.
Document constructor params via PHP promoted syntax, not PHPDoc. Redundant @var tags that drift out of sync.

7- Pitfalls to Watch For

  • Circular dependencies – containers will throw “circular reference” errors.
  • Lazy services – if a service is heavy (e.g., DB connection), register it as a factory or use lazy proxies.
  • Visibility mismatch – private is inaccessible to child classes; choose protected if subclasses need the dependency.

8- Conclusion

Constructor property promotion + typed properties erase the ceremony around
dependency injection. When paired with a PSR-11 container you get:

  • Cleaner constructors
  • Engine-enforced types
  • Seamless test doubles

Next time you spin up a PHP 8 project, try this pattern—you’ll never look back.

Comments 1 total

Add comment