Service Container

The Zephyr Service Container is a powerful dependency injection container that manages class dependencies and performs dependency injection automatically.

Introduction

The container is PSR-11 compliant and provides features like automatic dependency resolution (autowiring), service tagging, lazy loading, and more.

Basic Usage

Getting the Container

use ZephyrPHP\Container\Container;

// Get singleton instance
$container = Container::getInstance();

// Or use helper function
$container = container();

Resolving Services

// PSR-11 compliant
$userService = $container->get(UserService::class);

// With parameters
$report = $container->make(ReportGenerator::class, ['format' => 'pdf']);

// Using helper functions
$service = resolve(UserService::class);
$app = app(UserService::class);

Method Injection

$result = $container->call([UserController::class, 'index']);
$result = $container->call('UserController@show', ['id' => 123]);

Binding Types

bind() - Transient

Creates a new instance each time (unless cached by the underlying container).

$container->bind(LoggerInterface::class, FileLogger::class);

// With closure
$container->bind(LoggerInterface::class, function ($container) {
    return new FileLogger('/var/log/app.log');
});

singleton() - Shared Instance

Returns the same instance every time.

$container->singleton(DatabaseConnection::class, function ($container) {
    return new DatabaseConnection(
        host: env('DB_HOST'),
        database: env('DB_DATABASE')
    );
});

// Using helper
singleton(CacheInterface::class, RedisCache::class);

factory() - Always New

Always creates a fresh instance, never cached.

$container->factory(RequestId::class, function () {
    return new RequestId(uniqid('req_', true));
});

scoped() - Request Singleton

Singleton within the current scope/request, cleared at scope end.

$container->scoped(AuthenticatedUser::class, function ($container) {
    return User::fromSession();
});

// Clear at end of request
$container->clearScopedInstances();

instance() - Existing Object

Register an existing instance.

$config = new Config(['debug' => true]);
$container->instance(Config::class, $config);

Conditional Binding

// Only bind if not already bound
$container->bindIf(LoggerInterface::class, FileLogger::class);
$container->singletonIf(CacheInterface::class, ArrayCache::class);

Aliases

Create shortcuts to services.

$container->singleton(ZephyrPHP\Log\LogManager::class, LogManager::class);
$container->alias('logger', ZephyrPHP\Log\LogManager::class);
$container->alias('log', 'logger'); // Chained aliases work

// Resolve the alias chain
$source = $container->getAlias('log'); // Returns ZephyrPHP\Log\LogManager::class

// Check if alias
$container->isAlias('logger'); // true

Tagging

Group related services together.

$container->singleton(EmailNotifier::class);
$container->singleton(SmsNotifier::class);
$container->singleton(PushNotifier::class);

$container->tag([
    EmailNotifier::class,
    SmsNotifier::class,
    PushNotifier::class,
], 'notifiers');

// Get all tagged services (resolved)
$notifiers = $container->tagged('notifiers');

// Get tag members without resolving
$members = $container->getTagged('notifiers');

Contextual Bindings

Different implementations based on the consuming class.

$container->when(PhotoController::class)
          ->needs(FileSystem::class)
          ->give(LocalFileSystem::class);

$container->when(VideoController::class)
          ->needs(FileSystem::class)
          ->give(S3FileSystem::class);

Environment Bindings

Different bindings per environment.

// Set the environment
$container->setEnvironment(env('APP_ENV', 'production'));

// Define environment-specific bindings
$container->whenEnvironment('production')
          ->singleton(CacheInterface::class, RedisCache::class);

$container->whenEnvironment('testing')
          ->singleton(CacheInterface::class, ArrayCache::class);

$container->whenEnvironment('development')
          ->singleton(CacheInterface::class, FileCache::class);

Lazy Loading

Delay instantiation until first use.

$container->lazy(ExpensiveService::class, function ($container) {
    // This only runs when you actually use the service
    return new ExpensiveService(
        $container->get(DatabaseConnection::class)
    );
});

// Get the proxy (instant - no instantiation yet)
$service = $container->get(ExpensiveService::class);

// Now it instantiates
$service->doSomething();

Decorators

Wrap services with additional functionality.

$container->singleton(LoggerInterface::class, FileLogger::class);

// Add decorators with priority (higher = outer)
$container->decorate(LoggerInterface::class, CachingLogger::class, priority: 10);
$container->decorate(LoggerInterface::class, AsyncLogger::class, priority: 20);

// Result: AsyncLogger -> CachingLogger -> FileLogger

// Using closures
$container->decorate(LoggerInterface::class, function ($logger, $container) {
    return new MetricsLogger($logger, $container->get(Metrics::class));
});

Resolution Middleware

Add middleware that runs during service resolution.

$container->addResolutionMiddleware(function ($abstract, $next, $container) {
    $start = microtime(true);
    $result = $next();
    $time = microtime(true) - $start;

    if ($time > 0.1) {
        Log::warning("Slow resolution: {$abstract} took {$time}s");
    }

    return $result;
});

Service Locator

Create a container with limited service access.

$locator = $container->createServiceLocator([
    LoggerInterface::class,
    CacheInterface::class,
    EventDispatcher::class,
]);

// Use in controller
class UserController
{
    public function __construct(private ServiceLocator $services) {}

    public function index()
    {
        $logger = $this->services->get(LoggerInterface::class); // Works
        $db = $this->services->get(DatabaseInterface::class);   // Throws - not in locator
    }
}

Resolving Events

Hooks that fire during resolution.

// Before resolving
$container->beforeResolving(ExpensiveService::class, function ($abstract, $params, $container) {
    Log::info("About to resolve: {$abstract}");
});

// After resolving
$container->afterResolving(LoggerInterface::class, function ($logger, $container) {
    $logger->setChannel('app');
});

// Global hooks (all services)
$container->beforeResolving(function ($abstract, $params, $container) {
    // Runs for ALL resolutions
});

$container->afterResolving(function ($object, $container) {
    // Runs for ALL resolutions
});

Service Providers

Basic Provider

app/Providers/AppServiceProvider.php
<?php

namespace App\Providers;

use ZephyrPHP\Container\ServiceProvider;
use ZephyrPHP\Container\Container;

class AppServiceProvider extends ServiceProvider
{
    public function register(Container $container): void
    {
        $container->singleton(CacheInterface::class, RedisCache::class);
    }

    public function boot(Container $container): void
    {
        // After all providers registered
    }
}

Deferred Provider

Only loaded when its services are needed.

app/Providers/MailServiceProvider.php
<?php

namespace App\Providers;

use ZephyrPHP\Container\DeferredServiceProvider;
use ZephyrPHP\Container\Container;

class MailServiceProvider extends DeferredServiceProvider
{
    public function provides(): array
    {
        return [MailerInterface::class, 'mailer'];
    }

    public function register(Container $container): void
    {
        $container->singleton(MailerInterface::class, SmtpMailer::class);
        $container->alias('mailer', MailerInterface::class);
    }
}

Validation & Debugging

Validate Bindings

$errors = $container->validate();

if (!empty($errors)) {
    foreach ($errors as $abstract => $error) {
        echo "Error in {$abstract}: {$error}\n";
    }
}

Get Binding Definition

$info = $container->getDefinition(LoggerInterface::class);
// Returns:
// [
//     'abstract' => 'LoggerInterface',
//     'type' => 'singleton',
//     'concrete' => FileLogger::class,
//     'resolved' => false,
//     'aliases' => ['logger', 'log'],
//     'tags' => ['logging'],
//     'decorators' => [...],
// ]

Dump Container State

$state = $container->dump();
// Returns all bindings, singletons, factories, aliases, tags, etc.

Production Optimization

Compilation

if (env('APP_ENV') === 'production') {
    $container->enableCompilation(storage_path('cache/container'));
}

// Check status
if ($container->isCompiled()) {
    echo "Container is compiled";
}

Helper Functions

container();                    // Get container
app();                          // Get application
app(UserService::class);        // Resolve service
resolve(UserService::class);    // Resolve service
singleton($abstract, $concrete); // Register singleton
bind($abstract, $concrete);     // Register binding
tagged('notifiers');            // Get tagged services

API Reference

Binding Methods

Method Description
bind($abstract, $concrete)Transient binding
singleton($abstract, $concrete)Shared singleton
factory($abstract, $concrete)Always new instance
scoped($abstract, $concrete)Request-scoped singleton
lazy($abstract, $concrete)Delayed instantiation
instance($abstract, $instance)Register existing object

Resolution Methods

Method Description
get($id)PSR-11 resolve
has($id)PSR-11 check
make($abstract, $params)Resolve with parameters
call($callable, $params)Call with DI

Organization Methods

Method Description
alias($alias, $abstract)Create alias
tag($abstracts, $tag)Tag services
tagged($tag)Get tagged services
decorate($abstract, $decorator)Add decorator
Related

Learn about Helper Functions for container shortcuts, Middleware for request filtering, and Controllers which use the container for dependency injection.