View Source

<h1>Zend\ServiceLocator RFC</h1>

<p>Currently, Zend\Di is being used as both a DI Container (DiC) as well as a Service Locator (SL) for sharing services (objects who are shared and are dependencies of other objects in other parts of an application). This presents a problem in that when ZF2 modules are written, an assumption that a DiC based Service Locator is available as opposed to simply a Service Locator. Since the minimally assumed interface is a DiC presenting itself as a SL, a module writer is making assumptions about the implementation details of a DiC being present in all applications.</p>

<h2>Some Concepts &amp; History</h2>

<p>DiC (Zend\Di), is a complex component. This is not to say this is true of Zend\Di specifically, but all Di Containers in general. DiC requires intimate knowledge of the set of classes that make up the code base from which instances are produced. They must know class structure including method signatures, interface hints, supertype/subtype relationships, which parameters represent dependencies and which parameters represent static or configuration based information and of those, which are required and which are optional. Any DiC solution that does not require a developer to write an excessive amount of &quot;wiring information&quot; is going to be less performant in PHP. If a DiC solution does require lots of &quot;wiring information&quot; then there is nothing to gain from the DiC solution itself, as you can simply write the wirings themselves.</p>

<p>DiC's are sexy solutions because they take information in the form of configuration and through a magical set of processes (to the casual observer/consumer), have the ability to produce instances on demand. It has the added benefit of storing these instances inside a registry for shared usage and also presents then in a lazy-loadable fashion. These concepts, in and of themselves are beneficial to application writers.</p>

<p>It is even more important to know where DiC solutions originated: in the .Net and Java world. In both of these platforms, there is a long-lived process where the DiC is statically stored and available to child-threads (web requests). The tradeoff is that however long it takes to build up the rich set of information/definitions required to be able to create instances on demand, this is paid up front on &quot;application startup time&quot;, as opposed to each thread paying the price of DiC startup.</p>

<p>In PHP, this DiC startup cost is paid on every request. And, in any DiC solution, the more information it is ultimately responsible for, the more of a startup cost said DiC solution will have in each and every request to the PHP application.</p>

<p>Also specific to PHP, configuration is not op-code cacheable in the same way that actual code is and fewer performance gains can be attained by throwing an op-code cache at an application that is entirely dependent on a configured DiC.</p>

<h2>Proposal for Zend\ServiceLocator</h2>

<p>To build a ServiceLocator component (as Zend\ServiceLocator), this is a standalone component- it has no hard dependencies on any other components in the Zend Framework. There exists one integration point for utilizing Zend\Di (DiC) as a resource for retrieving objects to present as services.</p>

<h3>Objectives and responsibilities:</h3>

<ul>
<li>Ability to alias services with different names</li>
<li>Ability to Lazy-Load services, and provide infrastructure to accomplish this</li>
<li>Ability to validate the types (classes) of services to their respective names</li>
<li>Ability to inject the ServiceLocator into services that require Locator awareness</li>
<li>Ability to OPTIONALLY pull objects from a DiC and present them as services
<ul>
<li>Ability for this Di based service to utilize a service locator (who's services are not part of a DiC to be used)</li>
</ul>
</li>
</ul>


<h3>Interfaces:</h3>

<h4>ServiceLocatorInterface:</h4>

<ul>
<li>A simple interface that allows for the 4 primary tasks:
<ul>
<li>setting services</li>
<li>retrieving services</li>
<li>checking for a service by name</li>
<li>creating aliases of existing services or aliases</li>
</ul>
</li>
</ul>


<p>This interface ensures that any one SL can easily be swapped out with another SL provided they implement this interface.</p>

<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
<?php
namespace Zend\ServiceLocator {

interface ServiceLocatorInterface
{

public function get($nameOrAlias);
public function set($name, $service);
public function has($nameOrAlias);
public function createAlias($alias, $nameOrAlias);
}

}
]]></ac:plain-text-body></ac:macro>

<ac:macro ac:name="note"><ac:rich-text-body>
<p>Important note on get(). By definition, a service is a self contained object. Regardless if it is lazy loaded, or provided to the service locator via set(), get() does not provide the ability to attain &quot;variations&quot; on purpose. Since services are shared, variations should exist by their own name (the aliasing) and should be declared up front, so that each consumer can say with certainty that a particular service (by-name) has the same stateful setup. Trying to pull a variation at runtime (as you can with Di) is discouraged when objects are viewed as &quot;services&quot; and locatable by a service locator.</p></ac:rich-text-body></ac:macro>

<h4>Other interfaces:</h4>

<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
<?php

namespace Zend\ServiceLocator {

interface LazyServiceInterface
{
public function createService(ServiceLocatorInterface $serviceLocator);
}

interface ServiceLocatorAwareInterface
{
public function setServiceLocator(ServiceLocatorInterface $serviceLocator);
}

}
]]></ac:plain-text-body></ac:macro>

<h4>Other facilities:</h4>

<h5>Validator</h5>

<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
<?php

namespace Zend\ServiceLocator {

class ServiceValidator
{
protected $serviceLocator = null;
protected $serviceToTypeMaps = array();
protected $serviceToTypeMapOwners = array();

public function __construct(ServiceLocatorInterface $serviceLocator)
{ }

public function addServiceToTypeMap(array $serviceToTypeMap, $owner = null)
{ }

public function validate($returnErrorArray = false)
{ }

}

}
]]></ac:plain-text-body></ac:macro>

<h5>For Di based Service Integration</h5>

<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
<?php

namespace Zend\ServiceLocator\Di {

use Zend\ServiceLocator\LazyServiceInterface,
Zend\ServiceLocator\ServiceLocatorInterface,
Zend\ServiceLocator\Exception,
Zend\Di\Di,
Zend\Di\Exception\ClassNotFoundException as DiClassNotFoundException;

class DiService extends Di implements LazyServiceInterface
{
const USE_SL_BEFORE_DI = 'before';
const USE_SL_AFTER_DI = 'after';
const USE_SL_NONE = 'none';

public function __construct(Di $di, $diInstanceName, array $diInstanceParameters = array(), $useServiceLocator = self::USE_SL_NONE)
{
// this is a proxy-object
}

public function createService(ServiceLocatorInterface $serviceLocator)
{
// method triggered at service creation time to do the creation
}

public function get($name, array $params = array(), $calledFromDiService = false)
{
// overrides from Zend\Di\Di to complete the proxy-loop
}

}

}
]]></ac:plain-text-body></ac:macro>

<h3>Examples</h3>

<h4>Basic setting/getting of service</h4>

<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
use Zend\ServiceLocator\ServiceLocator;
$sl = new ServiceLocator;
$sl->set('myservice', new \stdClass);
$myservice = $sl->get('myservice');
]]></ac:plain-text-body></ac:macro>

<h4>Basic setting of service, wrapped in closure for lazy-loading</h4>

<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
use Zend\ServiceLocator\ServiceLocator;
$sl = new ServiceLocator;
$sl->set('myservice', function () { return new \stdClass; });
$myservice = $sl->get('myservice'); // closure invoked here, replaces value of myservice internally
]]></ac:plain-text-body></ac:macro>

<h4>ServiceLocator awareness</h4>

<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
use Zend\ServiceLocator\ServiceLocator,
Zend\ServiceLocator\ServiceLocatorAwareInterface,
Zend\ServiceLocator\ServiceLocatorInterface;

class ServiceAwareService implements ServiceLocatorAwareInterface
{
public $sl = null;
public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
{
$this->sl = $serviceLocator;
}
}

$sl = new ServiceLocator;
$sl->set('myservice', new ServiceAwareService);
assert($sl === $sl->get('myservice')->sl);
]]></ac:plain-text-body></ac:macro>

<h5>A purely lazy service wrapper</h5>

<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
use Zend\ServiceLocator\LazyServiceInterface,
Zend\ServiceLocator\ServiceLocator;

class DbService implements LazyServiceInterface
{
protected $config = null;

public function __construct($config) {
$this->config = $config;
}

public function createService(ServiceLocator $sl) {
return new \Zend\Db\Adapter($this->config);
}
}

$sl = new ServiceLocator;
$sl->set('db', new \DbService($config));
assert($sl->get('db') instanceof \Zend\Db\Adapter); // true;
]]></ac:plain-text-body></ac:macro>


<h2>Proposal for Zend\Mvc &amp; Zend\Module integration</h2>

<p>Zend\ServiceLocator should become the &quot;first class citizen&quot; in the Zend\Mvc application infrastructure (as opposed to Zend\Di). What does that mean? It means that instead for forcing Zend\Di as the primary means of sharing services between parts of an application (local or 3rd party), that a more lightweight component (smaller footprint in both code as well as runtime implications) act as the primary interface for sharing these services. In terms of Zend\Mvc and Zend\Module, Zend\Mvc will have the well-known service location object composed in, and Zend\Module will provide features that allow application &amp; 3rd party modules to interact with this ServiceLocator.</p>

<h3>Changes in Zend\Mvc\Application:</h3>

<ul>
<li>Instead of composing Zend\Di, Zend\ServiceLocator will be composed (probably created by default)</li>
</ul>


<h3>Zend\Module will add Zend\Module\Feature\Service.</h3>

<ac:macro ac:name="note"><ac:rich-text-body>
<p>Suggetion: Zend\Module\Consumer renamed to Zend\Module\Feature as modules that interact with the service locator will not only consume application resources, but might also provide application resources (such as services). Since that is the case, I suggest renaming Consumer to Feature as it is more applicable as to the role of this aspect of Zend\Module.</p></ac:rich-text-body></ac:macro>

<ul>
<li>Ability to provide services and aliases from the Module class</li>
<li>Ability to provide a map of names to types for service validation
<ul>
<li>in order to use services by name, they should first pass type validation so that modules can be sure that objects of a particular service name are of a known type</li>
</ul>
</li>
</ul>



<h4>Interfaces:</h4>

<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
<?php

namespace Zend\Module\Feature\Service {

interface ServiceConsumer
{
/**
* Return an array for passing to Zend\Loader\AutoloaderFactory.
*
* @return ServiceValidatorMap
*/
public function getServiceValidatorMap();
}

interface ServiceProvider
{
/**
* Return an array for passing to Zend\Loader\AutoloaderFactory.
*
* @return ServiceCollection
*/
public function getServices();
}

}
]]></ac:plain-text-body></ac:macro>

<h4>Facilities (objects that are returned when using above interfaces):</h4>

<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
<?php

namespace Zend\Module\Feature\Service {



class ServiceCollection
{
protected $services = null;

public function __construct(array $services = array())
{
!$services ?: $this->setServices($services);
}

public function setServices(array $services)
{
foreach ($services as $name => $service) {
if (!is_string($name)) {
throw new \Exception('Service array key must be a string for the service name');
}
if (!is_object($service)) {
if (is_string($service) && !is_callable($service)) {
throw new \Exception('Service value must be an object, or something that can produce an object');
}
}
}
$this->services = $services;
}

public function getServices()
{
return $this->services;
}

}

class ServiceValidatorMap
{
protected $serviceValidatorMapArray = null;

public function __construct(array $serviceValidatorMapArray = array())
{
!$serviceValidatorMapArray ?: $this->setServiceTypeMap($serviceValidatorMapArray);
}

public function setServiceTypeMap(array $serviceValidatorMapArray)
{
foreach ($serviceValidatorMapArray as $name => $type) {
if (!is_string($name)) {
throw new \Exception('Service array key must be a string for the service name');
}
if (!is_string($type) && !class_exists($type)) {
throw new \Exception('Service type must be a valid class name.');
}
}
$this->serviceValidatorMapArray = $serviceValidatorMapArray;
}

public function getServiceTypeMapArray()
{
return $this->serviceValidatorMapArray;
}

}
}
]]></ac:plain-text-body></ac:macro>

<h4>Examples: Usage Inside Modules</h4>

<p>A Module class example.</p>

<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
<?php


namespace MyModule {

use Zend\Module\Feature\Service as ServiceFeature,
Zend\ServiceLocator;

class Module implements ServiceFeature\ServiceProvider, ServiceFeature\ServiceConsumer {

/* application configuration */
protected $config;


/** required by ServiceConsumer */
public function getServiceValidatorMap() {
return new Service\ServiceValidatorMap(
'doctrine' => 'Spiffyjr\Doctrine',
'edpuser' => 'EdpUser\EdpUser',
'mailer' => 'Zend\Mail\Transport',
$this->config['service_names']['db'] => 'Zend\Db\Adapter' // can validate config provided name to type
);
}

/** required by ServiceProvider */
public function getServices()
{
return new Service\ServiceCollection(
// services
array(

// lazyload via closure
'myservice' => function() {
return new MyModule\Services\MyService;
},

// lazy-load via closure that is service-locator aware
'myconsumingservice' => function(ServiceLocator $sl) {
return new MyModule\Services\MyConsumingService($sl->get('my-doctrine'));
},

// expose a di instance as a service
'mymailer' => new ServiceLocator\Di\DiService($this->getDi(), 'mymailer'),

// simply assign a LazyService instance to the locator
'someotherservice' => new MyModule\Service\SomeOtherService, // implements LazyServiceInterface

// variation of a service
// the MyDoctrineService has a createService() method that will be called
// when 'mydoctrine' is retrieved. What this will do is create (if necessary)
// a variation of the original doctrine service
'mydoctrine' => new MyModule\Service\MyDoctrineService,
),
// aliases
array(
// hard wired names
'my-doctrine' => 'doctrine'

// use configured name (local to app) for the service alias
// now all usage of my-db is guaranteed to point to correct service
'my-db' => $config['service_names']['db']
)
);
}

}
}
]]></ac:plain-text-body></ac:macro>

<p>Utilization inside a controller:</p>

<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[

<?php
namespace MyModule\Controller {

class MyController implements ServiceLocatorAwareInterface
{
protected $serviceLocator = null;
public function setServiceLocator(ServiceLocatorInterface $serviceLocator) {
$this->serviceLocator = $serviceLocator;
}

public function doAction()
{
// the hardwired doctrine one
$mydoctrine = $this->serviceLocator->get('mydoctrine');


// the dynamically aliased db one
$mydb = $this->serviceLocator->get('my-db'); // no need to use a configuration value
}

}

}
]]></ac:plain-text-body></ac:macro>

<h4>Questions and Answers</h4>

<h5>Question</h5>
<p>How do I get configuration into the various services?</p>

<p>As an example, in development, I might use a &quot;File&quot; mail transport, and need to specify the path to which to write files, but in production, I might configure an &quot;SMTP&quot; mail transport, and need to pass in SMTP-Auth credentials. Additionally, it's typically easiest to aggregate all application configuration at once, versus piecemeal, by service. How can I pass application configuration to the Service Locator, and then get at that configuration when creating my services?</p>

<h5>Answer</h5>

<p>First, it is important to know that configuration itself can be a service, after all,<br />
we treat it as a &quot;dependency&quot;.</p>

<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
<?php

$config = include 'application/config/application.php';
$serviceLocator->set('configuration', $config);

// then a consumer:
$serviceLocator->set('dbAdapter', function (ServiceLocator $serviceLocator) {
return new Zend\Db\Adapter\Adapter($serviceLocator->get('configuration')['db']) // 5.4 syntax for brevity
});

// now:
$dbAdapter = $serviceLocator->get('dbAdapter');

]]></ac:plain-text-body></ac:macro>

<p>Taking this a step further:</p>

<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
<?php

// this is probably inside config/autoload/application.global.php, or .local in production
return array(
'mailer' => array(
'name' => 'localhost.localdomain',
'host' => '127.0.0.1',
'connection_class' => 'login',
'connection_config' => array(
'username' => 'fooey',
'password' => '12barbar90!',
),
)
);

// this is probably inside config/autoload/application.local.php for development
return array(
'mailer' => array(
'path' => __DIR__ . '/data/mail/'
)
);

// this is probably inside config/autoload/service.global.php
$services = array(
'mailer' => function () {
return new Zend\Mail\Transport\Sendmail();
}
);

// this is probably inside config/autoload/service.local.php in dev environment
return array(
'mailer' => function ($serviceLocator) {
return new Zend\Mail\Transport\File(
$serviceLocator->get('configuration')['mailer']
);
}
);

$serviceLocator = new Zend\ServiceLocator\ServiceLocator($services[$env]);
$serviceLocator->set('configuration', $configs[$env]);

$mailer = $serviceLocator->get('mailer');
$mailer->send($message);

]]></ac:plain-text-body></ac:macro>

<p>Taking this example even further, we can demonstrate that services don't have<br />
to be Closure objects. They can be &quot;Service objects&quot;.</p>

<p>Service Objects exist as a way to lazily create services as needed. They should<br />
remain extremely lightweight.</p>

<p>These Service Objects can be hand coded and shipped, or generated by something like<br />
Zend\Di\Introspection\ServiceGenerator:</p>

<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
<?php

// in config/autoload/service.global.php
return array(
'mailer' => new Application\Service\Mailer('mailer', 'sendmail');
);

// then, inside src/Application/Service/Mailer.php
namespace Application\Service {
use Zend\ServiceLocator\LazyServiceInterface,
Zend\ServiceLocator\ServiceLocator,
Zend\Mail\Transport;

class Mailer implements LazyServiceInterface {
protected $configKey = null;
protected $type = 'sendmail';
public function __construct($configKey, $type = 'sendmail') {
$this->configKey = $configKey;
$this->type = $type;
}
public function createService(ServiceLocator $serviceLocator) {
$config = $serviceLocator->get('configuration')[$this->configKey];
if ($type == 'file') {
return new Transport\File($config);
} else {
return new Sendmail($config);
}
}
}
}
]]></ac:plain-text-body></ac:macro>

<h5>Question</h5>
<p>Can I mix DI and Service Location?</p>

<p>I might want to fallback to DI, or pull dependencies out of DI. Some of those may have dependencies I've defined in my Service Locator, however, while others may not. How can I handle such a mixed strategy? Related: how can I get DI configuration into the DI container when used in this fashion?</p>

<h5>Answer</h5>
<p>The DiC instance is something separate from the Service Locator. To continue to use a DiC with the service locator, you would use a Di based Service:</p>

<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
<?php

$serviceLocator->set('instancename', $diService($zendDiInstance, 'instancename');

]]></ac:plain-text-body></ac:macro>


<h5>Question</h5>

<p>What about controllers?</p>

<p>Currently, controllers are pulled from the locator, with the idea of emphasizing dependency injection over service location for gaining access to dependencies. However, with the DIC, we can often omit configuration for individual controllers when they have no dependencies or when dependencies can by auto-discovered. Will this approach be possible with a Service Locator, possibly by having a DI-aware Locator? If so, how?</p>

<h5>Answer</h5>

<p>Controllers are, as we've seen, a special case.</p>

<p>We want all the benefits of injecting services and DI for our controllers, but we don't<br />
necessarily want to expose our controllers as &quot;services&quot; application wide.</p>

<p>The answer to this is scoped containers or, scoped Service Locators.</p>

<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
<?php

$dispatcherSL = $serviceLocator->createScopedLocator('dispatcher');
// configure as normal
// use as normal:
// * services in parent are available to child container
// * services in child container can only be pulled from child container

$dispatcherSL->set('IndexController', new ServiceClass('SomeNs\Controller\IndexController'));
$controller = $dispatcherSL->get('IndexController');
$controller->indexAction();

]]></ac:plain-text-body></ac:macro>


<h5>Question</h5>

<p>My module may create artifacts based on calculations that I want available elsewhere in my application; how can I get these into the service locator? How would I access them elsewhere? (This one should show both injecting such artifacts, as well as pulling them from a locator.)</p>

<h5>Answer</h5>

<p>It's important to remember, just about any object or array can be a service:</p>

<ac:macro ac:name="code"><ac:plain-text-body><![CDATA[
<?php

// as easy as:
$serviceLocator->set('mycool_calculations', $calculations);

// OR, from inside a module:
namespace MyCoolModule {
class Module implements ServiceProvider {
public function getServices() {
return array(
'mycool_calculations' => MyCalculator::getCalculations() // returns an array
);
}
}
}

// elsewhere in some code
$calculationArray = $serviceLocator->get('mycool_calculations');

]]></ac:plain-text-body></ac:macro>

<h5>Question</h5>

<p>Can the Service Locator return different instances of a service? As an example, EventManager instances should be discrete per-class, but each should be injected with a shared SharedEventManager instance. Can this be done with the Service Locator?</p>

<h5>Answer</h5>

<p>It is important to remember Service Locators do not instantiate objects for you, it is up to the<br />
Service Object (or Closure), to instantiate the object. The Service Locator itself does not care<br />
how a particular service is created or how (if at all) its dependencies are injected.</p>

<p>That said, it might make sense to consider moving the &quot;shared&quot; support from DI to the ServiceLocator.</p>

<h5>Question</h5>

<p>How can a module define services for the application's service locator instance? (This is in the RFC already.)</p>

<h5>Answer</h5>

<p>See the above section.</p>