Back to Blog
Lesson 54 of the PHP: Modern PHP from the Ground Up course
PHPSeptember 11, 20263 min read

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.

phpservice containerdependency injectionarchitectureoop
Row of vibrant cargo containers on train tracks under a clear blue sky.

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:

  1. Registration: Storing instructions on how to build a service.
  2. Resolution: Creating the object (and its dependencies) only when needed.
  3. 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.

PHP
class 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).

  1. Create a new Container class file.
  2. Move your database connection logic into a bind registration.
  3. Update your router to resolve the controller from the container rather than calling new directly.

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.

Similar Posts