Centralizing Services with Phalcon’s DI Container: A Practical Guide
Learn how to register shared services in Phalcon’s DI container, inject them into controllers, and avoid tight coupling as your application grows.
05 Sept 2026, 10:42 UTC

Problem: scattered service creation
As a Phalcon project grows, instantiating services directly inside controllers leads to duplicated configuration and tight coupling. Changing a database driver or logger implementation then requires edits in many places.
Thesis: use the built‑in DI container to centralize service definitions
By registering core services once in the container and retrieving them where needed, you improve testability, enforce loose coupling, and simplify configuration management.
Understanding the DI container
Phalcon’s Di component acts as a service locator. Services are registered with a name, a factory (closure or class name), and a sharing flag. By default, services are shared (singleton per request) and resolved lazily on first access.
Practical engineering decision: move common services into the container
Typical candidates are the database connection, logger, cache, and mailer. Registering them in a dedicated services.php file keeps bootstrap code clean and makes the dependencies explicit for anyone reading the controller.
Worked example: a shared Mailer service
First, define a simple mailer class:
// src/Mailer.php
namespace App;
class Mailer
{
public function send(string $to, string $subject, string $body): void
{
// In a real app you would use a transport library.
// For demonstration we just log the message.
error_log("Mail to {$to}: {$subject}\n{$body}");
}
}
Next, register it as a shared service:
// config/services.php
use Phalcon\Di\FactoryDefault;
use App\Mailer;
$di = new FactoryDefault();
$di->setShared(
'mailer',
function () {
return new Mailer();
}
);
return $di;
Finally, inject the mailer into a controller using Phalcon’s setter injection (the @Inject annotation):
// src/Controllers/NewsletterController.php
use Phalcon\Mvc\Controller;
use Phalcon\Di\Injectable;
class NewsletterController extends Controller
{
/**
* @Inject
*/
protected $mailer;
public function subscribeAction()
{
// $this->mailer is already set by the DI container
$this->mailer->send(
'user@example.com',
'Welcome',
'Thanks for subscribing!'
);
$this->view->setVar('message', 'Subscription processed');
}
}
The controller never creates a mailer instance; it receives the shared instance from the container. No constructor changes are required.
Trade‑off / limitation
While the DI container removes boilerplate, relying on $this->di->get() or annotations can hide dependencies, making static analysis harder. Explicit constructor injection makes those dependencies visible at compile time, but adds more code. Choose the approach that matches your team’s readability preferences.
Actionable closing
- Create a
config/services.phpfile and register your core services (db, logger, mailer, cache) as shared. - Enable setter injection in controllers by adding the
@Injectannotation to properties you want populated. - Run the example above, trigger the
subscribeActionroute, and verify that the mailer’ssendmethod is called (check your error log or use a mock mailer). - Inspect the container with
$this->di->getServices()to confirm the mailer appears as shared and is instantiated once per request.
By centralizing service definitions, you keep controllers focused on handling requests rather than wiring objects, making the codebase easier to maintain as it scales.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.