A simple dependency injection container for PHP 7.4+.
This is a lightweight dependency injection (DI) container. It provides automatic dependency resolution, interface binding, and singleton management.
- Auto-wiring: Automatically resolves constructor dependencies
- Interface Binding: Bind interfaces to concrete implementations
- Singleton Support: Manage singleton instances with automatic caching and clearing
- Zero Configuration: Works out of the box with no setup
- Type-Safe: Full type hints and generics support
- Thoroughly Tested: Comprehensive unit tests across PHP 7.4-8.4
Install via Composer:
composer require agussuroyo/container- PHP 7.4 or higher
- No additional dependencies
<?php
use AgusSuroyo\Container\Container;
// Create a container instance
$container = new Container();
// Resolve a simple class
$instance = $container->get(MyClass::class);
// The container automatically resolves dependencies
$service = $container->get(MyService::class);The container can automatically resolve classes with no dependencies or with resolvable dependencies:
class SimpleService
{
public function doSomething(): void
{
echo "Doing something!";
}
}
$service = $container->get(SimpleService::class);
$service->doSomething();The container automatically resolves constructor dependencies:
class Database
{
public function query(string $sql): array
{
// Execute query
return [];
}
}
class UserRepository
{
public function __construct(
private Database $database
) {}
public function findAll(): array
{
return $this->database->query('SELECT * FROM users');
}
}
// Container automatically injects Database into UserRepository
$repository = $container->get(UserRepository::class);Bind interfaces to concrete implementations:
interface LoggerInterface
{
public function log(string $message): void;
}
class FileLogger implements LoggerInterface
{
public function log(string $message): void
{
file_put_contents('app.log', $message . PHP_EOL, FILE_APPEND);
}
}
// Bind interface to implementation
$container->bind(LoggerInterface::class, FileLogger::class);
// Now you can resolve the interface
$logger = $container->get(LoggerInterface::class);
$logger->log('Application started');Use closures for custom instantiation logic:
$container->bind(Database::class, function () {
return new Database(
host: 'localhost',
username: 'root',
password: 'secret'
);
});
$db = $container->get(Database::class);The get() method automatically returns the same instance on subsequent calls:
$instance1 = $container->get(MyService::class);
$instance2 = $container->get(MyService::class);
// Both variables reference the same instance
assert($instance1 === $instance2);You can also explicitly declare singletons:
$container->singleton(Cache::class, RedisCache::class);Use make() to create a new instance each time (bypasses singleton cache):
$instance1 = $container->make(MyClass::class);
$instance2 = $container->make(MyClass::class);
// Different instances
assert($instance1 !== $instance2);Check if a class or interface is bound:
if ($container->bound(LoggerInterface::class)) {
$logger = $container->get(LoggerInterface::class);
}Clear a specific singleton instance or all singleton instances:
// Clear a specific singleton
$instance1 = $container->get(MyService::class);
$container->clearInstance(MyService::class);
$instance2 = $container->get(MyService::class);
// $instance1 !== $instance2 (new instance created)
// Clear all singletons
$container->clearInstance();This is useful for testing scenarios or when you need to reset the container state without creating a new container instance.
The container respects default parameter values:
class ConfigService
{
public function __construct(
private string $environment = 'production'
) {}
public function getEnv(): string
{
return $this->environment;
}
}
// Uses the default value 'production'
$config = $container->get(ConfigService::class);
echo $config->getEnv(); // Outputs: productionThe container recursively resolves nested dependencies:
class Logger { }
class Database
{
public function __construct(private Logger $logger) {}
}
class UserRepository
{
public function __construct(private Database $database) {}
}
class UserService
{
public function __construct(private UserRepository $repository) {}
}
// Automatically resolves: UserService -> UserRepository -> Database -> Logger
$service = $container->get(UserService::class);// Bind with factory pattern
$container->bind(Connection::class, function () use ($config) {
return match($config['driver']) {
'mysql' => new MySQLConnection($config['mysql']),
'pgsql' => new PostgresConnection($config['pgsql']),
default => throw new Exception('Unknown driver')
};
});
// Bind multiple implementations
$container->bind('logger.file', FileLogger::class);
$container->bind('logger.email', EmailLogger::class);Bind an abstract type (interface or class name) to a concrete implementation.
$abstract: Interface or class name$concrete: Class name (string) or factory closure
Bind a singleton (same as bind(), included for semantic clarity).
Resolve and return an instance. Subsequent calls return the same instance (singleton behavior).
- Returns: Instance of the requested type
- Throws:
InvalidArgumentExceptionif class doesn't exist - Throws:
RuntimeExceptionif class is not instantiable
Create a new instance each time, bypassing the singleton cache.
- Returns: New instance of the requested type
- Throws:
InvalidArgumentExceptionif class doesn't exist - Throws:
RuntimeExceptionif class is not instantiable
Check if an abstract type has been bound or resolved.
- Returns:
trueif bound,falseotherwise
Clear a specific singleton instance or all singleton instances.
$abstract: The abstract to clear, ornullto clear all instances- Note: This does not remove bindings, only clears cached instances
The container throws clear exceptions for common issues:
Thrown when a class doesn't exist:
try {
$container->get('NonExistentClass');
} catch (InvalidArgumentException $e) {
echo $e->getMessage(); // "Class NonExistentClass not found"
}Thrown when a class cannot be instantiated:
// Abstract class
try {
$container->get(AbstractLogger::class);
} catch (RuntimeException $e) {
echo $e->getMessage(); // "Class AbstractLogger is not instantiable"
}
// Unresolvable parameter
class NeedsString
{
public function __construct(string $name) {}
}
try {
$container->get(NeedsString::class);
} catch (RuntimeException $e) {
echo $e->getMessage(); // "Cannot resolve parameter name"
}✅ Classes with no constructor
✅ Classes with constructor dependencies (other classes)
✅ Classes with interface dependencies (if bound)
✅ Classes with optional parameters (default values)
✅ Nested dependencies (recursive resolution)
❌ Abstract classes
❌ Interfaces without bindings
❌ Primitive types without default values (string, int, bool, etc.)
❌ Union types
❌ Intersection types
❌ Variadic parameters
❌ Classes that don't exist
For unresolvable dependencies, use bindings with closures:
// Problem: Cannot resolve primitive type
class EmailService
{
public function __construct(
private string $apiKey,
private string $fromEmail
) {}
}
// Solution: Use closure binding
$container->bind(EmailService::class, function () {
return new EmailService(
apiKey: $_ENV['EMAIL_API_KEY'],
fromEmail: 'noreply@example.com'
);
});The project includes comprehensive tests:
# Run all tests
composer test
# Run PHPStan static analysis
composer phpstan
# Run both tests and static analysis
composer check- Unit Tests: Core functionality, edge cases, error handling
- Feature Tests: Integration and feature tests
- Static Analysis: PHPStan level max with strict rules
- PHP 7.4+
- Composer
- PHPUnit 9.5+ (10+ for PHP 8.1+)
- PHPStan 1.10+
# Clone the repository
git clone https://github.com/agussuroyo/container.git
cd container
# Install dependencies
composer install
# Run tests
composer testThe project uses:
- PSR-4 autoloading
- Strict types declaration
- PHPStan level max
- PHPStan strict rules
- PHPUnit for testing
- CI/CD testing across PHP 7.4, 8.0, 8.1, 8.2, 8.3, and 8.4
- Simplicity: Minimal API surface, easy to understand
- Type Safety: Full PHP type hints and strict types
- Single Responsibility: Each method has one clear purpose
- Fail Fast: Clear exceptions for invalid operations
Container
├── $instances // Singleton cache (array<string, object>)
├── $bindings // Interface bindings (array<string, callable>)
├── get() // Resolve with caching
├── make() // Create new instance
├── bind() // Register binding
├── singleton() // Register singleton
├── bound() // Check if bound
├── clearInstance() // Clear singleton cache
└── resolveDependencies() // Recursive resolutionContributions are welcome! Please follow these guidelines:
- Fork the repository
- Create a feature branch
- Write tests for new functionality
- Ensure all tests pass (
composer check) - Submit a pull request
This container focuses on simplicity. It provides only essential features without unnecessary complexity.
Yes! The container fully supports PHP 8.0+ constructor property promotion.
The container doesn't handle circular dependencies. Design your classes to avoid circular references, or use setter injection as a workaround.
Yes! Use clearInstance() to clear a specific singleton or clearInstance(null) to clear all singletons. Bindings are preserved, so resolved instances will be recreated on next get() call.
PHP doesn't have true multi-threading, but each request gets its own container instance, so there are no concurrency issues in typical PHP applications.
For issues, questions, or contributions, please visit the GitHub repository.