Phalcon DI Shared vs Non-Shared Services: A Diagnostic Guide
A diagnostic guide for Phalcon 4.x/5.x dependency injection issues caused by non-shared services, with ordered checks, fixes, and escalation criteria.
18 Sept 2026, 16:46 UTC

The Symptom: Fresh Objects Where You Expected One
When a Phalcon application calls $di->get('db') twice and receives two different adapter objects, or when a session service appears to forget data between calls, the root cause is often the container's default resolution mode. In Phalcon\Di\Di, services are non-shared by default: every get() call constructs a fresh instance unless the registration explicitly marks the service as shared. The useful takeaway is that singleton-like behavior must be declared, not assumed.
A dependency injection (DI) container is a registry that stores service definitions—objects, closures, or class names—and resolves them by name. A shared service is created on first access and cached for the lifetime of that container. A non-shared service acts as a factory, returning a new object each time it is requested.
Cause and Diagnostic Table
| Observable behavior | Probable cause | Diagnostic signal |
|---|---|---|
get('db') returns a different object on each call | Service registered without the shared flag | $di->get('db') === $di->get('db') evaluates to false |
| Session or config data appears lost between calls | Stateful adapter resolved as non-shared | Same identity check returns false for the adapter name |
| Closure throws an error resolving dependencies | Closure signature omits the injected container | Definition looks like function () { ... } instead of function ($di) { ... } |
Phalcon\Di\Di::getDefault() returns null | setDefault() not invoked during bootstrap | Global accessor returns null or an unexpected container |
Ordered Checks
1. Confirm the Phalcon Extension Is Active
Run the following in your terminal or deployment shell. No special permissions are required beyond read access to the PHP binary.
php -m | grep phalcon
Expected check: the output lists phalcon. If the extension is absent, none of the framework classes exist and DI diagnostics cannot proceed.
2. Audit Service Registrations in Bootstrap
Open your application bootstrap file, commonly located at /path/to/app/bootstrap.php or a services provider file. Search for $di->set( and $di->setShared(. Look for registrations that lack the shared flag where singleton behavior is expected, and note the exact string case of each service name—'db' and 'DB' are treated as separate registrations.
3. Run an Identity Check Script
Create a temporary script at /path/to/app/di_check.php to compare object identity. Replace /path/to/vendor/autoload.php with your actual autoloader path.
<?php
require '/path/to/vendor/autoload.php';
$di = new Phalcon\Di\Di();
// Non-shared registration
$di->set('service', function () {
return new stdClass();
});
$a = $di->get('service');
$b = $di->get('service');
var_dump($a === $b);
// Shared registration
$di->setShared('shared_service', function () {
return new stdClass();
});
$c = $di->get('shared_service');
$d = $di->get('shared_service');
var_dump($c === $d);
Run it from the terminal:
php /path/to/app/di_check.php
Expected check: the first var_dump should return bool(false); the second should return bool(true). Risk: this creates a temporary file in your application directory; delete it after verification.
4. Verify the Default Container
Temporarily append the following line to your bootstrap after container construction, then reload the application or run a CLI entry point:
var_dump(Phalcon\Di\Di::getDefault());
Expected check: the output is an instance of Phalcon\Di\Di. Remove the line after confirming.
Fixes Tied to Diagnostic Findings
- Identity check returns
falsefor a service that should be a singleton. Change$di->set('db', $closure);to$di->setShared('db', $closure);or pass the shared flag as the third argument:$di->set('db', $closure, true);. Verify the exact method signature against your installed major version, because Phalcon 3.x differs from 4.x and 5.x. - Closure cannot resolve dependencies. The container is passed as the first argument to the closure. Change
function () { return new Mailer(); }tofunction ($di) { return new Mailer($di->get('config')); }. Relying on outer scope instead of the injected container causes resolution failures. getDefault()returnsnull. Call$di->setDefault();immediately after constructing the container and before any code attempts global access.- Case-sensitivity mismatch. Standardize service name strings across all
set(),setShared(), andget()calls. The container does not normalize case.
Escalation Criteria
Escalate beyond simple registration changes when any of the following conditions hold:
- State leaks across logical requests in a long-running worker (for example, under Swoole or RoadRunner). A shared service caches for the container's lifetime; if that container persists beyond a single request, mutable objects such as the HTTP request or response can leak state. The fix is architectural: reset mutable shared services explicitly or scope the container per job.
- The extension is loaded but class signatures differ from documentation. Run
php --ri phalconto inspect the installed version. Phalcon 5.x targets PHP 7.4 and newer, while older 3.x releases may use different DI method signatures. Consult the migration guide for that major version before refactoring. - Multiple containers are active. If
getDefault()returns a container different from the one populated in bootstrap, audit for accidentalnew Phalcon\Di\Di()calls that bypass the default instance.
Limitations and Version Assumptions
Shared services are cached for the container's lifetime, not for a calendar request. Under traditional PHP-FPM that lifetime is one request; under a worker model it may span many requests. Marking a service shared also does not make it globally unique across multiple container instances. Finally, the === identity check confirms object identity, not functional correctness—a shared database adapter can still hold stale connections.
Practical Verification
After applying fixes, re-run the identity script. For services intended to be singletons, $di->get('name') === $di->get('name') must return true. For intentionally non-shared factories, it must return false. As an application-level check, register a service that increments an internal counter when called; if the service is shared, the counter persists across get() calls within the same container lifetime. If it resets, the service is still resolving as non-shared.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.