Pop PHP
The Basics

Services

The service locator is a registry of names, each paired with a description of how to build one object. Nothing is built until something asks, and once built it is held for the rest of the request. There is no dependency injection behind it — no constructor signature is read and nothing is auto-wired.

Registering a Service#

Two places register services, and they hold the same kind of value. The services key of a config array is data, so it belongs in a config file:

PHPapp/config/app.http.php
<?php

return [
    'services' => [
        'session' => ['call' => 'Pop\Session\Session::getInstance'],
        'notifier' => [
            'call'   => 'App\Service\Notifier',
            'params' => ['channel' => 'email']
        ]
    ]
];

setService() does the same thing imperatively, and is what an application class's load() uses when the definition needs a value only the running application has:

PHP
$app = new Pop\Application();

$app->setService('logger', [
    'call'   => 'Pop\Log\Logger',
    'params' => ['writer' => new Pop\Log\Writer\File(__DIR__ . '/app.log')]
]);

Both end up in the same Pop\Service\Locator. Applications & Bootstrap covers where load() sits in the bootstrap sequence; this page is about what goes into the definition and what comes back out.

Define a Service#

A service is defined by anything Pop\Utils\CallableObject can call, given either as a bare value or as an array with a call key and an optional params key. These are what the locator understands:

Definition What getService() returns
'App\Service\Notifier' new App\Service\Notifier()
'new App\Service\Notifier' the same — the new prefix is accepted and ignored
'App\Service\Notifier::build' the return value of the static method
'App\Service\Notifier->send' constructs the class, then returns the return value of send()
'my_factory_function' the return value of the named function
function() { ... } the closure's return value
['call' => <any of the above>, 'params' => [...]] the same, with params passed to whichever call it is

Any other PHP callable — [$object, 'method'], an object with __invoke() — works too, but has to go in the call key rather than being passed bare. A bare array is read as the call/params shape, so setService('notifier', [$object, 'send']) throws Pop\Service\Exception: Error: A callable service was not passed; ['call' => [$object, 'send']] is the form that works.

params is an array, and its keys matter. A string key becomes a PHP named argument, which is why ['writer' => ...] above lands on Logger::__construct()'s $writer parameter regardless of position; a numeric-keyed array is passed positionally. The two cannot be mixed — a named parameter followed by a positional one throws Error: Cannot use positional argument after named argument when the service resolves.

PHP
$app = new Pop\Application();

$app->setService('session', ['call' => 'Pop\Session\Session::getInstance']);
$app->setService('cookie', 'Pop\Cookie\Cookie::getInstance');
$app->setService('uploads', [
    'call'   => 'Pop\Http\Server\Upload',
    'params' => ['dir' => __DIR__ . '/uploads', 'maxSize' => 1000000]
]);

echo get_class($app->getService('session'));   // Pop\Session\Session

Lazy Construction#

A definition is stored, not run. Nothing in setService() checks that the class exists, that the method is real or that the parameters fit — the first getService() for a name does all of that, and every later call for the same name returns the object the first call produced.

PHP
$app = new Pop\Application();

$app->setService('now', function() { return new DateTime(); });

var_dump($app->services()->isLoaded('now'));    // false — registered, never built

$first = $app->getService('now');

var_dump($app->services()->isLoaded('now'));    // true
var_dump($app->getService('now') === $first);   // true — the same object
var_dump($app->services()->reload('now') === $first);   // false — rebuilt

Laziness is the reason a service is the right home for anything expensive: a database connection, an HTTP client, a mail transport. Register all of them and a request that touches none pays for none.

It's also why a typo in a definition surfaces late and somewhere unhelpful. A name that is not a class, a method that does not exist on one, a params array that does not fit the constructor — all of it throws out of the first getService(), not out of setService():

PHP
$app = new Pop\Application();

$app->setService('broken', 'App\Service\Notfier');   // typo, accepted silently

$app->getService('broken');
// Pop\Utils\Exception: Error: Unable to prepare the callable object for execution.

A misspelled method is clearer — 'App\Service\Notifier->snd' throws Error: The method 'snd' does not exist in the class 'App\Service\Notifier'. — but both come from the same place, on first use.

Two services that each resolve the other throw Pop\Service\Exception naming both. The repeated pair in the message is the detection depth, not a measure of the problem.

Retrieving a Service#

getService() on the application is the ordinary way in, and it throws for a name that was never registered:

PHP
$app = new Pop\Application();

$app->setService('logger', ['call' => 'Pop\Log\Logger']);

$app->getService('logger');

$app->getService('nope');
// Pop\Service\NotFoundException: Error: The service 'nope' has not been added to the service locator

services()->isAvailable('nope') answers the same question without throwing, and is the guard for an optional service. The locator also implements PSR-11, so has() is a synonym for isAvailable(), get() for the locator's own get(), and the two exception types satisfy Psr\Container\NotFoundExceptionInterface and Psr\Container\ContainerExceptionInterface — a Pop\Service\Locator can be handed to any library that type-hints Psr\Container\ContainerInterface.

PHP
$app = new Pop\Application();

$app->setService('logger', ['call' => 'Pop\Log\Logger']);

var_dump($app->services()->isAvailable('logger'));    // true
var_dump($app->services()->has('nope'));              // false

$app->removeService('logger');
var_dump($app->services()->isAvailable('logger'));    // false

The locator supports property and array access as shorthands for the same two calls — $app->services['logger'] reads, $app->services()->logger reads, and both isset($app->services()->logger) and isset($app->services['logger']) are isAvailable(). Inside a controller the route is $this->application()->getService('logger'), which Controllers covers along with why dependencies here are pulled rather than pushed. Outside both, Pop\App::services('logger') reaches the application registered during bootstrap.

Binding Parameters Yourself#

The locator wraps every definition in a Pop\Utils\CallableObject, and Locator::set() deliberately keeps a CallableObject you built yourself instead of wrapping it again. That matters when the parameters are easier to assemble in steps than to write as one array literal:

PHP
$app = new Pop\Application();

$notifier = new Pop\Utils\CallableObject('App\Service\Notifier');
$notifier->addNamedParameter('channel', 'email');
$notifier->addNamedParameter('from', 'noreply@example.com');

$app->setService('notifier', $notifier);

The same object is reachable afterward through services()->getCallable('notifier') and services()->getParameters('notifier'), and setParameters(), addParameter() and removeParameters() on the locator edit a definition in place:

PHP
$app = new Pop\Application();

$app->setService('notifier', [
    'call'   => 'App\Service\Notifier',
    'params' => ['channel' => 'email']
]);

$app->getService('notifier');

$app->services()->setParameters('notifier', ['channel' => 'sms']);

$app->services()->reload('notifier');

A definition edited after the service has been resolved has no effect on the resolved object — reload() is what picks the change up, which is what the last line is for.

One behavior of CallableObject is worth knowing before you put a closure in params: a parameter that is itself callable is invoked, and its return value is what gets passed. That's a lazy dependency, and it runs when the service is first resolved rather than when it is registered:

PHP
$app = new Pop\Application();

$app->setService('notifier', [
    'call'   => 'App\Service\Notifier',
    'params' => ['logger' => function() { return new Pop\Log\Logger(); }]
]);

App\Service\Notifier receives a Pop\Log\Logger, not the closure. A parameter that genuinely is meant to be a callback has to be wrapped — in an object with an __invoke(), or in a single-element array — or it will be called and its result passed instead.

The Service Container#

Pop\Service\Container is a static registry of locators, for code that has no application object to ask. The first Pop\Service\Locator constructed registers itself under 'default', which is the one the application built during bootstrap:

PHP
use Pop\Service\Container;

$app = new Pop\Application();

$app->setService('logger', ['call' => 'Pop\Log\Logger']);

var_dump(Container::has('default'));

echo get_class(Container::get('default')->get('logger'));   // Pop\Log\Logger

Container::get() with no argument is the same as Container::get('default'), and an unknown name throws Pop\Service\Exception: Error: The service locator 'reporting' has not been added. Additional locators are registered by name, which is how a subsystem gets a namespace of its own:

PHP
$reporting = new Pop\Service\Locator(['clock' => function() { return new DateTime(); }], false);

Pop\Service\Container::set('reporting', $reporting);

echo get_class(Pop\Service\Container::get('reporting')->get('clock'));   // DateTime

The false second argument is what keeps that locator out of the 'default' slot when nothing has claimed it yet.

Prefer $app->getService() or App::services() wherever an application is in reach, and keep Container for what it exists for: static code with no handle on one. 'default' is claimed by the first locator constructed, where Pop\App::get() returns the most recent application.

The locator exposes more than this page covers — per-parameter editing, and the Countable/ArrayAccess surface it inherits from the manager base class — see the popphp README.

See Also#