Dependency Injection and Services
Table of Contents
- Introduction
- Container Basics
- Service Registration
- Dependency Injection Patterns
- Service Providers
- Service Factories
- Container Compilation
- Testing with DI
- Best Practices
- Troubleshooting
Introduction
Glueful ships a lightweight, fast PSR‑11 container tailored to the framework. It supports constructor autowiring, explicit factories, aliases, tagging, and a PHP code generator that compiles definitions for production. This document reflects the current container implementation used by the framework.
Highlights:
- PSR‑11 compatible interface
- Constructor autowiring with an Inject attribute (#Glueful\Container\Autowire\Inject)
- Simple array DSL for app/extension providers
- Aliases and interface bindings
- Tags and tag‑based iterators
- Lazy warmup groups (background/request‑time)
- Compile‑to‑PHP container for production (best‑effort, with fallback)
Container Basics
Getting services
// Using helpers
$logger = app(Psr\Log\LoggerInterface::class);
$cache = app(Glueful\Cache\CacheStore::class);
$router = app(Glueful\Routing\Router::class);
// PSR-11 directly
$c = container();
$database = $c->get('database'); // some services are string IDs
// Optional access pattern
if (has_service(Glueful\Auth\AuthenticationService::class)) {
$auth = app(Glueful\Auth\AuthenticationService::class);
}
Notes:
- There is no
getOptional()
; usehas_service()
as a guard. parameter('key')
reads parameters via the ParamBag/config if exposed.
Resolution semantics
- Class name:
app(Foo\Bar::class)
autowires constructor dependencies. - Aliases: string IDs (e.g.,
'cache.store'
,'database'
) resolve to services. - Interfaces: bind via alias so
app(Interface::class)
returns the implementation. - Parameters: inject via
#[Glueful\Container\Autowire\Inject(param: 'key')]
when autowiring, or read viaparameter('key')
inside a factory.
Tags and tagged iterators
Services may be tagged. Each tag is exposed as a container service of the same name that resolves to an array of instances ordered by priority
descending. Example: app('my.tag')
returns an array of tagged services.
Special lazy warmup tags:
lazy.background
— warmed after the first response returnslazy.request_time
— warmed during first request processing
CLI: php glueful di:lazy:status [--warm-background] [--warm-request]
Service Registration
Providers should define services in a static services()
method (preferred) that returns a simple array DSL. Framework core providers often implement a defs()
instance method (typed definitions via BaseServiceProvider). If both forms exist for a provider, the typed defs()
is used.
The services() array DSL
Supported keys per service entry:
class
string: concrete class (if omitted and the ID is a FQCN, the ID is used)autowire
bool: constructor autowiring (default false; set true for class autowire)factory
callable|string|array: one offn(Container $c) => ...
,'Class::method'
,[ClassName::class, 'method']
, or['@service.id','method']
arguments
array: constructor args; strings beginning with@
are treated as service referencesshared
bool: singleton when true (default true).singleton
orbind
keys map toshared
as shorthandsalias
string|array: create alias IDs that resolve to this servicetags
array: list of tag names or maps like['name' => 'tag.name', 'priority' => 10]
Notes:
- Anonymous Closures in
factory
are allowed in development but rejected by the production compiler; prefer class/method factories for production. - Use
#[Inject]
for parameter/config injection with autowiring.
Example:
use Glueful\Extensions\ServiceProvider;
use Psr\Log\LoggerInterface;
final class AppServiceProvider extends ServiceProvider
{
public static function services(): array
{
return [
// Autowired service (singleton by default)
App\Services\UserService::class => [
'autowire' => true,
'alias' => 'user_service',
'tags' => [['name' => 'domain.user', 'priority' => 50]],
],
// Interface binding via alias
App\Services\RedisCache::class => [
'autowire' => true,
'alias' => App\Contracts\CacheInterface::class,
],
// Factory service
LoggerInterface::class => [
'factory' => [App\Factories\LoggerFactory::class, 'create'],
'shared' => true,
],
// String ID alias convenience
'payment' => [
'class' => App\Services\PaymentService::class,
'autowire' => true,
],
];
}
}
DSL Cheatsheet and Shorthands
Service spec keys:
class
string — FQCN; defaults to the ID if the ID is a FQCN.autowire
bool — emit autowired class definition; ignoresarguments
.factory
— any of:'Class::method'
,[ClassName::class, 'method']
,['@service.id','method']
, or a Closure (dev only).arguments
array — constructor args; values starting with@
are service refs.shared
bool — singleton when true (default: true).alias
string|array — additional IDs that resolve to the same service.tags
array — either['tag.name', 'other.tag']
or[['name' => 'tag.name', 'priority' => 10]]
.
Shorthands:
singleton: true|false
→ maps toshared
.bind: true|false
→ maps toshared
(bind=true → shared; bind=false → not shared).
Service references:
- Use
'@id'
insidearguments
and in factory target arrays.'@'
alone is invalid.
Production rules (enforced by loader/compiler):
- No Closure factories in production.
- No arbitrary object instances in
arguments
in production (scalars/arrays/enums only).
Examples:
return [
// Class + arguments (singleton)
'mail.transport' => [
'class' => App\Mail\Transport::class,
'arguments' => ['smtp', 587, '@'.Psr\Log\LoggerInterface::class],
'singleton' => true, // shorthand
'tags' => ['lazy.request_time'],
],
// Autowire
App\Search\Indexer::class => [
'autowire' => true,
'bind' => true, // shorthand for shared
'alias' => 'search.indexer',
],
// Factory using service method
'blog.client' => [
'class' => Vendor\Blog\Client::class,
'factory' => ['@http.client', 'forBlog'],
'shared' => true,
'alias' => [Vendor\Blog\Client::class, 'blog.http'],
'tags' => [['name' => 'lazy.background', 'priority' => 5]],
],
// Static factory method string
Psr\Log\LoggerInterface::class => [
'factory' => App\Factories\LoggerFactory::class.'::create',
'shared' => true,
],
];
Typed defs() examples (advanced)
For maximum performance and explicit control, providers can return typed definitions. Framework providers commonly extend Glueful\Container\Providers\BaseServiceProvider
which offers helpers for autowire, alias, and tag.
use Glueful\Container\Providers\BaseServiceProvider;
use Glueful\Container\Definition\{FactoryDefinition, ValueDefinition, DefinitionInterface};
final class CoreProvider extends BaseServiceProvider
{
/** @return array<string, DefinitionInterface|callable|mixed> */
public function defs(): array
{
$defs = [];
// Autowire singleton
$defs[App\Services\HealthService::class] = $this->autowire(App\Services\HealthService::class);
// Factory definition (shared)
$defs['db.pool'] = new FactoryDefinition(
'db.pool',
fn(\Psr\Container\ContainerInterface $c) => \Vendor\Db\Pool::fromConfig((array) config('database.pool', []))
);
// String alias for convenience
$defs['health'] = $this->alias('health', App\Services\HealthService::class);
// Parameter/value style service
$defs['feature.flags'] = new ValueDefinition('feature.flags', [
'beta' => (bool) config('app.beta', false),
]);
// Tag for lazy warmup (higher priority warms earlier)
$this->tag('db.pool', 'lazy.background', 10);
return $defs;
}
}
Extension providers can also publish tags via a static tags()
method which the ContainerFactory reads when assembling the container:
final class MyExtensionProvider extends \Glueful\Extensions\ServiceProvider
{
public static function services(): array { /* ... */ }
public static function tags(): array
{
return [
'lazy.request_time' => [
'payment', // string ID
['service' => App\Search\Indexer::class, 'priority' => 5],
],
];
}
}
Dependency Injection Patterns
Constructor injection (recommended)
use Psr\Log\LoggerInterface;
use App\Repositories\UserRepository;
class UserService
{
public function __construct(
private UserRepository $repository,
private LoggerInterface $logger,
private string $defaultRole = 'user'
) {}
}
// Registration
public static function services(): array
{
return [
App\Services\UserService::class => [
'autowire' => true,
// Explicit args (optional) — 'arguments' => ['@'.UserRepository::class, '@'.LoggerInterface::class, 'member']
],
];
}
Interface dependencies
interface CacheInterface { /* ... */ }
class RedisCache implements CacheInterface
{
public function __construct(private \Redis $redis) {}
}
class ProductService
{
public function __construct(private CacheInterface $cache) {}
}
// Bind interface by aliasing to the implementation entry
public static function services(): array
{
return [
RedisCache::class => [
'autowire' => true,
'arguments' => ['@redis'],
'alias' => CacheInterface::class,
],
];
}
Optional dependencies and config
Use #[Inject]
for configuration values and constructor defaults for optional services.
use Glueful\Container\Autowire\Inject;
use Psr\Log\LoggerInterface;
class ApiClient
{
public function __construct(
#[Inject(param: 'api.base_url')] private string $baseUrl,
#[Inject(param: 'api.key')] private string $apiKey,
?LoggerInterface $logger = null,
) {}
}
public static function services(): array
{
return [ ApiClient::class => ['autowire' => true] ];
}
Service Providers
Service providers organize service registration and lifecycle.
Enable providers via config:
- App providers:
config/serviceproviders.php
(enabled
,dev_only
, oronly
for allow‑list) - Vendor extensions:
config/extensions.php
(enabled
,dev_only
,disabled
, optional Composer scan)
Example provider:
use Glueful\Extensions\ServiceProvider;
final class PaymentServiceProvider extends ServiceProvider
{
public static function services(): array { return [/* ... */]; }
public function register(): void
{
// merge config, register routes, migrations, etc.
$this->mergeConfig('payment', require base_path('config/payment.php'));
}
public function boot(): void
{
// optional: runs after all providers are registered
}
}
Framework‑provided services (selection):
Psr\Log\LoggerInterface
— PSR‑3 loggerGlueful\Cache\CacheStore
— cache (also'cache.store'
)Glueful\Routing\Router
— HTTP router'database'
— database connection factoryGlueful\Database\QueryBuilder
— query builder- Middleware aliases:
'auth'
,'rate_limit'
,'csrf'
,'metrics'
,'tracing'
, etc.
Service Factories
Factories provide dynamic service creation. Prefer class/method factories for production.
use Glueful\Bootstrap\ConfigurationCache;
use Psr\Log\LoggerInterface;
class EmailServiceFactory
{
public static function create(\Psr\Container\ContainerInterface $c): EmailServiceInterface
{
$config = ConfigurationCache::get('mail', []);
return match ($config['driver'] ?? 'smtp') {
'smtp' => new SmtpEmailService(/* ... */),
'sendmail' => new SendmailEmailService(/* ... */),
'log' => new LogEmailService($c->get(LoggerInterface::class)),
default => throw new \InvalidArgumentException('Unsupported mail driver'),
};
}
}
public static function services(): array
{
return [
EmailServiceInterface::class => [
'factory' => [EmailServiceFactory::class, 'create'],
'shared' => true,
],
];
}
Container Compilation
Glueful compiles service definitions to a compact PHP class in production. The framework automatically prefers a precompiled container at storage/cache/container/CompiledContainer.php
; otherwise it attempts best‑effort compilation at runtime and falls back to the dynamic container if unsupported definitions are present.
CLI support:
php glueful di:container:debug --services # List services
php glueful di:container:debug My\\Service # Inspect a service
php glueful di:container:debug --aliases # Show aliases
php glueful di:container:debug --tags # Show tags
php glueful di:container:debug --parameters # Show parameters
php glueful di:container:map --format=json # Dump service map (ids, types, aliases, tags)
php glueful di:container:validate --check-circular # Check circular deps
php glueful di:container:compile --optimize # Compile for prod
php glueful di:lazy:status --warm-background # Warm background set
Compiler support matrix:
- Supported: AutowireDefinition, ValueDefinition, TaggedIteratorDefinition, AliasDefinition
- Not compiled (fallback to runtime): FactoryDefinition and any definition involving runtime closures or non‑serializable objects
Testing with DI
Unit tests:
- Construct services directly and pass mock dependencies.
Integration tests:
- Boot the framework to build the real container, then resolve services via
app()
. - For overrides, layer a child container with
container()->with([ Service::class => fn($c) => new FakeService(), ])
and inject it where appropriate (e.g., into console commands or your own entrypoints).
Best Practices
- Prefer constructor injection; avoid pulling from the container inside services.
- Bind interfaces via aliases and target the interface in your constructors.
- Keep services single‑purpose; split responsibilities rather than adding flags.
- Inject configuration with
#[Inject(param: 'key')]
instead of reading config in method bodies. - Use tags for batch operations and to defer heavy warmups to lazy groups.
Troubleshooting
Common issues:
Missing service:
if (!has_service(App\Services\UserService::class)) {
throw new \RuntimeException('UserService not registered');
}
Circular dependency:
Glueful\Container\Exception\ContainerException: Circular dependency detected: A -> B -> A
Refactor to break the cycle (extract an interface, use a factory, or invert one dependency).
Debugging tools:
php glueful di:container:debug
to inspect services, aliases, tags, and parametersphp glueful di:container:validate
to validate graphs, circular refs, and providersphp glueful di:container:compile
to precompile for production