Phalcon DI Container: Architecture Note on Service Provisioning and Trust Boundaries
Explains how Phalcon’s DI container establishes trust boundaries, enables lazy loading, and what failure modes to watch for when designing service provisioning.
03 Apr 2026, 11:31 UTC

Requirements
When building a Phalcon application you need a central place to:
- Provide services (e.g., database adapters, loggers, cache) to controllers and models.
- Control whether a service is shared (singleton) or created fresh on each request.
- Allow configuration changes per environment without touching business logic.
The DI container must also respect trust boundaries: application code should not be able to inadvertently corrupt shared state, and external dependencies should be isolated from the framework’s internal mechanics.
Smallest Suitable Design
The minimal DI setup registers only the services that are actually used, using lazy‑loaded factories. This keeps memory footprint low and speeds bootstrap.
// public/index.php (run with web server user, needs read/execute on app files)
use Phalcon\Di\FactoryDefault;
$di = new FactoryDefault();
// Register a logger as a shared service – instantiated on first access
$di->setShared('logger', function () {
return new \Monolog\Logger('app');
});
// Register a request‑scoped service – new instance each get
$di->set('request', function () {
return new \Phalcon\Http\Request();
});
// Make the DI globally available (Phalcon convention)
Phalcon\Di::setDefault($di);
Only the logger and request services are defined; no unnecessary objects are created at startup.
Trust and Data Boundaries
Shared services act as singletons within a single request lifecycle. The container guarantees that:
- Code retrieving a shared service receives the exact same instance (
===comparison). - If a service’s factory throws an exception, the exception propagates to the caller; the container does not cache a broken instance.
This establishes a clear trust boundary: application logic can rely on the returned object being valid, while the DI remains responsible for creation and lifetime.
Operational Checks
Lazy Loading Verification
To confirm that a shared service is instantiated on first access, retrieve it twice and compare identities:
// In a controller or service (same permissions as above)
$logger1 = $di->getShared('logger');
$logger2 = $di->getShared('logger');
if ($logger1 === $logger2) {
// true → singleton behavior confirmed
}
Risk: if the factory has side‑effects (e.g., opens a file), those side‑effects occur only on the first call.
Runtime Override (Feature Toggle)
You can replace a service at any point before it is requested:
// Override the logger with a mock for testing
$di->set('logger', new \Mock\Logger());
$testLogger = $di->getShared('logger'); // returns the mock instance
Required permission: write access to the DI container (i.e., ability to edit the bootstrap or a service provider).
Check: retrieve the service after override and assert it is not the original instance.
Failure Modes
If a shared service’s factory throws an exception:
- The exception bubbles up to the caller.
- The DI does **not** cache the failed instance; a subsequent
getSharedwill invoke the factory again. - If the failure is due to a misconfigured resource (e.g., DB credentials), repeated requests will keep failing until the configuration is corrected.
To guard against leaving the container in an inconsistent state, wrap service retrieval in try/catch when the failure could be transient:
try {
$db = $di->getShared('db');
} catch (\Throwable $e) {
// log, fallback, or degrade gracefully
}
When the Design Would Change
The built‑in DI assumes a single‑process, shared‑nothing request model. You would reconsider it if:
- Your application runs in a distributed environment where a true singleton must be shared across nodes (e.g., a global cache). In that case, replace the Phalcon DI with an external service discovery or a dedicated shared‑state store.
- You need strict interface‑based contracts to improve testability; then you would register services against interfaces and rely on dependency injection frameworks that enforce those contracts.
- Service lifecycle becomes more complex (e.g., long‑running workers, queues) and you require per‑worker or per‑job scopes beyond request‑based lazy loading.
In such scenarios, you might keep the Phalcon DI for request‑scoped services but delegate cross‑process concerns to an external container or service mesh.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.