Compilation
Introduction
Container compilation is a feature that takes a bootstrapped, fully-configured container and compiles it into a standalone PHP class. The compiled container implements the PSR-11 ContainerInterface and skips all the overhead of definition resolution, reflection-based autowiring, and closure evaluation at runtime. The compiled class IS the container, with all service definitions baked directly into methods.
This is particularly valuable for production environments where you want maximum performance and minimal startup time. Your development workflow remains unchanged—you configure your container dynamically as usual—but when deploying to production, you compile that configuration into a static PHP class that has zero reflection overhead.
Quick Start
Getting from zero to a compiled container takes three simple steps.
Create a Bootstrap File
First, create a bootstrap file (e.g. bootstrap/container.php) that returns a fully-configured League\Container\Container instance. This is the container that will be compiled. All your service definitions go here.
<?php
use League\Container\Container;
$container = new Container();
$container->add(App\Database\Connection::class)
->addArgument('sqlite:memory')
;
$container->add(App\Repository\UserRepository::class)
->addArgument(App\Database\Connection::class)
;
$container->add(App\Service\UserService::class)
->addArgument(App\Repository\UserRepository::class)
;
return $container;
Run the CLI Compiler
Execute the compiler binary, pointing it to your bootstrap file:
vendor/bin/container-compile --input=bootstrap/container.php
This generates CompiledContainer.php in your current directory by default.
Use ContainerFactory to Load It
In your application entry point, use ContainerFactory to switch between compiled and dynamic containers depending on your environment:
<?php
use League\Container\ContainerFactory;
$container = ContainerFactory::create(
compiledClass: App\Infrastructure\CompiledContainer::class,
bootstrap: __DIR__ . '/bootstrap/container.php',
useCompiled: ($_ENV['APP_ENV'] ?? 'dev') === 'production',
);
$userService = $container->get(App\Service\UserService::class);
That’s it. When APP_ENV is production and the compiled container exists, you get instant service resolution. When developing locally, you get the full dynamic container with all its flexibility.
Prerequisites
Before attempting to compile your container, ensure the following conditions are met.
Bootstrap File Must Return a Container
The file passed to --input must return a League\Container\Container instance. The compiler requires this to analyse and compile your definitions.
<?php
use League\Container\Container;
$container = new Container();
return $container;
All Service Definitions Must Be Side-Effect Free
Definitions are analysed and compiled at compile time. They cannot perform side effects (such as writing to files, making HTTP requests, or logging) because those effects only happen during compilation, not at runtime. Register your service definitions with pure data and callable references only.
No Anonymous Closures as Concrete Values
The compiler cannot serialise anonymous closures. If you currently use closures as concrete values or arguments, you must refactor them into named callables or factory classes.
Unsupported (will not compile):
<?php
$container->add('service', fn() => new Service());
$container->add(Foo::class)->addArgument(fn() => 'value');
Supported alternatives: a factory class method, a static method on the service class, or an invokable class.
<?php
$container->add('service', [ServiceFactory::class, 'create']);
$container->add('service', [Service::class, 'make']);
$container->add('service', ServiceFactory::class);
CLI Reference
The container compiler is available at vendor/bin/container-compile.
Usage
vendor/bin/container-compile [options]
Options
| Option | Required | Description | Default |
|---|---|---|---|
--input=FILE |
Yes | Path to bootstrap file returning a Container instance | N/A |
--output=FILE |
No | Output path for the compiled PHP file | ./CompiledContainer.php |
--class=NAME |
No | Generated class name | CompiledContainer |
--namespace=NS |
No | PHP namespace for the generated class | (none) |
--check |
No | Check if compiled container is stale without writing | N/A |
--dry-run |
No | Validate and report compilation results without writing | N/A |
--help |
No | Show usage information | N/A |
Exit Codes
0- Compilation succeeded (or check mode found container is fresh)1- Compilation failed (or check mode found container is stale)2- Invalid command-line arguments
Examples
Basic Compilation
vendor/bin/container-compile --input=bootstrap/container.php
Generates CompiledContainer.php in the current directory.
With Custom Namespace and Class Name
vendor/bin/container-compile \
--input=bootstrap/container.php \
--output=src/Infrastructure/CompiledContainer.php \
--class=Container \
--namespace=App\Infrastructure
Generates src/Infrastructure/CompiledContainer.php with fully qualified class name App\Infrastructure\Container.
Dry Run (Validate Without Writing)
vendor/bin/container-compile \
--input=bootstrap/container.php \
--dry-run
Validates the container and reports compilation statistics without writing any files. Useful in CI pipelines to validate that your container is compilable before deployment.
Check Mode (For CI/CD)
vendor/bin/container-compile \
--input=bootstrap/container.php \
--check
Checks if the compiled container is stale compared to the bootstrap file. Exits with 0 if fresh, 1 if stale. The compiled class must be autoloadable (via Composer or otherwise) for check mode to work, as it uses class_exists() to locate the class and read its SOURCE_HASH constant.
PHP API Reference
You can also compile containers programmatically from PHP code.
Basic Compilation
<?php
use League\Container\Compiler\Compiler;
use League\Container\Compiler\CompilationConfig;
use League\Container\Container;
$container = new Container();
$container->add(MyService::class);
$compiler = new Compiler();
$result = $compiler->compile($container, new CompilationConfig(
namespace: 'App\Infrastructure',
className: 'CompiledContainer',
));
$result->writeTo('/path/to/output.php');
Compiler Class
The League\Container\Compiler\Compiler class provides the main compilation interface.
Methods
compile(Container|string $container, CompilationConfig $config): CompilationResult
Compiles a container into PHP source code. Accepts either a Container instance or a string path to a bootstrap file. Returns a CompilationResult value object.
isStale(string $compiledClass, Container|string $container): bool
Checks whether a previously-compiled container is stale. Compares the current container definitions against the source hash embedded in the compiled class. Returns true if recompilation is needed.
<?php
$compiler = new Compiler();
$isStale = $compiler->isStale(
App\Infrastructure\CompiledContainer::class,
__DIR__ . '/bootstrap/container.php'
);
if ($isStale) {
$result = $compiler->compile($container, $config);
$result->writeTo('/path/to/CompiledContainer.php');
}
CompilationConfig
Value object holding compilation settings.
<?php
use League\Container\Compiler\CompilationConfig;
$config = new CompilationConfig(
namespace: 'App\Infrastructure',
className: 'CompiledContainer',
);
Properties:
namespace: string- PHP namespace for the generated class (default: empty string)className: string- Class name (default:CompiledContainer)
Both values are validated to ensure they form valid PHP identifiers and namespaces.
CompilationResult
Value object returned by Compiler::compile().
Properties:
phpSource: string- The generated PHP source codefullyQualifiedClassName: string- Fully qualified class name (e.g.App\Infrastructure\CompiledContainer)sourceHash: string- SHA-256 hash of the definition state (used for staleness detection)serviceCount: int- Number of services compiledwarnings: array- List of warnings (e.g. non-compilable definitions)
Methods:
writeTo(string $path): void
Writes the compiled PHP source to a file. Uses atomic writing (temporary file + rename) to ensure data integrity.
<?php
$result->writeTo('/path/to/CompiledContainer.php');
Error Handling
The compiler throws League\Container\Compiler\CompilationException when it encounters errors that prevent compilation. The exception contains structured error information.
<?php
use League\Container\Compiler\CompilationException;
try {
$compiler = new Compiler();
$result = $compiler->compile($container, $config);
} catch (CompilationException $exception) {
$errors = $exception->getErrors();
foreach ($errors as $error) {
echo "Service: " . $error['serviceId'] . PHP_EOL;
echo "Type: " . $error['errorType'] . PHP_EOL;
echo "Message: " . $error['message'] . PHP_EOL;
echo "Fix: " . $error['suggestedFix'] . PHP_EOL;
}
}
ContainerFactory
League\Container\ContainerFactory provides a convenient way to switch between compiled and dynamic containers based on your environment.
Usage
<?php
use League\Container\ContainerFactory;
$container = ContainerFactory::create(
compiledClass: App\Infrastructure\CompiledContainer::class,
bootstrap: __DIR__ . '/bootstrap/container.php',
useCompiled: $_ENV['APP_ENV'] === 'production',
);
Method Signature
ContainerFactory::create(string $compiledClass, string|callable $bootstrap, bool $useCompiled = true): ContainerInterface
$compiledClass- Fully qualified class name of your compiled container$bootstrap- Either a file path (string) or callable that returns aContainerInterface$useCompiled- Whether to use the compiled container (if it exists)
Behaviour
When $useCompiled is true:
- If the compiled class exists and is loadable, an instance is created and returned
- If the compiled class does not exist, the bootstrap file/callable is used instead
- This allows graceful fallback if compilation hasn’t happened yet
When $useCompiled is false:
- The bootstrap file/callable is always used
- The compiled class is never loaded
Examples
File-Based Bootstrap
<?php
use League\Container\ContainerFactory;
$container = ContainerFactory::create(
compiledClass: App\Infrastructure\CompiledContainer::class,
bootstrap: __DIR__ . '/bootstrap/container.php',
useCompiled: true,
);
Callable Bootstrap
<?php
use League\Container\ContainerFactory;
$container = ContainerFactory::create(
compiledClass: App\Infrastructure\CompiledContainer::class,
bootstrap: fn() => new League\Container\Container(),
useCompiled: true,
);
Environment-Aware Switching
<?php
use League\Container\ContainerFactory;
$container = ContainerFactory::create(
compiledClass: App\Infrastructure\CompiledContainer::class,
bootstrap: __DIR__ . '/bootstrap/container.php',
useCompiled: match($_ENV['APP_ENV'] ?? 'dev') {
'production' => true,
'staging' => true,
default => false,
},
);
Development and Production Workflow
A typical workflow looks like this:
Development
During development, use the dynamic container. This gives you full flexibility: you can add services at runtime, use closures, test with mocks, and rely on events and listeners. Your bootstrap file (e.g. config/container.php) might look like:
<?php
use League\Container\Container;
$container = new Container();
$container->add(Database::class)
->addArgument($_ENV['DATABASE_URL'] ?? 'sqlite:memory')
;
$container->afterResolve(LoggableInterface::class, fn($service) => $service->setLogger($logger));
return $container;
Use ContainerFactory with useCompiled: false:
<?php
$container = ContainerFactory::create(
compiledClass: App\Infrastructure\CompiledContainer::class,
bootstrap: __DIR__ . '/config/container.php',
useCompiled: false,
);
Deployment
Before deploying to production:
- Ensure all service definitions are compilable (no closures, side effects, etc.)
- Run the compiler to generate the compiled class
- Commit the compiled class to version control
- Deploy with
useCompiled: true
vendor/bin/container-compile --input=bootstrap/container.php
git add src/Infrastructure/CompiledContainer.php
git commit -m "Recompile container for production deployment"
Runtime Switching
In production, the application entry point remains the same:
<?php
$container = ContainerFactory::create(
compiledClass: App\Infrastructure\CompiledContainer::class,
bootstrap: __DIR__ . '/bootstrap/container.php',
useCompiled: $_ENV['APP_ENV'] === 'production',
);
$app = $container->get(Application::class);
$app->run();
The compiled container will be used in production. If compilation hasn’t happened yet, it gracefully falls back to the dynamic container.
CI/CD Integration
Use the --check flag in your CI pipeline to detect when compiled containers become stale.
Detecting Stale Compiled Containers
Create a CI step that checks if your compiled container is up to date with your bootstrap file:
#!/bin/bash
vendor/bin/container-compile --input=bootstrap/container.php --check
if [ $? -eq 1 ]; then
echo "ERROR: Compiled container is stale. Run:"
echo " vendor/bin/container-compile --input=bootstrap/container.php"
exit 1
fi
This exits with code 1 if recompilation is needed, preventing stale deployments.
Deploy Script
A complete deployment script might look like:
#!/bin/bash
set -e
echo "Installing dependencies..."
composer install --no-dev --optimize-autoloader
echo "Running tests..."
vendor/bin/phpunit
echo "Compiling container..."
vendor/bin/container-compile \
--input=bootstrap/container.php \
--output=src/Infrastructure/CompiledContainer.php \
--namespace=App\Infrastructure \
--class=CompiledContainer
echo "Deployment ready"
Source Hash Constant
The compiled container includes a SOURCE_HASH constant that holds a SHA-256 hash of your definition state. This hash is based on:
- All service definition IDs and their configuration
- Dependency graph structure
- Constructor arguments
- Method calls
- Tagged definitions
- The compiler version (
COMPILER_VERSIONconstant)
The hash does NOT depend on file modification times. It captures the semantic state of your definitions, so reordering code or refactoring without changing configuration does not invalidate the hash. Upgrading the container library itself may also invalidate the hash if the compiler version changes.
What Gets Compiled
This table shows which definition types are supported and how they compile:
| Definition Type | Compiled As |
|---|---|
| Class with explicit constructor arguments | new ClassName($this->get(Dep::class), 'literal') |
| Interface aliased to concrete class | return $this->get(ConcreteClass::class) |
| Class relying on autowiring | Reflected at compile time, baked as explicit new with resolved dependencies |
| Literal or scalar value | Serialised via var_export() in a method returning the value |
| Static method callable | FactoryClass::methodName(...$resolvedArgs) |
| Instance method callable | $this->get(FactoryClass::class)->methodName(...) |
| Invokable class registered by string | Dependencies resolved, invoked as (new InvokableClass(...))->__invoke(...) |
| Definition with method calls | Sequential method invocations after construction |
| Tagged services | Method returning array of $this->get() calls for each tagged service |
What Gets Rejected
The compiler collects all errors and reports them together, rather than failing on the first error. Each error includes a suggested migration path.
Anonymous Closures
Error type: closure_concrete
Migration: Replace with a named callable. This will not compile:
<?php
$container->add(Logger::class, fn() => new Logger());
Instead, create a factory class and reference it:
<?php
namespace App\Factory;
class LoggerFactory
{
public static function create(): Logger
{
return new Logger();
}
}
$container->add(Logger::class, [LoggerFactory::class, 'create']);
Closure::fromCallable()
Closure::fromCallable() returns a Closure and is caught by the same closure_concrete check.
Migration: Use the callable array directly. This will not compile:
<?php
$container->add('handler', Closure::fromCallable([HandlerFactory::class, 'handle']));
Replace with the callable array:
<?php
$container->add('handler', [HandlerFactory::class, 'handle']);
Object Instances as Concrete or Arguments
Error type: object_concrete
Migration: Register a factory that constructs the instance. This will not compile:
<?php
$instance = new Service();
$container->add('service', $instance);
Replace with a factory method reference:
<?php
$container->add('service', [ServiceFactory::class, 'getInstance']);
Interfaces with No Binding
Error type: unresolvable_interface_parameter
Migration: Add an explicit binding for the interface. If Controller depends on LoggerInterface but nothing binds it, this will fail:
<?php
$container->add(Controller::class);
Add the missing binding:
<?php
$container->add(LoggerInterface::class, FileLogger::class);
$container->add(Controller::class);
Circular Dependencies
Error type: circular_dependency
Migration: Break the cycle using an interface or factory. A direct circular dependency like this will not compile:
<?php
$container->add(ServiceA::class)->addArgument(ServiceB::class);
$container->add(ServiceB::class)->addArgument(ServiceA::class);
Introduce an interface to break the cycle:
<?php
interface ServiceInterface {}
class ServiceA implements ServiceInterface
{
public function __construct(private ServiceB $serviceB) {}
}
class ServiceB
{
public function __construct(private ServiceInterface $service) {}
}
$container->add(ServiceInterface::class, ServiceA::class);
$container->add(ServiceA::class);
$container->add(ServiceB::class);
Migration Guide
If you have an existing container that currently uses dynamic features, here’s how to prepare it for compilation.
Closures to Named Callables
Replace all closure-based definitions with static or instance method callables. A closure-based definition like this will not compile:
<?php
$container->add(Mailer::class, function() {
return new Mailer(
smtpHost: $_ENV['SMTP_HOST'],
smtpPort: (int)$_ENV['SMTP_PORT'],
);
});
Extract the logic into a factory class and reference the method:
<?php
class MailerFactory
{
public static function create(): Mailer
{
return new Mailer(
smtpHost: $_ENV['SMTP_HOST'],
smtpPort: (int)$_ENV['SMTP_PORT'],
);
}
}
$container->add(Mailer::class, [MailerFactory::class, 'create']);
Event Listeners to Method Calls
The compiled container does not fire events. Replace afterResolve() listeners with addMethodCall() on definitions. An event-based approach like this will not work in the compiled container:
<?php
$container->afterResolve(LoggableInterface::class, function (object $service) use ($container) {
$service->setLogger($container->get(Logger::class));
});
Replace with explicit method calls on the affected definitions:
<?php
$container->add(SomeService::class)
->addMethodCall('setLogger', [Logger::class])
;
getNew() Usage
The compiled container implements the PSR-11 ContainerInterface only. It does not have a getNew() method. If you use getNew() to get non-shared instances, you must:
- Change code that calls
getNew()to useget()instead - Ensure those services are registered as non-shared (the default)
Code using getNew():
<?php
$service1 = $container->getNew(Service::class);
$service2 = $container->getNew(Service::class);
Replace with get() on a non-shared definition (which is the default behaviour):
<?php
$container->add(Service::class);
$service1 = $container->get(Service::class);
$service2 = $container->get(Service::class);
assert($service1 !== $service2);
Type Hints: DefinitionContainerInterface to ContainerInterface
If you type-hint against DefinitionContainerInterface in your services, you’ll get a compilation error in the compiled container (it only implements ContainerInterface). Change your type hints from DefinitionContainerInterface:
<?php
use League\Container\DefinitionContainerInterface;
class MyService
{
public function __construct(private DefinitionContainerInterface $container) {}
}
To ContainerInterface:
<?php
use Psr\Container\ContainerInterface;
class MyService
{
public function __construct(private ContainerInterface $container) {}
}
Non-ReflectionContainer Delegates
If you use custom delegates (via delegate()), they are not compiled. A setup using custom delegates:
<?php
$container->delegate(MyCustomLocator::class);
Replace by adding explicit definitions for each service that was resolved through the delegate:
<?php
$container->add(Service1::class, [MyCustomLocator::class, 'getService1']);
$container->add(Service2::class, [MyCustomLocator::class, 'getService2']);
Limitations
The compiled container has important limitations you should be aware of:
No Event Dispatch
The compiled container does not fire any events. All event-based functionality (on(), afterResolve(), afterResolving()) is ignored during compilation and will not work in the compiled container.
Mitigation: Use addMethodCall() to configure services instead.
No Runtime Additions
You cannot call add(), addShared(), or any definition methods on a compiled container. All services must be defined at compile time.
Mitigation: If you need to add services at runtime, use the dynamic container instead.
No getNew() Method
The compiled container only implements Psr\Container\ContainerInterface. It does not have the getNew() method from DefinitionContainerInterface.
Mitigation: Use get() and ensure non-shared services are registered as such.
No Closure Support
Closures cannot be serialised. Any closure used as a concrete value, argument, or factory will cause a compilation error.
Mitigation: Refactor closures into factory classes or static methods.
Non-ReflectionContainer Delegates Not Compiled
Custom delegates are not included in the compiled container. Only the ReflectionContainer delegate (used for autowiring) is compiled.
Mitigation: Add explicit definitions for services that would be resolved through custom delegates.
Error Handling
When compilation fails, the compiler throws League\Container\Compiler\CompilationException. This exception contains structured error information and can be caught and handled programmatically.
Exception Structure
Each error in the exception contains:
serviceId- The ID of the service that caused the errorerrorType- Category of the error (e.g.circular_dependency,closure_concrete,object_concrete)message- Description of what went wrongsuggestedFix- Recommendation for how to fix it
Catching and Handling Errors
<?php
use League\Container\Compiler\CompilationException;
use League\Container\Compiler\Compiler;
try {
$compiler = new Compiler();
$result = $compiler->compile($container, $config);
} catch (CompilationException $exception) {
$errors = $exception->getErrors();
if (count($errors) === 0) {
echo "Compilation failed: " . $exception->getMessage() . PHP_EOL;
} else {
echo count($errors) . " compilation error(s) found:" . PHP_EOL . PHP_EOL;
foreach ($errors as $error) {
echo "Service: {$error['serviceId']}" . PHP_EOL;
echo "Error: {$error['errorType']}" . PHP_EOL;
echo "Message: {$error['message']}" . PHP_EOL;
echo "Fix: {$error['suggestedFix']}" . PHP_EOL . PHP_EOL;
}
}
}
The compiler collects all errors before throwing, so you can address multiple issues at once rather than fixing them one at a time.