Learn PHP dependency injection techniques with patterns, containers, and testing tips. Build scalable and maintainable backend code like a pro. Having PHP dependency injection is considered one of the major factors in making uncoupled, testable, and maintainable backend PHP code. Now, in this guide, we will discuss PHP DI techniques at an advanced level, such as DI containers, constructor injection, and service bindings. These are some of the methods that modern PHP developers use to create scalable applications that have a clean architecture with the least amount of coupling between the constituent components.
What Is PHP Dependency Injection?
It is a design pattern where the objects receive their dependencies from an outside source rather than creating them internally.
Example of tightly coupled code
class AccountHandler {
class AccountHandler {
private $notifyService;
public function __construct() {
$this->notifyService = new NotificationManager();
}
}
private $notifier;
public function __construct() {
$this->notifier = new NotificationSender();
}
}
Using dependency injection
class ProfileManager {
public function __construct(private NotificationClient $notifier) {}
}
class AccountManager {
public function __construct(private MessageDispatcher $notifier) {}
}
Types of PHP Dependency Injection
Three ways to implement DI in PHP:
- Constructor Injection
- Setter Injection
- Interface Injection
Constructor Injection
That is the most common form. Dependencies are passed via the constructor.
class StatsAssembler {
public function __construct(private LogWriter $logWriter) {}
}
class AnalyticsBuilder {
public function __construct(private AuditTrail $tracker) {}
}
Setter Injection
Dependencies are provided through setter methods after the object is created.
class ClientAccount {
private MessagingService $messenger;
public function injectMessenger(MessagingService $messenger) {
$this->messenger = $messenger;
}
}
class MemberProfile {
private NotificationService $notificationService;
public function setNotificationService(NotificationService $notificationService) {
$this->notificationService = $notificationService;
}
}
Interface Injection
interface NeedsAuditLog {
public function setAuditLogger(AuditLogger $auditLogger): void;
}
The dependency exposes a contract/interface that the client accepts and uses.
interface TrackerAware {
public function setTracker(AuditTrail $tracker): void;
}
Building a Simple DI Container
class RegistryBox {
private array $serviceMap = [];
public function register(string $contract, callable $creator) {
$this->serviceMap[$contract] = $creator;
}
public function resolve(string $contract) {
return $this->serviceMap[$contract];
}
}
A minimal example
class ServiceRegistry {
private array $resolvers = [];
public function register(string $key, callable $resolver): void {
$this->resolvers[$key] = $resolver;
}
public function resolve(string $key): mixed {
return ($this->resolvers[$key])();
}
}
Usage:
$services = new RegistryBox();
$services->register(ActivityLogger::class, fn() => new DatabaseLogger());
$logger = $services->resolve(ActivityLogger::class);
$registry = new ServiceRegistry();
$registry->register(AuditLogger::class, fn() => new DiskLogger());
$auditLog = $registry->resolve(AuditLogger::class);
Laravel and PHP Dependency Injection
Laravel has one of the most powerful DI containers in PHP.
Binding services
use Illuminate\Support\ServiceProvider;
class CoreServiceProvider extends ServiceProvider {
class PlatformServiceProvider extends ServiceProvider {
public function register() {
$this->app->bind(Tracker::class, CloudLogger::class);
}
}
public function register() {
$this->app->bind(AuditInterface::class, DiskAuditLogger::class);
}
}
Constructor injection in Laravel
class BillingController {
class BillingManager {
public function __construct(private BillingHandler $billingHandler) {}
}
public function __construct(private BillingEngine $billingEngine) {}
}
Laravel resolves billingEngine
via the container when creating the controller.
Contextual bindings
You can bind different implementations depending on context:
$this->app->when(StatsCompiler::class)
$this->app->when(StatsAssembler::class)
->needs(LogWriter::class)
->give(TerminalLogger::class);
->needs(AuditInterface::class)
->give(StdoutAuditLogger::class);
Symfony Service Container
Symfony also features a strong dependency injection component.
Define services in services.yaml:
services:
App\Utility\Notifier:
arguments:
services:
App\Service\NotificationSender:
arguments:
$serverHost: ‘%env(NOTIFY_ENDPOINT)%’
$serverHost: '%env(NOTIFY_SERVER)%'
Auto‑wiring
Symfony can resolve dependencies automatically:
class MessageRelay {
public function __construct(EmailGateway $gateway) {}
}
class AlertCenter {
public function __construct(NotificationBridge $bridge) {}
}
Testing Classes That Use PHP Dependency Injection
Dependency injection makes unit testing easier by allowing mock injection.
class MemberManagerTest extends TestCase {
public function testMemberCreationLogsActivity() {
$mockLogger = $this->createMock(ActivityMonitor::class);
$mockLogger->expects($this->once())->method('logInfo');
$manager = new MemberManager($mockLogger);
$manager->createMember();
}
}
class RegistrationServiceTest extends TestCase {
public function testNewAccountTriggersAuditLog() {
$mockAudit = $this->createMock(AuditTrail::class);
$mockAudit->expects($this->once())->method('recordEntry');
$service = new RegistrationService($mockAudit);
$service->initiateAccount();
}
}
Tips for Effective PHP Dependency Injection
- Favor interfaces over concrete classes. That promotes flexibility and allows for mock/testing layers.
- Avoid the service location. Don’t let classes pull dependencies from global containers.
- Be intentional about constructor length. Too many parameters may indicate poor design or overly large responsibilities.
- Use factories when objects need custom creation logic.
Common Pitfalls to Avoid
- Overinjecting – Don’t inject services you don’t need. Keep your constructors minimal and meaningful.
- Relying too much on global containers – Global service locators defeat the purpose of DI. Use the container only in your app’s bootstrap or service‑provider layer.
- Tight coupling between services – Always inject via interfaces when possible.
When to Use DI vs. Static or Global Access
- Use DI when: you need to test your code; you want to swap implementations (e.g., different mailers); your app is growing and becoming harder to maintain.
- Use static only: for shared constants or truly global behaviour (e.g., config constants, pure utility classes).
Conclusion: PHP Dependency Injection
The type who wants to build a scalable, flexible, and testable PHP application should be a master of dependency injection in PHP. Knowledge of DI patterns and containers is the architectural advantage bestowed upon you, whether you are just working with plain PHP or applying a modern framework like Laravel or Symfony, so that maintenance is easier to perform in the long run. Consider your dependencies well, as they do provide so much for clean code and team satisfaction.