Dependency injection works fine without a container

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.