gacela-project/container

A minimalistic container dependency resolver

Fund package maintenance!
chemaclass.com/sponsor

Installs: 62 247

Dependents: 2

Suggesters: 0

Security: 0

Stars: 9

Watchers: 1

Forks: 0

pkg:composer/gacela-project/container

0.8.0 2025-11-08 22:36 UTC

README

GitHub Build Status Scrutinizer Code Quality Scrutinizer Code Coverage Psalm Type-coverage Status MIT Software License

A minimalistic, PSR-11 compliant dependency injection container with automatic constructor injection and zero configuration.

Features

  • 🚀 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]

Installation

composer require gacela-project/container

Quick Start

Basic Usage

use Gacela\Container\Container;

// Simple auto-wiring
$container = new Container();
$instance = $container->get(YourClass::class);

With Bindings

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 FileLogger

Contextual Bindings

Different 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);

PHP 8 Attributes

Use attributes for declarative dependency configuration:

#[Inject] - Specify Implementation

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);

#[Singleton] - Single Instance

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)

#[Factory] - New Instances

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.

How It Works

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

Example

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);

Advanced Features

Factory Services

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 instance

Extending Services

Wrap 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);
});

Protecting Closures

Prevent closures from being executed:

$closure = fn() => 'Hello World';
$container->set('greeting', $container->protect($closure));

$result = $container->get('greeting'); // Returns the closure itself

Resolving Callables

Automatically inject dependencies into any callable:

$result = $container->resolve(function (LoggerInterface $logger, CacheInterface $cache) {
    $logger->info('Cache cleared');
    return $cache->clear();
});

Service Introspection

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'
]
*/

Performance Optimization

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!

Service Aliasing

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

API Reference

Container Methods

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

Static Methods

// Quick instantiation without container setup
$instance = Container::create(YourClass::class);

Best Practices

1. Use Constructor Injection

// Good
class UserController {
    public function __construct(
        private UserService $userService,
        private LoggerInterface $logger
    ) {}
}

// Avoid setter injection (not supported)

2. Always Use Type Hints

// Good - type hint required
public function __construct(LoggerInterface $logger) {}

// Bad - will throw exception
public function __construct($logger) {}

3. Provide Default Values for Scalars

// 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!

4. Use Bindings for Interfaces

// Always bind interfaces to implementations
$bindings = [
    LoggerInterface::class => FileLogger::class,
    CacheInterface::class => RedisCache::class,
];

5. Warm Up in Production

// In your bootstrap file
$container->warmUp([
    // List frequently used services
    UserService::class,
    AuthService::class,
    Router::class,
]);

Error Handling

The container provides clear, actionable error messages with helpful suggestions:

Missing Type Hint

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

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.

Unresolvable Scalar

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') { ... }

Service Not Found (with suggestions)

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

Requirements

  • PHP >= 8.1
  • PSR-11 Container Interface

Testing

composer test          # Run tests
composer quality       # Run static analysis
composer test-coverage # Generate coverage report

Real-World Example

See how it's used in the Gacela Framework

License

MIT License. See LICENSE file for details.