Phalcon DI Container as a Request-Scoped Service Boundary
Register Phalcon services lazily, separate shared from new instances, and validate the DI graph.
11 Oct 2025, 15:00 UTC

The problem: eager services and shared state
In a Phalcon application, it is easy to instantiate the database adapter, cache client, and logger as soon as the bootstrap file runs. That approach wastes memory on services a particular request never touches, and it lets a shared object hold mutable data longer than it should. The useful takeaway is to treat Phalcon's DI container as the single trust boundary for service creation: register each service once, resolve it lazily, use shared instances for stateless resources and new instances for request-specific state, and validate the dependency graph in tests before deployment.
Requirements
- Services must not be created unless a route actually resolves them.
- Stateless resources such as a database connection should behave as singletons within one HTTP request.
- Objects that carry request-specific data must receive a fresh instance on every resolve.
- The container graph must be testable without starting a web server.
Smallest suitable design
A minimal bootstrap creates a Phalcon DI container and registers closures, not objects. The closure runs only on the first $di->get('name') call, which is lazy loading.
<?php
use Phalcon\Di\FactoryDefault;
use Phalcon\Db\Adapter\Pdo\Mysql;
$di = new FactoryDefault();
$di->setShared(
'db',
function () {
return new Mysql([
'host' => '127.0.0.1',
'username' => 'app_user',
'password' => 'replace-with-secret',
'dbname' => 'app_db',
]);
}
);
$di->set(
'requestLogger',
function () {
return new RequestLogger(uniqid('req_', true));
}
);
setShared registers a singleton for the current request: every $di->get('db') returns the same object. set returns a new object on every resolve. A Phalcon DI container is the object that creates and holds these named services; lazy loading means the closure is not executed until the service is first requested.
Trust and data boundaries
PHP rebuilds the container for every HTTP request, so a shared service is request-scoped by default, not application-scoped. The boundary breaks when a shared service stores mutable data such as the current user ID or a form submission. Register that kind of object with set instead, or reset its state at the start of each request. In micro-application setups, the DI container is often the only runtime core, so the router, dispatcher, and explicitly loaded services define the attack surface.
Phalcon can tie container resolution to its event manager in some releases. Confirm the exact event names in your installed version before writing listeners that run before or after a service is created.
Operational checks
Run these from the project root in a terminal as a user with read access to the bootstrap file.
$ cd /path/to/project
$ php -l config/bootstrap.php
Expected output: No syntax errors detected. This catches typos but not circular dependencies, which surface only when a service is resolved.
$ vendor/bin/phpunit tests/DiGraphTest.php
Expected: all tests pass. A useful test resolves every registered service once and asserts that shared services return identical objects while non-shared services return distinct objects.
Risk: running the linter as a different user than the web server can hide permission errors that appear only in production.
Failure modes
- Circular dependency: service A depends on B, and B depends on A. PHP throws a fatal error at resolve time, not at registration time.
- Shared mutable state: a shared logger or repository keeps data from an earlier route in the same request.
- Missing service:
$di->get('missing')throws an exception; wrap resolution in a try/catch at framework boundaries if needed.
When this design should change
- Long-running workers under Swoole or ReactPHP: the container may persist across requests, making shared singletons effectively cross-request. Recreate the container per job or avoid shared state.
- Multi-tenancy: if each tenant needs a different database credential, register a factory that resolves the tenant from the request instead of a single shared connection.
- Compile-time validation: if you need dependency errors caught before deployment, Phalcon's runtime DI is not sufficient; consider an external container or generated factory code.
Limitations
The container is a PHP-level construct. It cannot validate constructor signatures or detect missing parameters until the closure executes. There is no rollback step because registering services changes only in-memory state for the current request; fixing a bad registration means editing the bootstrap and redeploying.
Practical verification
- Register a shared service and a non-shared service in the bootstrap.
- Call
$di->get('db')twice and compare with===; the result should be true. - Call
$di->get('requestLogger')twice and compare with!==; the result should be true. - Check memory with
memory_get_usage()before and after the first$di->get()to confirm the service was not instantiated at registration time.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.