Dependency Injection
By the end of this lesson you'll know why creating dependencies with new inside a class is a trap, and how constructor injection, interfaces, and a small DI container let you build decoupled, testable PHP — the way Laravel and Symfony do under the hood.
Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.
Part of the free Php course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
1️⃣ The Problem: new Inside a Class
A dependency is simply another object your class needs to do its job — a database, a logger, a mailer. The trap beginners fall into is letting a class build its own dependencies with new . That's called tight coupling : the two classes are glued together and can't be separated. Watch what goes wrong below.
Because UserService calls new MySQLConnection() itself, you're stuck with MySQL forever and you can never test the service without hitting a real database. The dependency is also hidden — nothing in the constructor tells a caller what this class actually needs.
2️⃣ Constructor Injection & Programming to Interfaces
Constructor injection fixes this: instead of building the dependency, the class receives it as a constructor argument. And rather than type-hinting a concrete class like MySQLConnection , you type-hint an interface — a contract that says "anything passed here must have a query() method." This is called programming to an interface , and it's what makes the class swappable and testable.
Same UserService , three different databases — including a one-off fake for testing — and the class never changed. The constructor signature now documents exactly what the service depends on. This shift, where the class no longer controls how its dependencies are created, is called Inversion of Control (IoC) : control over construction is inverted and handed to the caller.
3️⃣ A DI Container, Autowiring & Lifetimes
Wiring everything by hand gets tedious once you have dozens of services. A DI container is a registry that knows how to build each service so you don't repeat the wiring. You register a factory with bind() (a fresh object each time — a transient lifetime) or singleton() (built once and reused — a shared lifetime), then ask for a service with get() . Real containers add autowiring : they read constructor type-hints via reflection and resolve the whole dependency tree automatically, so you rarely write the factories yourself.
The PHP world standardises how you read from a container with PSR-11 : a shared ContainerInterface with just two methods, get($id) and has($id) . Laravel, Symfony, and PHP-DI all implement it, so code that type-hints the interface works with any of them.
4️⃣ Your Turn: Inject a Dependency
Now you try. The script below builds its mailer the wrong way — convert it to constructor injection. Fill in each ___ using the 👉 hint, then run it and check it against the Output panel.
One more — this time register a service as a singleton and prove the container hands back the same object twice.
📋 Quick Reference — Dependency Injection
No code is filled in this time — just a brief and an outline. Write it yourself, run it on onecompiler.com/php or your own machine, then check your result against the expected output in the comments. This is the same write-run-check loop you'll use on every real refactor.
Practice quiz
What is dependency injection in one sentence?
- A class loads its dependencies from a global variable
- PHP automatically injects every class
- A class is handed the objects it needs from outside instead of creating them with 'new'
- A way to compress objects before storing them
Answer: A class is handed the objects it needs from outside instead of creating them with 'new'. DI means you pass dependencies in (usually via the constructor) rather than letting the class build them itself.
What problem does calling 'new MySQLConnection()' inside a class cause?
- Tight coupling — the class is glued to MySQL and can't be tested or swapped
- A syntax error
- Slower compilation
- Duplicate database rows
Answer: Tight coupling — the class is glued to MySQL and can't be tested or swapped. Building a dependency internally welds the class to one implementation, so you can't swap it or pass a fake in tests.
What is the preferred way to inject a required dependency?
- Setter injection after construction
- A global $container variable
- Reading it from $_SESSION
- Constructor injection
Answer: Constructor injection. Constructor injection means a required dependency is supplied at construction, so the object can never exist in a half-built, invalid state.
You should type-hint a dependency as what?
- Always the concrete class
- An interface (the contract), not a concrete class
- The string 'mixed'
- An array
Answer: An interface (the contract), not a concrete class. Programming to an interface (e.g. private Database $db) lets any implementation, including a test fake, be injected.
What is Inversion of Control (IoC)?
- The class no longer controls how its dependencies are created — that control is handed to the caller or container
- The database controls the application
- Loops run in reverse order
- Errors are thrown instead of returned
Answer: The class no longer controls how its dependencies are created — that control is handed to the caller or container. IoC is the broader idea; dependency injection is the most common way to achieve it.
What is a DI container?
- A Docker image for PHP
- A type of array
- A registry that knows how to build each service so you don't wire them by hand
- A database table of objects
Answer: A registry that knows how to build each service so you don't wire them by hand. You register how each service is built once, and the container constructs the object graph when you ask for something.
In the lesson's container, what does bind() create on each resolve?
- The same shared instance forever
- A fresh new instance every time (transient lifetime)
- Nothing — it only registers a name
- A database row
Answer: A fresh new instance every time (transient lifetime). bind() returns a new object every resolve (transient); singleton() builds once and reuses the same object (shared).
When should a service be a singleton (shared lifetime)?
- For every object without exception
- Only for objects you mutate per request
- Never — singletons are an anti-pattern
- For stateless, expensive-to-build, or globally-shared services like a logger or DB connection
Answer: For stateless, expensive-to-build, or globally-shared services like a logger or DB connection. Share things safe to share (logger, connection, config); create fresh instances whenever shared state would cause bugs.
What does PSR-11 define?
- A password hashing standard
- A common container interface with get($id) and has($id)
- The cron expression format
- A logging format
Answer: A common container interface with get($id) and has($id). PSR-11's ContainerInterface standardises how you read services from a container, so code works with Laravel, Symfony, PHP-DI, etc.
Why is passing the whole Container into a class (the service locator) an anti-pattern?
- It is slower than autowiring
- Containers can't be passed as arguments
- It hides the real dependencies — the constructor no longer says what the class needs
- It causes SQL injection
Answer: It hides the real dependencies — the constructor no longer says what the class needs. Inject the specific services a class requires, not the container, so dependencies stay visible in the constructor signature.