| Attribute | Dependency Injection | Service Locator |
|---|---|---|
| How dependencies are provided | Injected explicitly via constructor or setter | Resolved from a global or passed locator object |
| Visibility | Dependencies are explicit in the API | Dependencies are hidden inside the locator |
| Testability | Easy to mock and unit test | Harder to isolate; often requires container setup |
| Ease of use | More boilerplate for wiring | Convenient for quick lookups and legacy code |
| Best for | Large codebases, teams, libraries | Small apps, scripts, or transitional code |
Definition Dependencies are provided to an object from the outside, typically via constructor injection, setter injection, or method injection. The object does not know how to create its dependencies.
Definition A central registry or container exposes a method to retrieve services by name or interface. Objects ask the locator for the dependencies they need.
We have a Mailer service and an OrderProcessor that needs to send confirmation emails.
<?php
interface MailerInterface {
public function send(string $to, string $subject, string $body): bool;
}
class SmtpMailer implements MailerInterface {
public function send(string $to, string $subject, string $body): bool {
// send via SMTP
return true;
}
}
class OrderProcessor {
private MailerInterface $mailer;
public function __construct(MailerInterface $mailer) {
$this->mailer = $mailer;
}
public function process(array $order): void {
// process order...
$this->mailer->send($order['email'], 'Order received', 'Thanks for your order');
}
}
// wiring
$mailer = new SmtpMailer();
$processor = new OrderProcessor($mailer);
$processor->process(['email' => 'alice@example.com']);
<?php
class ServiceLocator {
private array $services = [];
public function set(string $id, $service): void {
$this->services[$id] = $service;
}
public function get(string $id) {
return $this->services[$id] ?? null;
}
}
class OrderProcessor {
private ServiceLocator $locator;
public function __construct(ServiceLocator $locator) {
$this->locator = $locator;
}
public function process(array $order): void {
$mailer = $this->locator->get('mailer');
$mailer->send($order['email'], 'Order received', 'Thanks for your order');
}
}
// wiring
$locator = new ServiceLocator();
$locator->set('mailer', new SmtpMailer());
$processor = new OrderProcessor($locator);
$processor->process(['email' => 'alice@example.com']);
With DI you can pass a test double directly into the constructor. No container setup required.
<?php
use PHPUnit\Framework\TestCase;
class OrderProcessorTest extends TestCase {
public function testSendsEmail() {
$mailer = $this->createMock(MailerInterface::class);
$mailer->expects($this->once())
->method('send')
->with('alice@example.com', 'Order received', $this->anything())
->willReturn(true);
$processor = new OrderProcessor($mailer);
$processor->process(['email' => 'alice@example.com']);
}
}
With a service locator you typically need to create a locator and register mocks, or mock the locator itself. This adds indirection and setup.
<?php
class OrderProcessorTest extends TestCase {
public function testSendsEmail() {
$mailer = $this->createMock(MailerInterface::class);
$mailer->expects($this->once())
->method('send');
$locator = new ServiceLocator();
$locator->set('mailer', $mailer);
$processor = new OrderProcessor($locator);
$processor->process(['email' => 'alice@example.com']);
}
}
Both approaches have negligible runtime differences for most web apps. The main cost is wiring complexity and test runtime. If you use a DI container with autowiring, measure cold-start time for CLI or serverless environments. For high-performance hotspots, prefer explicit wiring and avoid runtime reflection in tight loops.
Refactor a controller that uses a locator into one that uses DI. Steps condensed for clarity.
<?php
// Before
class UserController {
private ServiceLocator $locator;
public function __construct(ServiceLocator $locator) {
$this->locator = $locator;
}
public function register(array $data) {
$userRepo = $this->locator->get('user_repository');
$mailer = $this->locator->get('mailer');
// ...
}
}
// After
class UserController {
private UserRepositoryInterface $userRepo;
private MailerInterface $mailer;
public function __construct(UserRepositoryInterface $userRepo, MailerInterface $mailer) {
$this->userRepo = $userRepo;
$this->mailer = $mailer;
}
public function register(array $data) {
// dependencies are explicit and testable
}
}
// Composition root
$container->set(UserRepositoryInterface::class, function() { return new UserRepository(); });
$container->set(MailerInterface::class, function() { return new SmtpMailer(); });
$controller = $container->get(UserController::class);