Phalcon Dependency Injection Container: Architecture Note on Service Management and Trust Boundaries
Explains how Phalcon's DI container enables lazy loading, lifecycle control, and event integration while defining trust boundaries and failure conditions.
13 Jul 2026, 22:15 UTC

Requirements
Phalcon applications need a mechanism to instantiate services only when they are actually used, to control whether a service is shared across the request or recreated each time, and to allow cross‑cutting concerns (logging, security, decoration) to intervene during service creation. The solution must stay lightweight so that bootstrap time remains low and must keep explicit boundaries between components to avoid hidden coupling.
Smallest Suitable Design
The Phalcon Dependency Injection (DI) container satisfies these requirements with a single global singleton class (Phalcon\Di\Di) that stores service definitions as either class names or closures. Resolution occurs on first get() call, invoking the stored definition and caching the result when the service is marked as shared. The container is deliberately minimal: only the services required for a given request are defined in the bootstrap or module file, and no extra abstraction layers are introduced.
Example Bootstrap Definition
// public/index.php (run with php -S localhost:8000 -t public)
use Phalcon\Di\FactoryDefault;
use Phalcon\Mvc\Micro;
$di = new FactoryDefault();
// Define a lightweight logger service (fresh per request)
$di->setShared('logger', function () {
return new \Monolog\Logger('app');
});
// Define a database connection (shared for the request)
$di->set('db', function () {
return new \Phalcon\Db\Adapter\Pdo\Mysql([
'host' => '127.0.0.1',
'username' => 'root',
'password' => '',
'dbname' => 'test',
]);
});
$app = new Micro($di);
$app->get('/hello', function () use ($di) {
$logger = $di->get('logger'); // fresh each call
$db = $di->get('db'); // same instance throughout request
return 'OK';
});
$app->handle();
Place the file in the project’s public/ directory and run the built‑in PHP server. No special permissions are required beyond read access to the file and execute permission for the PHP binary.
Trust/Data Boundaries
Services should depend on abstractions (interfaces) rather than concrete classes. This keeps the DI container as a trust boundary: the container knows how to create an object, but the consumer only knows the contract it receives. By injecting interfaces, you prevent a service from reaching into the container’s internal registry and creating hidden dependencies.
Illustrative Interface Injection
// src/Service/OrderProcessor.php
namespace App\Service;
interface OrderProcessorInterface {
public function process(array $order): void;
}
class OrderProcessor implements OrderProcessorInterface {
private \Monolog\LoggerInterface $logger;
public function __construct(\Monolog\LoggerInterface $logger) {
$this->logger = $logger;
}
public function process(array $order): void {
$this->logger->info('Processing order', $order);
// … business logic …
}
}
// In bootstrap
$di->setShared('orderProcessor', function ($di) {
return new App\Service\OrderProcessor($di->get('logger'));
});
Here OrderProcessor trusts only the logger interface; the DI container supplies the concrete logger.
Operational Checks
To verify that the container behaves as expected, perform the following steps in a terminal with the project root as the working directory:
Start the built‑in server:
php -S localhost:8000 -t publicRequest the endpoint twice in separate tabs or using
curl:curl http://localhost:8000/hello curl http://localhost:8000/helloCheck the logs (if you configured a stream handler) to see whether a new logger instance appears each request (fresh) while the database connection logs only once per request (shared).
Expected observations:
- The
loggerservice returns a different object on eachget()call (fresh mode). - The
dbservice returns the identical object for all calls within the same request (shared mode). - If an event listener is attached (see below), it fires on every resolution.
Event Manager Integration Check
Attach a listener to the afterServiceResolve event to confirm the hook works:
$di->getEventsManager()->attach('afterServiceResolve', function ($event, $di, $name, $instance) {
error_log("Service '$name' resolved at " . date('c'));
});
After making a request, inspect the web server’s error log; you should see a line for each service resolved.
Failure Modes
Misusing the DI container can lead to the following problems:
- Over‑definition: Declaring many heavy services (e.g., large ORM objects) as shared increases memory footprint even if they are never used.
- Constructor injection of heavy dependencies: If a service’s constructor instantiates expensive collaborators eagerly, the lazy‑loading benefit is lost.
- Hidden coupling via the container: Accessing
$di->get()inside a service creates a dependency on the container itself, breaking the trust boundary and making unit testing harder. - Shared state leakage: Forgetting to reset mutable state in a shared service between requests can cause cross‑request contamination.
Conditions That Would Change the Design
The current DI‑centric design would be revisited if:
- The application shifts to a micro‑service architecture where each service is deployed independently and does not need a shared request‑scoped container.
- Performance profiling shows that closure‑based resolution adds measurable overhead compared to compile‑time dependency injection (e.g., using a container that generates factories).
- A new requirement demands hierarchical containers (e.g., per‑tenant DI scopes) that the current flat singleton cannot provide without significant refactoring.
Practical Verification Summary
To confirm that the DI container meets the architectural goals:
- Define a mix of shared and fresh services in
public/index.php. - Run the built‑in PHP server and hit an endpoint multiple times.
- Observe instance identity (shared vs fresh) via
spl_object_hash()or debugger. - Attach an
afterServiceResolvelistener and verify it logs for each resolution. - Ensure no service accesses
$di->get()internally; refactor any that do to receive dependencies via constructor or setter injection.
If all checks pass, the container is providing lazy loading, lifecycle control, event integration, and clear trust boundaries as intended.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.