DEV Community

A0mineTV
A0mineTV

Posted on

Dependency Injection & Conciseness in Modern PHP

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.

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.