Dependency injection is passing collaborators in rather than constructing them inside. A container is a tool for assembling those graphs automatically. The two get conflated so thoroughly that projects add a container before they have any injection at all.
// not DI: the dependency is chosen here and cannot be replaced
final class Mailer
{
public function send(Message $m) { (new SmtpClient())->deliver($m); }
}
// DI, no container required
final class Mailer
{
private $transport;
public function __construct(Transport $transport)
{
$this->transport = $transport;
}
public function send(Message $m) { $this->transport->deliver($m); }
}
The second version is testable and reconfigurable with nothing but a constructor. A container becomes worth the indirection when the graph gets deep enough that wiring it by hand in the entry point is genuinely tedious — which for a small application may be never. Adding one first tends to produce service locators wearing a container’s clothes.