Using Zend Framework Service Manager to Wire a Custom Logger Service
Learn how Zend Framework's Service Manager centralizes service definition, enables lazy loading, and simplifies retrieval—while understanding its testing trade‑offs.
14 Jun 2026, 06:50 UTC

Problem: Services scattered across modules cause duplicated instantiation
When building a PHP application with Zend Framework (or its successor Laminas), it is common to create helper classes—loggers, mailers, API clients—in many places. Each module may instantiate its own copy, leading to wasted memory and hidden coupling.
Thesis: Centralize service definition with the Service Manager
The Zend Framework Service Manager implements a configurable service locator that lets you declare how a service is built, retrieve it on demand, and benefit from lazy loading. This keeps construction logic in one place and makes dependencies explicit when you ask for a service.
How the Service Manager works
The container stores definitions such as factories (callbacks that return an object), invokables (classes with no constructor arguments), aliases (alternative names), and abstract factories (fallback resolvers). By default, a service is instantiated only the first time it is requested, which reduces startup overhead.
Defining a logger service
Assume a simple logger class:
namespace App\Service;
class Logger
{
public function log(string $message): void
{
error_log(sprintf('[%s] %s', date('c'), $message));
}
}
To make it available through the Service Manager, add a factory to config/autoload/global.php (or a module‑specific config file):
return [
'service_manager' => [
'factories' => [
App\Service\Logger::class => function ($container) {
return new App\Service\Logger();
},
],
],
];
The key is the fully‑qualified class name; the factory receives the container and can inject other services if needed.
Retrieving the logger in a controller
In any controller that has access to the service locator (e.g., via $this->getServiceLocator() in Laminas MVC), request the logger:
public function logAction()
{
$logger = $this->getServiceLocator()->get(App\Service\Logger::class);
$logger->log('User visited the log page');
return new ViewModel(['status' => 'logged']);
}
The Service Manager calls the factory the first time get() is invoked and returns the same instance on subsequent calls within the same request.
Trade‑off: hidden dependencies
While the Service Manager removes boilerplate, retrieving a service with get() obscures what a controller actually needs. This can make unit testing harder because you must mock the container or rely on concrete implementations. A common mitigation is to inject dependencies via constructor factories, keeping the locator usage limited to configuration wiring.
Practical verification
- Create a minimal Laminas MVC skeleton:
composer create-project laminas/laminas-mvc-skeleton demo(run in a directory where you have write access; no special privileges required). - Add the Logger class under
demo/src/App/Service/Logger.phpwith the code shown above. - Insert the factory configuration into
demo/config/autoload/global.phpas demonstrated. - Add the
logActionmethod to an existing controller (e.g.,IndexController) and route to it. - Request the action in a browser or via
curl http://localhost/demo/public/index/logand check your PHP error log for a line containing[timestamp] User visited the log page.
If the log line appears, the Service Manager successfully instantiated and supplied the logger. If you see a “Service not found” exception, double‑check the factory key matches the class name exactly and that the module’s configuration is loaded.
Actionable closing
Start by moving one frequently used helper into the Service Manager, verify lazy loading with a simple log, then gradually replace direct new calls with container retrieval. Keep the locator usage confined to configuration factories; prefer constructor injection wherever possible to retain explicit dependencies and testability.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.