Container¶
Overview¶
Phalcon\Container\Container is a modern dependency injection container built alongside the existing Phalcon\Di\Di. It supports autowiring, service lifetimes, lazy value resolution, service tags, and decorator extension. It is designed for both standard PHP shared-nothing requests and long-running environments (Octane, Swoole, RoadRunner).
Container is the recommended choice for new projects. Phalcon\Di\Di remains fully supported and is not being removed.
WARNING
Container cannot be used with components that use the Inversion of Control methodology in Phalcon. That is, the container is available in the component, and it is used to retrive a different service to be used in that component. An example is the Response object which internally retrieves the Filter service for filtering. For this to happen, we need to widen the getDI() and setDI() which will affect backwards compatibiity. As such, Container cannot be used out of the box for this version. This refers to the Registering as the Framework Default section below. In the next major version we will change the interfaces to offer this functionality.
NOTE
Container does not implement PSR-11 ContainerInterface. It implements ioc-interop/IocContainer (getService()/hasService()). A PSR-11 bridge adapter is planned for a future release.
Quick Start¶
<?php
use Phalcon\Container\Container;
$container = new Container();
$container->set('mailer', Mailer::class);
$mailer = $container->get('mailer');
Registering Services¶
By Class Name¶
Pass a fully-qualified class name as a string. The container resolves constructor dependencies automatically on first resolution via autowiring.
<?php
use Phalcon\Container\Container;
$container = new Container();
$container->set('logger', FileLogger::class);
$container->set('mailer', Mailer::class);
// Mailer's constructor receives a FileLogger automatically if type-hinted
$mailer = $container->get('mailer');
By Closure¶
Pass a closure. The container is passed as the only argument when the closure is invoked.
<?php
use Phalcon\Container\Container;
$container = new Container();
$container->set('db', function (Container $c) {
return new DatabaseConnection(
$c->get('db.host'),
(int) $c->get('db.port'),
);
});
By Object Instance¶
Pass an already-constructed object. It is returned as-is on every get() call.
<?php
use Phalcon\Container\Container;
$container = new Container();
$container->set('config', new Config(['debug' => true]));
Retrieving Services¶
get() - Shared Instance¶
Returns a shared (cached) instance for SCOPED and SINGLETON lifetimes. Calls the definition once and stores the result.
new() - Fresh Instance¶
Always creates a new instance, bypassing the shared cache, regardless of the service lifetime.
has() - Check¶
Returns true if the name is registered, cached, is a parameter, or is an autowirable class name.
Service Lifetimes¶
Three lifetimes are available via Phalcon\Container\Definition\ServiceLifetime:
| Constant | Behavior |
|---|---|
SCOPED | Shared within a request. Default. In long-running environments, clear per-request via unsetInstances(ServiceLifetime::SCOPED). |
SINGLETON | Shared across all requests. Never cleared automatically. |
TRANSIENT | Never cached. A new instance is created on every get() call - identical to always calling new(). |
In standard PHP (shared-nothing model), SCOPED and SINGLETON behave identically. The distinction matters only in long-running process environments.
<?php
use Phalcon\Container\Container;
use Phalcon\Container\Definition\ServiceLifetime;
$container = new Container();
$container->set('session', Session::class)
->setLifetime(ServiceLifetime::SCOPED); // cleared per-request (default)
$container->set('config', AppConfig::class)
->setLifetime(ServiceLifetime::SINGLETON); // persists across requests
$container->set('query', QueryBuilder::class)
->setLifetime(ServiceLifetime::TRANSIENT); // always a fresh instance
Autowiring¶
Autowiring is enabled by default. When a class name is registered - or looked up directly by class name - the container uses reflection to resolve constructor dependencies automatically.
<?php
use Phalcon\Container\Container;
class LoggerAwareMailer
{
public function __construct(private FileLogger $logger) {}
}
$container = new Container();
$container->set('logger', FileLogger::class);
$container->set('mailer', LoggerAwareMailer::class);
// FileLogger is injected into LoggerAwareMailer automatically
$mailer = $container->get('mailer');
// Without defining FileLogger or LoggerAwareMailer
$container = new Container();
$container->set('mailer', LoggerAwareMailer::class);
Autowiring can be disabled globally. setAutowire() returns the container, so it can be chained with other registration calls:
When disabled, only explicitly registered services are resolved.
Service Definition¶
set() returns a ServiceDefinition object. Use it to configure the service fluently before it is first resolved.
<?php
use Phalcon\Container\Container;
use Phalcon\Container\Definition\ServiceLifetime;
$container = new Container();
$container->set('mailer', Mailer::class)
->setLifetime(ServiceLifetime::TRANSIENT)
->addTag('notification.sender');
The Freeze Pattern¶
ServiceDefinition is mutable during registration. On first resolution, the container calls freeze() on the definition: reflection runs once, constructor arguments are collated and locked, and all setters are disabled. Subsequent resolutions use the pre-computed data with no further reflection overhead.
Attempting to modify a frozen definition - calling setLifetime(), addTag(), setArgument(), etc. - throws FrozenDefinition.
Constructor Argument Overrides¶
Use setArgument() to override specific constructor parameters by name:
<?php
use Phalcon\Container\Container;
use Phalcon\Container\Resolver\Lazy\Lazy;
$container = new Container();
$container->set('db', DatabaseConnection::class)
->setArgument('host', Lazy::get('db.host'))
->setArgument('port', Lazy::get('db.port'));
Interface Binding¶
Use bind() to map an interface to a concrete class. Services that type-hint the interface receive the concrete implementation during autowiring.
<?php
use Phalcon\Container\Container;
$container = new Container();
$container->bind(LoggerInterface::class, FileLogger::class);
// Mailer type-hints LoggerInterface - FileLogger is injected automatically
$container->set('mailer', Mailer::class);
$mailer = $container->get('mailer');
bind() is a semantic wrapper around set() and returns a ServiceDefinition for further configuration.
Aliases¶
Register a short name that points to an existing service. The alias chain is resolved transparently on every get() or has() call.
<?php
use Phalcon\Container\Container;
$container = new Container();
$container->set(LoggerInterface::class, FileLogger::class);
$container->setAlias(LoggerInterface::class, 'logger');
// Both names resolve to the same shared instance
$a = $container->get(LoggerInterface::class);
$b = $container->get('logger');
NOTE
Circular aliases are detected at registration time and throw CircularAliasFound.
Parameters¶
Parameters store scalar values (strings, integers, arrays, environment variables) separately from services. They are retrieved via get() the same as services.
<?php
use Phalcon\Container\Container;
use Phalcon\Container\Resolver\Lazy\Lazy;
$container = new Container();
$container
->setParameter('db.name', 'my_database')
->setParameter('db.host', Lazy::env('DB_HOST'))
->setParameter('db.port', Lazy::env('DB_PORT', 'int'))
->setParameter('app.allowed_ips', Lazy::csEnv('ALLOWED_IPS'));
// Retrieve
$host = $container->get('db.host');
$port = $container->get('db.port');
Resolvable parameter values (such as Lazy::env()) are resolved on first get() and their result is cached back.
Lazy Values¶
Phalcon\Container\Resolver\Lazy\Lazy is a static factory that provides convenient construction of all lazy value types. Import one class instead of twelve separate ones.
| Factory Method | Purpose |
|---|---|
Lazy::get(string $id) | Resolves a service by name via get() |
Lazy::newInstance(string $id) | Creates a fresh instance via new() |
Lazy::call(callable $callable) | Calls a callable, passing the container |
Lazy::getCall(string $id, string $method, array $args) | Gets a shared service then calls a method on it |
Lazy::newCall(string $id, string $method, array $args) | Creates a fresh instance then calls a method on it |
Lazy::staticCall(string $className, string $method, array $args) | Calls a static method on a class |
Lazy::functionCall(string $function, array $args) | Calls a plain PHP function |
Lazy::arrayValues(array $values) | Resolves an array of lazy values recursively |
Lazy::callableGet(string $id) | Returns a closure wrapping get($id) |
Lazy::callableNew(string $id) | Returns a closure wrapping new($id) |
Lazy::env(string $name, string $type = null) | Reads an environment variable with optional type casting |
Lazy::csEnv(string $name, string $type = null) | Reads a comma-separated environment variable into a typed array |
Usage¶
<?php
use Phalcon\Container\Container;
use Phalcon\Container\Resolver\Lazy\Lazy;
$container = new Container();
$container
->setParameter('db.host', Lazy::env('DB_HOST'))
->setParameter('db.port', Lazy::env('DB_PORT', 'int'));
$container->set('db', DatabaseConnection::class)
->setArgument('host', Lazy::get('db.host'))
->setArgument('port', Lazy::get('db.port'));
// Static factory
$container->setParameter('app.version', Lazy::staticCall(AppVersion::class, 'current', []));
// Callable wrappers - useful for passing lazy service references
$getMailer = $container->callableGet('mailer'); // Closure: fn() => $container->get('mailer')
$newMailer = $container->callableNew('mailer'); // Closure: fn() => $container->new('mailer')
Service Tags¶
Tag services to group them by a label. Retrieve all tagged services as resolved instances with getByTag().
<?php
use Phalcon\Container\Container;
$container = new Container();
$container->set('subscriber.email', EmailSubscriber::class)
->addTag('event.subscriber');
$container->set('subscriber.log', LogSubscriber::class)
->addTag('event.subscriber');
// Returns [EmailSubscriber instance, LogSubscriber instance]
$subscribers = $container->getByTag('event.subscriber');
foreach ($subscribers as $subscriber) {
$subscriber->onEvent($event);
}
Service Extension (Decoration)¶
Decorate an existing service definition with additional callables that run after instantiation. Each extender receives the current instance and the container, and must return the (modified) instance.
<?php
use Phalcon\Container\Container;
$container = new Container();
// On the definition - fluent, before first resolution
$container->set('mailer', Mailer::class)
->addExtender(function (Mailer $mailer, Container $c) {
$mailer->setLogger($c->get('logger'));
return $mailer;
});
// Via the container - after registration, before first resolution
$container->extend('mailer', function (Mailer $mailer, Container $c) {
$mailer->setDebug(true);
return $mailer;
});
Calling extend() after the service has already been resolved throws CannotExtendResolved.
Service Providers¶
Service providers encapsulate related service registrations into a dedicated class. They implement Phalcon\Contracts\Container\Service\Provider.
<?php
use Phalcon\Contracts\Container\Service\Collection;
use Phalcon\Contracts\Container\Service\Provider;
use Phalcon\Container\Resolver\Lazy\Lazy;
class DatabaseProvider implements Provider
{
public function provide(Collection $services): void
{
$services
->setParameter('db.host', Lazy::env('DB_HOST'))
->setParameter('db.port', Lazy::env('DB_PORT', 'int'))
->setParameter('db.name', Lazy::env('DB_NAME'));
$services->set('db', function (object $c) {
return new DatabaseConnection(
$c->get('db.host'),
$c->get('db.port'),
$c->get('db.name'),
);
});
}
}
ContainerFactory¶
Phalcon\Container\ContainerFactory bootstraps a fully configured Container from a set of Provider instances, keeping the Container constructor lean.
<?php
use Phalcon\Container\ContainerFactory;
$container = (new ContainerFactory())
->addProvider(new DatabaseProvider())
->addProvider(new MailProvider())
->addProvider(new CacheProvider())
->newContainer();
newContainer() creates a fresh Container, calls $provider->provide($container) on each registered provider in order, and returns the fully configured container.
Built-in Service Providers¶
Phalcon ships two ready-made providers that register the standard framework services, so a Container can stand in for Phalcon\Di\FactoryDefault without listing every service by hand.
| Provider | Registers |
|---|---|
Phalcon\Container\Provider\Web | The standard set of services that Phalcon\Di\FactoryDefault pre-registers for web (MVC) applications. |
Phalcon\Container\Provider\Cli | The standard set of services that Phalcon\Di\FactoryDefault\Cli pre-registers for CLI applications. |
<?php
use Phalcon\Container\ContainerFactory;
use Phalcon\Container\Provider\Web;
$container = (new ContainerFactory())
->addProvider(new Web())
->newContainer();
Using Container Without Di¶
Container can be used as the primary - or sole - dependency injection mechanism in your application. Phalcon\Di\Di is not required.
Standalone Bootstrap¶
Use ContainerFactory with service providers to build a fully configured container, then pass it directly to Phalcon\Mvc\Application in place of Di.
<?php
use Phalcon\Container\ContainerFactory;
use Phalcon\Mvc\Application;
use Phalcon\Mvc\View;
use Phalcon\Mvc\Url;
$container = (new ContainerFactory())
->addProvider(new RouterProvider())
->addProvider(new ViewProvider())
->addProvider(new DatabaseProvider())
->addProvider(new MailProvider())
->newContainer();
$application = new Application($container);
$response = $application->handle($_SERVER['REQUEST_URI']);
$response->send();
Registering as the Framework Default¶
Passing Container to Di::setDefault() makes it available to any component extending Phalcon\Di\Injectable - including controllers. Magic property access ($this->serviceName) resolves services from Container automatically.
<?php
use Phalcon\Container\ContainerFactory;
use Phalcon\Di\Di;
$container = (new ContainerFactory())
->addProvider(new AppServiceProvider())
->newContainer();
Di::setDefault($container);
With this in place, a controller accessing $this->logger or $this->db will resolve those services from Container rather than Di.
Service Provider Example for MVC Services¶
When using Container as the sole container, all services that Phalcon\Mvc\Application depends on must be registered - the same services that FactoryDefault would otherwise pre-register.
<?php
use Phalcon\Contracts\Container\Service\Collection;
use Phalcon\Contracts\Container\Service\Provider;
use Phalcon\Mvc\Router;
use Phalcon\Mvc\Dispatcher;
use Phalcon\Mvc\View;
use Phalcon\Mvc\Url;
use Phalcon\Http\Request;
use Phalcon\Http\Response;
class MvcProvider implements Provider
{
public function provide(Collection $services): void
{
$services->set('router', Router::class);
$services->set('dispatcher', Dispatcher::class);
$services->set('request', Request::class);
$services->set('response', Response::class);
$services->set('view', function (object $c) {
$view = new View();
$view->setViewsDir(APP_PATH . '/views/');
return $view;
});
$services->set('url', function (object $c) {
$url = new Url();
$url->setBaseUri('/');
return $url;
});
}
}
NOTE
FactoryDefault pre-registers all standard Phalcon services for convenience. When using Container directly, register only the services your application actually uses - which also means you start with no unnecessary overhead.
Instance Management¶
For advanced use cases - such as long-running process request boundaries - shared instances can be managed directly.
<?php
use Phalcon\Container\Container;
use Phalcon\Container\Definition\ServiceLifetime;
$container = new Container();
// Inject pre-built instances — setInstance() is fluent
$container
->setInstance('request', $requestObject, ServiceLifetime::SCOPED)
->setInstance('response', $responseObject, ServiceLifetime::SCOPED);
// Clear all SCOPED instances at request end (Octane / Swoole pattern)
$container->unsetInstances(ServiceLifetime::SCOPED);
// Remove a single cached instance
$container->unsetInstance('session');
Exceptions¶
Every exception thrown by Container lives under the Phalcon\Container\Exceptions namespace and extends Phalcon\Container\Exceptions\Exception, which implements the Phalcon\Container\Exceptions\ContainerThrowable interface. Catch the interface to handle any container failure, or catch a specific subclass to react to a single cause.
<?php
use Phalcon\Container\Container;
use Phalcon\Container\Exceptions\ContainerThrowable;
use Phalcon\Container\Exceptions\ServiceNotFound;
$container = new Container();
try {
$service = $container->get('unknown-service');
} catch (ServiceNotFound $ex) {
echo 'Service not found: ' . $ex->getMessage();
} catch (ContainerThrowable $ex) {
echo 'Container error: ' . $ex->getMessage();
}
| Exception | Thrown when |
|---|---|
ServiceNotFound | get() or new() is called for an unregistered, non-autowirable name |
ServiceNotRegistered | getService() is called for a name that was never registered |
InstanceNotFound | getInstance() finds no cached instance for the name |
ParameterNotFound | getParameter() finds no parameter for the name |
CircularAliasFound | An alias chain points back to itself (detected at registration) |
FrozenDefinition | A frozen definition is mutated (setLifetime(), addTag(), setArgument(), ...) |
CannotExtendResolved | extend() is called after the service has already been resolved |
CannotResolveParameter | Autowiring cannot resolve a constructor parameter |
NoProcessorFound | No processor matches a definition during resolution |
NoClassSet | A class-backed definition is resolved with no class set |
NoFactorySet | A factory-backed definition is resolved with no factory set |
InvalidExtender | An extender returns a non-object |
EnvNotDefined | Lazy::env() reads an environment variable that is not defined |