Service Container
The WPZylos Container is a lightweight PSR-11 compliant dependency injection container designed for multi-plugin isolation.
Core Concepts
Dependency Injection
Instead of creating dependencies inside classes, inject them:
// Bad: Tight coupling
class UserController
{
public function show(): void
{
$db = new Database(); // Creates its own dependency
$user = $db->find(User::class, 1);
}
}
// Good: Dependency injection
class UserController
{
public function __construct(private Database $db) // Injected
{
}
public function show(): void
{
$user = $this->db->find(User::class, 1);
}
}
The Container's Role
The container:
- Knows how to create services
- Injects dependencies automatically
- Manages service lifecycles (singleton vs factory)
Basic Usage
Creating a Container
use WPZylos\Framework\Container\Container;
$container = new Container();
Binding Services
// Simple binding (new instance each time)
$container->bind(LoggerInterface::class, fn() => new FileLogger('/path/to/log'));
// Singleton (same instance every time)
$container->singleton(Database::class, fn() => new Database($config));
// Bind interface to implementation
$container->bind(CacheInterface::class, RedisCache::class);
Resolving Services
// Get a service
$logger = $container->get(LoggerInterface::class);
// Check if service exists
if ($container->has(CacheInterface::class)) {
$cache = $container->get(CacheInterface::class);
}
Auto-Wiring
The container automatically resolves dependencies using type hints:
class OrderService
{
public function __construct(
private Database $db,
private LoggerInterface $logger,
private CacheInterface $cache
) {
}
}
// Container automatically injects Database, Logger, and Cache
$service = $container->get(OrderService::class);
How Auto-Wiring Works
- Container inspects constructor parameters
- For each parameter, it resolves the type from the container
- Creates instance with resolved dependencies
// This happens automatically:
$db = $container->get(Database::class);
$logger = $container->get(LoggerInterface::class);
$cache = $container->get(CacheInterface::class);
$service = new OrderService($db, $logger, $cache);
Binding Patterns
Closure Binding
$container->bind(MailerInterface::class, function (Container $c) {
return new SmtpMailer(
$c->get(ConfigRepository::class)->get('mail.host'),
$c->get(ConfigRepository::class)->get('mail.port')
);
});
Class Binding
// Interface to class
$container->bind(CacheInterface::class, FileCache::class);
// Container will auto-wire FileCache's dependencies
Instance Binding
// Bind an existing instance
$container->instance(PluginContext::class, $context);
Contextual Binding
// Different implementations for different consumers
$container->when(AdminController::class)
->needs(LoggerInterface::class)
->give(AdminLogger::class);
$container->when(ApiController::class)
->needs(LoggerInterface::class)
->give(ApiLogger::class);
Singleton vs Transient
Singleton
Same instance returned every time:
$container->singleton(Database::class, fn() => new Database());
$db1 = $container->get(Database::class);
$db2 = $container->get(Database::class);
$db1 === $db2; // true
Use for:
- Database connections
- Configuration
- Caches
- Stateless services
Transient (bind)
New instance every time:
$container->bind(Request::class, fn() => Request::capture());
$req1 = $container->get(Request::class);
$req2 = $container->get(Request::class);
$req1 === $req2; // false
Use for:
- Request/Response objects
- Form builders
- Stateful objects
PSR-11 Compliance
The container implements Psr\Container\ContainerInterface:
interface ContainerInterface
{
public function get(string $id): mixed;
public function has(string $id): bool;
}
This allows interoperability with PSR-11 compatible packages.
Service Providers
Service providers organize bindings:
<?php
namespace Vendor\MyPlugin\Providers;
use WPZylos\Framework\Core\ServiceProvider;
class DatabaseServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->container->singleton(Database::class, function () {
return new Database([
'prefix' => $this->context->prefix(),
]);
});
}
public function boot(): void
{
// Called after all providers are registered
}
}
Why We Did It This Way
Isolated Containers (Not Singletons)
Problem: Traditional PHP frameworks use a singleton container accessible globally. When two plugins use the same framework, they share services.
// Bad: Singleton pattern (shared state)
$container = Container::getInstance();
Solution: Each plugin creates its own Container instance.
// Good: Isolated containers
$containerA = new Container(); // Plugin A's services
$containerB = new Container(); // Plugin B's services
No Service Locator Pattern
Problem: Accessing the container from anywhere couples code to the container and hides dependencies.
// Bad: Service locator (hidden dependencies)
class UserService
{
public function getUser(int $id): User
{
$db = Container::get(Database::class); // Hidden dependency
return $db->find(User::class, $id);
}
}
Solution: Always inject dependencies explicitly.
// Good: Explicit dependencies
class UserService
{
public function __construct(private Database $db) // Visible
{
}
public function getUser(int $id): User
{
return $this->db->find(User::class, $id);
}
}
Lightweight Implementation
Problem: Full-featured containers like Symfony's are powerful but heavy for WordPress plugins.
Solution: WPZylos Container focuses on essential features:
- Binding and resolving
- Auto-wiring
- Singletons
- PSR-11 compliance
No compiled containers, no YAML configuration, no annotation parsing.
Advanced Usage
Method Injection
$result = $container->call([$controller, 'show'], ['id' => 123]);
// Auto-wires method parameters, merges with provided arguments
Tagged Services
$container->bind(WidgetA::class)->tag('widgets');
$container->bind(WidgetB::class)->tag('widgets');
$widgets = $container->tagged('widgets');
// Returns all services tagged with 'widgets'
Aliases
$container->alias(Database::class, 'db');
$db = $container->get('db'); // Same as get(Database::class)
Testing
Mock the container for unit tests:
public function testUserServiceFindsUser(): void
{
$mockDb = $this->createMock(Database::class);
$mockDb->method('find')->willReturn(new User(['id' => 1]));
$container = new Container();
$container->instance(Database::class, $mockDb);
$service = $container->get(UserService::class);
$user = $service->getUser(1);
$this->assertSame(1, $user->id);
}
Next Steps
- Service Providers — Organizing container bindings
- Plugin Context — How context integrates with container