Cleaning Up Hidden Dependencies: Why Dependency Injection Beats __get
in PHP
In many legacy or homegrown PHP codebases, you’ll often come across classes that rely on magic methods like __get
to lazily load dependencies.
While this approach might seem convenient and even “smart” at first glance, it can create a host of hidden problems — from untestable code to tightly coupled logic that’s hard to refactor.
In this post, we’ll explore why this pattern is problematic and how Dependency Injection (DI) provides a cleaner, more scalable, and testable alternative.
The Magic Behind __get
The __get()
magic method in PHP is called whenever an inaccessible or non-existent property is read from an object. Developers often use this to “automatically” fetch service instances:
$instance = $this->api; // Triggers __get('api')
The __get()
method then returns a singleton or service instance based on the property name. While clever, this introduces invisible dependencies into your class. It looks like $this->api
is a regular property, but it’s actually being constructed or fetched behind the scenes.
The Problem with Magic
Here’s why relying on __get
(or any similar magic mechanism) can become a long-term liability:
1. Hidden Dependencies
Developers reading the class can’t easily tell what services it depends on. IDEs and static analyzers can’t either.
2. Poor Testability
Because dependencies are hardcoded and fetched internally (often using singletons), they’re hard to mock or replace in unit tests.
3. Tightly Coupled Architecture
The class knows too much about how to get its dependencies. It violates the Single Responsibility Principle by managing both its logic and the wiring of its services.
4. Fragile Code
Since the dependency list lives in a switch-case or lookup table inside __get
, adding or changing services often requires digging through that logic — and errors won’t be caught until runtime.
Dependency Injection to the Rescue
Instead of hiding dependencies behind magic methods, Dependency Injection (DI) makes them explicit. This means providing required services to a class through its constructor or setter methods.
Constructor Injection Example
class CheckoutService {
protected $api;
protected $cart;
protected $order;
public function __construct(API $api, Cart $cart, Order $order) {
$this->api = $api;
$this->cart = $cart;
$this->order = $order;
}
public function processCheckout() {
// All dependencies are ready and clear
$this->cart->applyDiscounts();
$this->order->finalize();
}
}
Now the class is:
- Explicit about what it needs
- Easy to test (you can mock
Cart
orOrder
for a unit test) - Easy to refactor and evolve
- Framework-friendly (works naturally with service containers in Laravel, Symfony, etc.)
Testing Becomes Simple
$mockCart = $this->createMock(Cart::class);
$mockOrder = $this->createMock(Order::class);
$service = new CheckoutService($api, $mockCart, $mockOrder);
You no longer need to override magic logic or mock static methods.
Use a Dependency Injection Container
Frameworks like Laravel, Symfony, or libraries like PHP-DI can auto-inject dependencies into your class constructors. This means you still enjoy the convenience of automatic wiring — but with all the benefits of clean architecture.
$container->get(CheckoutService::class); // Automatically injects dependencies
Conclusion
Magic methods like __get
can feel like a shortcut, but they often lead to tangled, opaque code that’s difficult to test and maintain. By adopting Dependency Injection, your classes become simpler, clearer, and far more flexible — a huge win for projects of any size.
If you’re refactoring a legacy application or starting something new, take the time to invest in DI. It’s one of the cleanest, most future-proof design decisions you can make in PHP.
Leave a Reply