Service Container Basics: Managing Dependencies in PHP
Learn how a service container simplifies object creation and dependency management in PHP. Stop manual instantiation and build cleaner, scalable architectures.

Previously in this course, we explored unit testing with PHPUnit to ensure our code works as expected. In this lesson, we level up our architecture by introducing the service container, a powerful tool to handle object lifecycle management and dependency resolution automatically.
Why You Need a Service Container
As your application grows, your classes start requiring more dependencies. You might find yourself writing "new" statements everywhere:
PHP$db = new Database($config); $logger = new Logger($db); $controller = new UserController($db, $logger);
This manual approach is fragile. If the Database constructor changes, you have to update it in every place you've instantiated it. A service container is a central object registry that holds the "recipe" for creating your objects. Instead of building them yourself, you ask the container to provide them.
From Manual Injection to Centralized Registration
A service container solves three main problems:
- Registration: Storing instructions on how to build a service.
- Resolution: Creating the object (and its dependencies) only when needed.
- Simplification: Reducing the boilerplate code inside your controllers.
A Simple Worked Example
Let’s build a minimal container class. Think of this as a storage box for "closures"—anonymous functions that know how to create your objects.
PHPclass Container { protected $bindings = []; #6A9955">// Register a service with a closure public function bind(string $name, callable $resolver) { $this->bindings[$name] = $resolver; } #6A9955">// Resolve(instantiate) the service public function get(string $name) { if (!isset($this->bindings[$name])) { throw new Exception("Service not found: {$name}"); } return $this->bindings[$name]($this); } }
Now, let's register a database connection and a controller:
PHP$container = new Container(); #6A9955">// Register the Database $container->bind('db', function($c) { return new Database('localhost', 'root', 'secret'); }); #6A9955">// Register the Controller, injecting the 'db' service from the container $container->bind('UserController', function($c) { return new UserController($c->get('db')); }); #6A9955">// Use it $userController = $container->get('UserController');
By passing $c (the container) into the closure, we allow the container to resolve dependencies recursively. This is the core of implementing dependency injection at scale.
Hands-on Exercise: Containerize Your App
In your current MVC project, locate your entry point (usually index.php).
- Create a new
Containerclass file. - Move your database connection logic into a
bindregistration. - Update your router to resolve the controller from the container rather than calling
newdirectly.
This shift helps you decouple your components, a concept explored in depth when learning about Laravel service container binding.
Common Pitfalls
- Over-injecting: Don't put everything in the container. Only put services that are shared or require complex configuration.
- The "Service Locator" Anti-pattern: Try to avoid passing the entire container into your objects. Instead, use the container only in your high-level entry points (like your router or front controller) to inject dependencies into constructors.
- Recursive Loops: If Service A needs B, and B needs A, your container will trigger a stack overflow. Keep your dependency graph flat.
Frequently Asked Questions
Does every object need to be in the container?
No. Simple data objects (DTOs) or value objects should just be instantiated normally with new. Only use the container for services (database, mailers, loggers).
Is this the same as the Laravel Service Container? The principles are identical. While frameworks provide advanced features like auto-wiring (detecting types automatically), the underlying mechanism is exactly the simple binding/resolution pattern we built above. For more on how this scales, check out the Laravel service container: A beginner’s guide.
Recap
The service container acts as a factory for your application. By registering your dependencies upfront, you centralize configuration and ensure that object creation remains clean and consistent. This is a critical step in moving from basic scripting to professional, maintainable software architecture.
Up next: We will explore Events and Listeners, allowing your application to react to internal changes without tightly coupling your business logic.
Work with me

Laravel SaaS MVP & Multi-Tenant App Development
Launch your SaaS MVP on Laravel — multi-tenant, subscription-ready, and built by the engineer behind a platform serving 10,000+ paying users.

Custom WordPress Plugin Development
Custom WordPress & WooCommerce plugins built to standard — by the developer behind a plugin with 5,000+ active installs and a SaaS with 10,000+ users.
