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.
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.
Common Errors (and the fix)
- Calling new on a dependency inside a class — the original sin of tight coupling. The moment you write $this->db = new MySQLConnection(); the class is welded to MySQL and can't be tested. Accept the dependency as a constructor argument instead and let the caller decide.
- The service locator anti-pattern — passing the whole Container into a class and calling $container->get(...) inside it. This just hides the dependencies again — the constructor no longer tells you what the class needs. Inject the specific services it requires, not the container.
- Over-injection (too many dependencies) — a constructor with 7+ injected services is a smell that the class is doing too much. Split it into smaller, focused classes with 2–3 dependencies each; the pain of a huge constructor is the design telling you something.
- Type-hinting a concrete class instead of an interface — writing __construct(private MySQLConnection $db) brings tight coupling back in disguise. Depend on an interface ( private Database $db ) so any implementation, including a test fake, can be injected.
- "Uncaught Error: Too few arguments to function __construct()" — you tried new UserService() after adding an injected parameter. With DI the caller must supply the dependency: new UserService(new MySQLConnection()) , or let a container build it.
Pro Tips
- 💡 Prefer constructor injection over setter injection. A required dependency in the constructor means the object can never exist in a half-built, invalid state.
- 💡 Use PHP 8 constructor promotion to keep DI tidy: public function __construct(private readonly Database $db) {} declares and stores the property in one line.
- 💡 Let the framework's container autowire. In Laravel or Symfony you usually just type-hint a dependency and the container builds and injects it for you — you rarely write factories by hand.
📋 Quick Reference — Dependency Injection
Term
Example
What It Means
Constructor injection
__construct(private Database $db)
Pass dependencies in (preferred)
Program to interface
private Database $db
Type-hint the contract, not a class
bind()
$c->bind(Id::class, fn)
New instance each resolve (transient)
singleton()
$c->singleton(Id::class, fn)
Built once, reused (shared)
get() / resolve()
$c->get(Id::class)
Fetch an instance from the container
PSR-11
ContainerInterface
Standard get()/has() container API
Frequently Asked Questions
Mini-Challenge: A Pluggable Notifier
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.
🎉 Lesson Complete!
- ✅ Building dependencies with new inside a class causes tight coupling — untestable and unswappable
- ✅ Constructor injection passes dependencies in from outside, so the caller stays in control
- ✅ Program to an interface , never a concrete class, so any implementation (including a test fake) fits
- ✅ Inversion of Control means the class no longer decides how its dependencies are created
- ✅ A DI container registers services with bind() / singleton() and resolves them with get()
- ✅ Lifetimes : singleton = one shared instance, bind = a fresh one each time; PSR-11 standardises the container API
- ✅ Next lesson: Advanced Error Handling — custom exception hierarchies and clean failure paths
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.
Continue this course
- Previous: Advanced OOP Patterns
- Next: Advanced Error Handling