Skip to Content
Course content

132: Symfony Dependency Injection Container

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this exact pattern dozens of times when developers move from plain PHP to Symfony. They understand the concept of a service, but they keep falling back on their old habits of manual instantiation. Take a look at this NewsletterManager class. At first glance, it looks perfectly fine, but it's actually a maintenance nightmare waiting to happen.

// src/Service/NewsletterManager.php
namespace App\Service;

use App\Service\Mailer;
use App\Repository\UserRepository;

class NewsletterManager
{
    private $mailer;
    private $userRepository;

    public function __construct()
    {
        // The "New" Keyword Trap
        $this->mailer = new Mailer(); 
        $this->userRepository = new UserRepository();
    }

    public function sendToAll()
    {
        $users = $this->userRepository->findAll();
        foreach ($users as $user) {
            $this->mailer->send($user->getEmail(), 'Weekly Update!');
        }
    }
}

The Problem: Hard-Coded Dependencies

The bug here isn't a syntax error—the code will technically run until it hits a wall. The problem is that NewsletterManager is now "tightly coupled" to the specific implementations of Mailer and UserRepository.

Imagine if the Mailer class needs an API key from your .env file to work. Since you're calling new Mailer() inside the constructor, the Mailer is created without any configuration. It's a "naked" object. To fix it, you'd have to pass the API key into the NewsletterManager, and then pass it again into the Mailer. If you have ten different services using the mailer, you're now passing that API key through ten different constructors. It's a mess.

The Fix: Embracing Constructor Injection

In Symfony, we don't create our own dependencies. We ask for them. The Dependency Injection (DI) Container is essentially a giant map that knows how to instantiate every service in your app and what those services need to function.

Here is how we rewrite this to let Symfony's container do the heavy lifting:

// src/Service/NewsletterManager.php
namespace App\Service;

use App\Service\Mailer;
use App\Repository\UserRepository;

class NewsletterManager
{
    private $mailer;
    private $userRepository;

    // We type-hint the dependencies we need
    public function __construct(Mailer $mailer, UserRepository $userRepository)
    {
        $this->mailer = $mailer;
        $this->userRepository = $userRepository;
    }

    public function sendToAll()
    {
        $users = $this->userRepository->findAll();
        foreach ($users as $user) {
            $this->mailer->send($user->getEmail(), 'Weekly Update!');
        }
    }
}

Now, when Symfony sees that NewsletterManager needs a Mailer, it looks into its container, finds the already-configured Mailer instance (with its API keys and settings), and injects it automatically. This is called Autowiring.

Managing Non-Class Dependencies

Sometimes you don't need another class; you need a string, an integer, or a path to a folder. You can't autowire a string because Symfony wouldn't know which string you want. This is where config/services.yaml comes back into play.

If your Mailer needs a specific sender_email from your parameters, you define it like this:

# config/services.yaml
parameters:
    app.sender_email: 'noreply@example.com'

services:
    # Default configuration for services in *this* file
    _defaults:
        autowire: true      
        autoconfigure: true 

    App\Service\Mailer:
        arguments:
            $senderEmail: '%app.sender_email%'

By doing this, you've separated your logic (how to send a mail) from your configuration (who is sending the mail). If you decide to change the sender email, you change one line in a YAML file, and every service using the Mailer is updated instantly. I can't stress enough how much this saves you during deployment across different environments (staging vs. production).




📋 Practical Task

Build a Configurable Discount Calculator Service

Your goal is to implement a DiscountCalculator service that is properly integrated into the Symfony DI container. This service should not use the new keyword for any of its dependencies.

  • Requirement 1: Create a DiscountCalculator service. It should depend on a ProductRepository (you can mock this as a simple class) to fetch product prices.
  • Requirement 2: The service needs a percentage value (e.g., 20 for 20%) that is NOT hard-coded in the class. Define this value in services.yaml under parameters and inject it into the constructor using a named argument.
  • Requirement 3: Create a simple Controller action that injects the DiscountCalculator and returns the discounted price of a dummy product ID.
  • Requirement 4: Ensure that autowire: true is enabled in your service configuration so that the ProductRepository is handled automatically.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.