A minimalistic, PSR-11 compliant dependency injection container with automatic constructor injection and zero configuration.
- 🚀 Zero Configuration: Automatic constructor injection without verbose setup
- 🔄 Circular Dependency Detection: Clear error messages when dependencies form a loop
- 📦 PSR-11 Compliant: Standard container interface for interoperability
- ⚡ Performance Optimized: Built-in caching and warmup capabilities
- 🔍 Introspection: Debug and inspect container state easily
- 🎯 Type Safe: Requires type hints for reliable dependency resolution
- 🏷️ PHP 8 Attributes: Declarative configuration with #[Inject], #[Singleton], and #[Factory]
composer require gacela-project/containeruse Gacela\Container\Container;
// Simple auto-wiring
$container = new Container();
$instance = $container->get(YourClass::class);Map interfaces to concrete implementations:
$bindings = [
LoggerInterface::class => FileLogger::class,
CacheInterface::class => new RedisCache('localhost'),
ConfigInterface::class => fn() => loadConfig(),
];
$container = new Container($bindings);
$logger = $container->get(LoggerInterface::class); // Returns FileLoggerDifferent implementations based on which class needs them:
// UserController gets FileLogger, AdminController gets DatabaseLogger
$container->when(UserController::class)
->needs(LoggerInterface::class)
->give(FileLogger::class);
$container->when(AdminController::class)
->needs(LoggerInterface::class)
->give(DatabaseLogger::class);
// Multiple classes can share the same contextual binding
$container->when([ServiceA::class, ServiceB::class])
->needs(CacheInterface::class)
->give(RedisCache::class);Use attributes for declarative dependency configuration:
Override type hints to inject specific implementations:
use Gacela\Container\Attribute\Inject;
class NotificationService {
public function __construct(
#[Inject(EmailLogger::class)]
private LoggerInterface $logger,
) {}
}
// EmailLogger will be injected even if LoggerInterface is bound to FileLogger
$service = $container->get(NotificationService::class);Mark a class to be instantiated only once:
use Gacela\Container\Attribute\Singleton;
#[Singleton]
class DatabaseConnection {
public function __construct(private string $dsn) {}
}
$conn1 = $container->get(DatabaseConnection::class);
$conn2 = $container->get(DatabaseConnection::class);
// $conn1 === $conn2 (same instance)Always create fresh instances:
use Gacela\Container\Attribute\Factory;
#[Factory]
class RequestContext {
public function __construct(private LoggerInterface $logger) {}
}
$ctx1 = $container->get(RequestContext::class);
$ctx2 = $container->get(RequestContext::class);
// $ctx1 !== $ctx2 (different instances)Performance Note: Attribute checks are cached internally, so repeated instantiations of the same class avoid expensive reflection operations, providing 15-20% performance improvement.
The container automatically resolves dependencies based on type hints:
- Primitive types: Uses default values (must be provided)
- Classes: Instantiates and resolves dependencies recursively
- Interfaces: Resolves using bindings defined in the container
class UserService {
public function __construct(
private UserRepository $repository,
private LoggerInterface $logger,
) {}
}
class UserRepository {
public function __construct(private PDO $pdo) {}
}
// Setup
$bindings = [
LoggerInterface::class => FileLogger::class,
PDO::class => new PDO('mysql:host=localhost;dbname=app', 'user', 'pass'),
];
$container = new Container($bindings);
// Auto-resolves UserService -> UserRepository -> PDO
$service = $container->get(UserService::class);Create new instances on every call:
$factory = $container->factory(fn() => new TempFile());
$container->set('temp_file', $factory);
$file1 = $container->get('temp_file'); // New instance
$file2 = $container->get('temp_file'); // Different instanceWrap or modify services (even before they're created):
$container->set('logger', fn() => new FileLogger('/var/log/app.log'));
$container->extend('logger', function ($logger, $container) {
return new LoggerDecorator($logger);
});Prevent closures from being executed:
$closure = fn() => 'Hello World';
$container->set('greeting', $container->protect($closure));
$result = $container->get('greeting'); // Returns the closure itselfAutomatically inject dependencies into any callable:
$result = $container->resolve(function (LoggerInterface $logger, CacheInterface $cache) {
$logger->info('Cache cleared');
return $cache->clear();
});Debug and inspect container state:
// Get all registered service IDs
$services = $container->getRegisteredServices();
// Check if service is a factory
if ($container->isFactory('temp_file')) {
// Returns new instance each time
}
// Check if service is frozen (accessed)
if ($container->isFrozen('logger')) {
// Cannot be modified anymore
}
// Get all bindings
$bindings = $container->getBindings();
// Get comprehensive statistics
$stats = $container->getStats();
/*
[
'registered_services' => 42,
'frozen_services' => 15,
'factory_services' => 3,
'bindings' => 8,
'cached_dependencies' => 25,
'memory_usage' => '2.34 MB'
]
*/Pre-resolve dependencies for faster runtime:
// During application bootstrap
$container->warmUp([
UserService::class,
OrderService::class,
PaymentProcessor::class,
]);
// Later requests benefit from cached dependency resolution
$service = $container->get(UserService::class); // Faster!Create multiple names for the same service:
// Create an alias
$container->alias('db', PDO::class);
// Access via alias or original name
$db1 = $container->get('db'); // Same instance
$db2 = $container->get(PDO::class); // Same instance
// Alias resolution is cached for optimal performance| Method | Description |
|---|---|
get(string $id): mixed |
Retrieve or create a service |
has(string $id): bool |
Check if service exists |
set(string $id, mixed $instance): void |
Register a service |
remove(string $id): void |
Remove a service |
resolve(callable $callable): mixed |
Execute callable with dependency injection |
factory(Closure $instance): Closure |
Mark service as factory (new instance each time) |
extend(string $id, Closure $instance): Closure |
Wrap/modify a service |
protect(Closure $instance): Closure |
Prevent closure execution |
getRegisteredServices(): array |
Get all service IDs |
isFactory(string $id): bool |
Check if service is a factory |
isFrozen(string $id): bool |
Check if service is frozen |
getBindings(): array |
Get all bindings |
warmUp(array $classNames): void |
Pre-resolve dependencies |
alias(string $alias, string $id): void |
Create an alias for a service (with caching) |
getStats(): array |
Get container statistics for debugging and performance monitoring |
when(string|array $concrete): ContextualBindingBuilder |
Define contextual bindings for specific classes |
// Quick instantiation without container setup
$instance = Container::create(YourClass::class);// Good
class UserController {
public function __construct(
private UserService $userService,
private LoggerInterface $logger
) {}
}
// Avoid setter injection (not supported)// Good - type hint required
public function __construct(LoggerInterface $logger) {}
// Bad - will throw exception
public function __construct($logger) {}// Good
public function __construct(
UserRepository $repo,
int $maxRetries = 3,
string $env = 'production'
) {}
// Bad - scalars without defaults cannot be resolved
public function __construct(string $apiKey) {} // Exception!// Always bind interfaces to implementations
$bindings = [
LoggerInterface::class => FileLogger::class,
CacheInterface::class => RedisCache::class,
];// In your bootstrap file
$container->warmUp([
// List frequently used services
UserService::class,
AuthService::class,
Router::class,
]);The container provides clear, actionable error messages with helpful suggestions:
No type hint found for parameter '$logger'.
Type hints are required for dependency injection to work properly.
Add a type hint to the parameter, for example:
public function __construct(YourClass $logger) { ... }
Circular dependency detected: ClassA -> ClassB -> ClassC -> ClassA
This happens when classes depend on each other in a loop.
Consider using setter injection or the factory pattern to break the cycle.
Unable to resolve parameter of type 'string' in 'UserService'.
Scalar types (string, int, float, bool, array) cannot be auto-resolved.
Provide a default value for the parameter:
public function __construct(string $param = 'default') { ... }
No concrete class was found that implements:
"App\LogerInterface"
Did you forget to bind this interface to a concrete class?
Did you mean one of these?
- App\LoggerInterface
- App\Service\LoggerInterface
You might find some help here: https://gacela-project.com/docs/bootstrap/#bindings
- PHP >= 8.1
- PSR-11 Container Interface
composer test # Run tests
composer quality # Run static analysis
composer test-coverage # Generate coverage reportSee how it's used in the Gacela Framework
MIT License. See LICENSE file for details.