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;
}
}
Since PHP 8 you can declare + type + assign in the constructor signature:
class ReportController
{
public function __construct(private LoggerInterface $logger) {}
}
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
) {}
}
-
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
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
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');
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
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.