PHP - Dependency Injection Containers in Standalone PHP
Introduction
As PHP applications grow larger, managing object creation and dependencies becomes increasingly difficult. A class often depends on one or more other classes to perform its tasks. If each class creates its own dependencies, the application becomes tightly coupled, making it difficult to maintain, test, and extend.
A Dependency Injection (DI) Container solves this problem by automatically creating objects and injecting their required dependencies. Although dependency injection is commonly associated with frameworks like Laravel and Symfony, it can also be implemented in standalone PHP applications without relying on any framework.
A standalone Dependency Injection Container helps developers build modular, reusable, and maintainable applications while reducing repetitive object creation code.
What is Dependency Injection?
Dependency Injection is a design pattern in which an object's required dependencies are supplied from outside rather than being created inside the object itself.
Instead of a class creating another class using the new keyword, the required object is passed into it through a constructor, setter method, or interface.
Without Dependency Injection
class Database
{
public function connect()
{
return "Connected";
}
}
class UserService
{
private $database;
public function __construct()
{
$this->database = new Database();
}
public function getUsers()
{
return $this->database->connect();
}
}
$user = new UserService();
echo $user->getUsers();
In this example:
-
UserService creates its own Database object.
-
Database cannot easily be replaced.
-
Testing becomes difficult.
-
The classes become tightly coupled.
Using Dependency Injection
class Database
{
public function connect()
{
return "Connected";
}
}
class UserService
{
private $database;
public function __construct(Database $database)
{
$this->database = $database;
}
public function getUsers()
{
return $this->database->connect();
}
}
$db = new Database();
$user = new UserService($db);
echo $user->getUsers();
Here:
-
Database is created outside UserService.
-
UserService only receives the dependency.
-
Different database implementations can easily be substituted.
-
Testing becomes much easier.
What is a Dependency Injection Container?
A Dependency Injection Container is a component that automatically creates objects and resolves their dependencies.
Instead of manually writing code to create every object, the container determines what each class requires and supplies the correct dependencies.
Without a container:
$db = new Database();
$mail = new MailService();
$user = new UserService($db, $mail);
$order = new OrderService($user);
With a container:
$order = $container->make(OrderService::class);
The container automatically creates every required object.
Responsibilities of a DI Container
A Dependency Injection Container typically performs the following tasks:
-
Creates objects automatically.
-
Resolves constructor dependencies.
-
Stores reusable objects.
-
Manages object lifecycles.
-
Supports configuration.
-
Reduces manual instantiation.
-
Improves code organization.
Building a Simple Dependency Injection Container
A basic DI container can be created using PHP's Reflection API.
class Container
{
protected $instances = [];
public function set($name, $object)
{
$this->instances[$name] = $object;
}
public function get($name)
{
return $this->instances[$name];
}
}
Using the container:
$db = new Database();
$container = new Container();
$container->set(Database::class, $db);
$database = $container->get(Database::class);
echo $database->connect();
This simple version only stores existing objects.
Automatically Creating Objects
A more advanced container automatically creates requested classes.
class Container
{
public function make($class)
{
return new $class();
}
}
Usage:
$container = new Container();
$db = $container->make(Database::class);
echo $db->connect();
Although basic, this demonstrates automatic object creation.
Resolving Constructor Dependencies
Suppose the following classes exist.
class Database
{
}
class MailService
{
}
class UserService
{
public function __construct(Database $db, MailService $mail)
{
}
}
A Dependency Injection Container examines the constructor and determines that UserService requires both Database and MailService. It then creates those objects first and passes them to the constructor automatically.
The dependency chain looks like this:
UserService
|
|-- Database
|
|-- MailService
This process is known as dependency resolution.
Constructor Injection
Constructor Injection is the most common form of dependency injection.
class Logger
{
}
class UserService
{
protected $logger;
public function __construct(Logger $logger)
{
$this->logger = $logger;
}
}
Advantages include:
-
Mandatory dependencies are guaranteed.
-
Objects remain immutable after creation.
-
Dependencies are clearly visible.
-
Easier unit testing.
Setter Injection
Dependencies can also be supplied using setter methods.
class UserService
{
protected $logger;
public function setLogger(Logger $logger)
{
$this->logger = $logger;
}
}
This method is useful for optional dependencies but may leave an object incomplete if the setter is not called.
Interface-Based Injection
Instead of depending on concrete classes, applications should depend on interfaces.
interface PaymentGateway
{
public function pay();
}
Implementation:
class RazorpayGateway implements PaymentGateway
{
public function pay()
{
return "Payment Successful";
}
}
Using the interface:
class OrderService
{
protected $gateway;
public function __construct(PaymentGateway $gateway)
{
$this->gateway = $gateway;
}
}
The container can inject any class that implements the PaymentGateway interface without changing OrderService.
Singleton Services
Some services should only have one instance throughout the application.
Examples include:
-
Database connection
-
Configuration manager
-
Logger
-
Cache manager
Example:
class Database
{
}
The container creates the object once and reuses the same instance whenever requested.
Benefits include:
-
Reduced memory usage.
-
Shared configuration.
-
Improved performance.
-
Consistent application state.
Transient Services
Transient services create a new object each time they are requested.
Example:
$user1 = $container->make(UserService::class);
$user2 = $container->make(UserService::class);
Here, user1 and user2 are different objects.
Transient services are suitable for:
-
Temporary data processors
-
Report generators
-
Validation services
-
Utility classes
Using PHP Reflection
Reflection enables the container to inspect classes at runtime.
It can determine:
-
Constructor parameters
-
Parameter types
-
Class methods
-
Properties
-
Interfaces
-
Parent classes
Example:
$reflection = new ReflectionClass(UserService::class);
$constructor = $reflection->getConstructor();
The container uses Reflection to identify required dependencies and create them automatically.
Advantages of Dependency Injection Containers
-
Reduces repetitive object creation.
-
Makes code more modular and reusable.
-
Simplifies testing by allowing mock dependencies.
-
Encourages loose coupling between classes.
-
Improves readability and maintainability.
-
Supports scalable application architecture.
-
Centralizes dependency management.
-
Facilitates replacing implementations without modifying dependent classes.
Limitations
-
Initial implementation can be complex.
-
Reflection introduces a small runtime overhead.
-
Incorrect dependency configuration may cause resolution errors.
-
Large projects require careful organization of service registrations.
Best Practices
-
Prefer constructor injection for mandatory dependencies.
-
Depend on interfaces instead of concrete classes.
-
Register singleton services only when a single shared instance is appropriate.
-
Keep service classes focused on a single responsibility.
-
Avoid embedding business logic within the container itself.
-
Use meaningful service names and namespaces.
-
Minimize global state and static dependencies.
-
Organize service registrations in a dedicated configuration file or bootstrap script.
-
Write unit tests using mock implementations to verify class behavior independently.
-
Document service dependencies to improve maintainability.
Conclusion
Dependency Injection Containers are an essential tool for building well-structured PHP applications. Even in standalone PHP projects without a framework, a DI container simplifies object creation, automatically resolves dependencies, and promotes loose coupling between components. By using constructor injection, interfaces, singleton and transient services, and PHP Reflection, developers can create applications that are easier to maintain, extend, and test. As projects grow in complexity, adopting a Dependency Injection Container becomes an effective way to manage dependencies efficiently and support scalable software design.