PHP Image Processing: Resizing, Compression, and Thumbnails Without Killing Performance

10 min read 1,962 words
PHP Image Processing: Resizing, Compression, and Thumbnails Without Killing Performance featured image

What Dependency Injection Actually Means in PHP

Dependency injection in PHP means passing dependencies into a class from outside rather than having the class create them itself. The class does not instantiate its own dependencies. Instead, something else provides them, usually through a constructor or a setter method.

This sounds like a small change, but it has a significant effect on how code behaves. When a class creates its own dependencies, it becomes tightly coupled to those specific implementations. Changing one of those dependencies means editing the class itself. When dependencies are injected, the class becomes more flexible and can work with different implementations without modification.

For a PHP developer working on projects that need to last, this approach makes a practical difference. It separates the concern of how something works from the concern of what it works with, which keeps code easier to modify over time.

The Problem With Classes That Create Their Own Dependencies

Consider a simple example. A UserNotifier class needs to send emails. A common mistake is to have the class create its own email service directly.

class UserNotifier
{
    private $mailer;

    public function __construct()
    {
        $this->mailer = new PHPMailerMailer();
    }

    public function notifyUser(User $user, string $message): void
    {
        $this->mailer->send($user->email, $message);
    }
}

This works initially. The UserNotifier can send emails. But testing becomes difficult. Any test that uses UserNotifier will also call the real PHPMailerMailer, which means tests depend on a working email server, valid SMTP credentials, and network access. That is not a test. That is a fragile setup that will fail for the wrong reasons.

The same problem appears when the code needs to change. If the project switches from PHPMailer to a different mail library, every class that creates its own mailer must be edited. In a larger project, that can mean dozens of places to update, each with its own risk of breaking something.

Passing Dependencies In Instead of Creating Them

Dependency injection solves this by making the class accept its dependencies from outside. The class does not care where the mailer comes from. It only cares that it has one when it needs to send a message.

class UserNotifier
{
    private MailerInterface $mailer;

    public function __construct(MailerInterface $mailer)
    {
        $this->mailer = $mailer;
    }

    public function notifyUser(User $user, string $message): void
    {
        $this->mailer->send($user->email, $message);
    }
}

The UserNotifier now depends on MailerInterface, which is an abstraction. Any class that implements that interface can be injected. In production, a real mailer goes in. In tests, a test double goes in instead.

Using a Test Double in Practice

A test double is a fake implementation used during testing. It behaves like the real thing but does not have side effects like sending actual emails or querying a live database.

class FakeMailer implements MailerInterface
{
    public array $sentMessages = [];

    public function send(string $to, string $message): void
    {
        $this->sentMessages[] = ['to' => $to, 'message' => $message];
    }
}

Now a test can inject the fake mailer and verify the notifier behaves correctly.

public function testNotifyUserSendsCorrectEmail(): void
{
    $fakeMailer = new FakeMailer();
    $notifier = new UserNotifier($fakeMailer);
    $user = new User('alex@example.com');

    $notifier->notifyUser($user, 'Your order has shipped.');

    $this->assertCount(1, $fakeMailer->sentMessages);
    $this->assertEquals('alex@example.com', $fakeMailer->sentMessages[0]['to']);
    $this->assertStringContainsString('shipped', $fakeMailer->sentMessages[0]['message']);
}

The test runs quickly, has no external dependencies, and clearly shows what the code is supposed to do. That is the practical benefit of dependency injection for testing.

Types of Dependency Injection in PHP

There are three common ways to inject dependencies into a class.

Constructor Injection

Dependencies are passed through the constructor. This is the most common approach and works well when a class needs its dependencies for its entire lifetime.

public function __construct(DatabaseConnection $db, LoggerInterface $logger)
{
    $this->db = $db;
    $this->logger = $logger;
}

Constructor injection makes dependencies explicit. Anyone reading the class can see what it needs to function. It also ensures the object is in a valid state before any methods are called.

Setter Injection

Dependencies are set through setter methods after the object is created. This is useful when a dependency might not always be needed, or when it needs to change during the object's lifetime.

public function setCache(CacheInterface $cache): void
{
    $this->cache = $cache;
}

Setter injection requires more care. A class using setter injection should handle the case where a dependency has not been set, or document clearly that certain methods require it. Failing to do this leads to null reference errors at runtime.

Interface Injection

An interface defines a setter method that implementing classes must provide. The injecting code uses that interface to set the dependency. This approach is less common in PHP but appears in some frameworks and container systems.

interface InjectableLogger
{
    public function injectLogger(LoggerInterface $logger): void;
}

class ReportGenerator implements InjectableLogger
{
    private ?LoggerInterface $logger = null;

    public function injectLogger(LoggerInterface $logger): void
    {
        $this->logger = $logger;
    }
}

For most PHP projects, constructor injection covers the majority of cases. Setter injection is useful for optional dependencies. Interface injection is typically reserved for framework integration.

How Dependency Injection Relates to Inversion of Control

Dependency injection is one form of inversion of control. Inversion of control means that the flow of a program is controlled by something external rather than by the code itself. With dependency injection, the control over which dependencies are used shifts from the class to the calling code or a container.

Dependency injection containers take this further by managing object creation and injection automatically. A container holds configuration about which concrete classes to use for each interface, and it constructs objects with the correct dependencies when requested.

$container = new Container();
$container->bind(MailerInterface::class, PHPMailerMailer::class);

$notifier = $container->make(UserNotifier::class);

The container resolves UserNotifier, sees it needs a MailerInterface, looks up the binding, creates a PHPMailerMailer, and injects it. The calling code does not need to know the details.

Containers are useful in larger applications where manually wiring dependencies becomes tedious. They also make it easier to swap implementations for different environments, such as using a fake mailer in tests and a real one in production.

Common Mistakes When Applying Dependency Injection

Dependency injection is straightforward in principle, but a few common mistakes can reduce its usefulness or create new problems.

  • Injecting too many dependencies: A class that takes ten dependencies in its constructor is a sign that the class is doing too much. Consider splitting it into smaller classes with fewer responsibilities.
  • Injecting the container itself: Passing a container into a class to resolve dependencies on demand defeats the purpose. The class still depends on the container, and testing becomes harder because the container must be mocked. Inject the specific dependencies instead.
  • Using dependency injection without interfaces: Injecting a concrete class still creates coupling. If the class cannot be replaced without changing the consuming code, the flexibility is limited. Use interfaces for dependencies that might change.
  • Creating service locators: A service locator is a static object that classes call to fetch dependencies. It hides dependencies from the constructor, making code harder to understand and test. Constructor injection makes dependencies visible and explicit.

Writing Testable PHP Classes: A Practical Workflow

Applying dependency injection to an existing codebase can feel overwhelming if the project is large. A practical approach is to write new classes with dependency injection from the start, and gradually refactor existing classes when they need changes or when tests are added.

For a new class, the workflow looks like this.

  1. Identify what the class needs to work: Does it need a database connection? Does it need to send notifications? Does it need to read configuration? These are candidates for dependencies.
  2. Define interfaces for external dependencies: If the class needs a mailer, define a MailerInterface first. This makes the contract explicit and allows different implementations.
  3. Accept dependencies through the constructor: Write the constructor to accept the interface types. Do not create instances inside the class.
  4. Write tests using test doubles: Create fake implementations of the interfaces and inject them. Verify the class behaves correctly without touching real external services.
  5. Wire everything together at the entry point: In the controller, command, or bootstrap file, create the real implementations and inject them into the classes that need them.

This workflow keeps new code testable from the start. Over time, the codebase accumulates more testable code, and the risk of breaking existing functionality when making changes reduces significantly.

Dependency Injection and Long-Term Maintainability

The real value of dependency injection becomes apparent over months and years, not weeks. Projects that use it are easier to change because dependencies are explicit and replaceable. Adding a new implementation, swapping a service, or writing tests does not require rewriting the consuming classes.

This matters for business websites and applications that need ongoing maintenance. When requirements change, when third-party services are updated, or when new features are added, a codebase built with dependency injection adapts more smoothly. The cost and return on investment of a custom booking system, for example, depend partly on how maintainable the underlying code is. Hard-to-change code accumulates technical debt that eventually slows every addition.

Dependency injection is one piece of a larger picture that includes clear responsibilities, sensible abstractions, and consistent patterns. On its own it does not solve everything, but it creates a foundation that makes other good practices easier to apply.

Related practical reading

These related guides can help you connect this topic with the wider website, server, security, and support decisions around it.

Frequently Asked Questions

Does dependency injection require a framework?
No. Dependency injection is a pattern that can be applied in plain PHP without any framework. Many PHP developers use it with containers like PHP-DI or Symfony DI, but a container is optional. Constructor injection works perfectly well with manual wiring, and the pattern is the same regardless of what tools are used.
When should I use an interface versus a concrete class?
Use an interface when the dependency is likely to change, when there are multiple implementations, or when the dependency is external such as a third-party library or database. Use a concrete class when the dependency is stable and unlikely to have alternatives. Not every dependency needs an interface, but reaching for one early is safer than retrofitting it later.
Can dependency injection cause performance issues?
In most web applications, the performance impact is negligible. Constructor calls and interface lookups add microseconds at most. If a project has thousands of requests per second, a dependency injection container with complex resolution logic might become a bottleneck, but this is rare and usually solvable with container caching. The maintainability benefits almost always outweigh the minimal overhead.
How do I introduce dependency injection into an existing project?
Start with new code. Write any new classes using dependency injection and interfaces. For existing code, refactor incrementally when changes are needed or when adding tests. Trying to rewrite a large codebase all at once introduces too much risk. The gradual approach keeps the project stable while improving its structure over time.
What is the difference between dependency injection and a service locator?
A service locator is a registry that classes use to request dependencies on demand. A class using a service locator calls something like $locator->get(MailerInterface::class) inside a method. This hides dependencies, makes testing harder, and creates implicit coupling. Dependency injection passes dependencies in through the constructor, making them visible and explicit. The service locator pattern is generally considered an anti-pattern in modern PHP development.