Service Manager

Introduction

The component are distributed under the "Poirot/ServiceManager" namespace and has a git repository, and, for the php components, a light autoloader and composer support.

All Poirot components following the PSR-4 convention for namespace related to directory structure and so, can be loaded using any modern framework autoloader. All packages are well tested and using modern coding standards.

Service Manager is a container that stores services or components (classes) and intended to simplify dependency injection and object construction in your application. Martin Fowler's article has well explained why DI container is useful.

// services.php

return [
    'implementations' => [
        'timezone' => \DateTimeZone::class,
        'serverTime' => \DateTimeImmutable::class,
    ],
    'services' => [
        'config' => [
            'local_time_zone' => 'Europe/Helsinki',
        ],
        'timezone' => factory(function(iServicesContainer $container) {
            return new \DateTimeZone($container->get('config')['local_time_zone']);
        }),
        'serverTime' => instance(\DateTimeImmutable::class, [
            'time' => 'now',
            'timezone' => get('timezone'),
        ])->setSharable(false),
    ],
];
$builder = new Builder(Builder::from(__DIR__ . '/services.php'));
$serviceManager = new Container($builder);

$format = 'Y-m-d H:i:s';
print $serviceManager->get('serverTime')->format($format);
sleep(3);
print $serviceManager->get('serverTime')->format($format);

// 2020-06-06 22:54:38
// 2020-06-06 22:54:41

Services Container

The Service Manager Container can stores services or components (classes), these services can be attained from container during script execution in application which could get benefit from the container provided features in many different ways to setup how the requested service should be created on request.

It can be used to managing classes dependencies and performing dependency injection, configuring objects, defining singleton like objects, lazy loading heavy construction objects and more ...

Service is any object extending iService interface which can result on creating any result type on request the service from container.

interface iService
{
    /**
     * Create Expected Service
     *
     * @return mixed
     */
    function createService();

    /**
     * The service which get cached and on each request
     * will not be created
     *
     * @return bool
     */
    function isSharable(): bool;

    /**
     * Check whether service can be replaced by another
     * service with same name in container?
     *
     * @return bool
     */
    function isAllowOverride(): bool;
}

Simple Container Service

As an instance here is the simple Value service object will get any php value and will return given value upon request of this service from container.

class Value
    implements iService
{
    protected $value;
    /** @var bool */
    protected $allowOverride = true;
    /** @var bool */
    protected $isSharable = true;

    
    /**
     * Constructor
     *
     * @param mixed $value
     * @param bool  $allowOverride
     * @param bool  $isSharable
     */
    function __construct(
        $value, 
        bool $allowOverride = true, 
        bool $isSharable = true
    ) {
        $this->value = $value;
        $this->setAllowOverride($allowOverride);
        $this->setSharable($isSharable);
    }

    /**
     * Create Service
     *
     * @return mixed
     */
    function createService()
    {
        return $this->value;
    }

    /**
     * Set is service allowed to override
     *
     * @param bool $canReplaced
     *
     * @return $this
     */
    function setAllowOverride(bool $canReplaced = true): self
    {
        $this->allowOverride = $canReplaced;
        return $this;
    }

    /**
     * @inheritDoc
     */
    function isAllowOverride(): bool
    {
        return $this->allowOverride;
    }

    /**
     * Set is service sharable?
     *
     * @param bool $sharable
     *
     * @return $this
     */
    function setSharable(bool $sharable = true): self
    {
        $this->isSharable = $sharable;
        return $this;
    }

    /**
     * @inheritDoc
     */
    function isSharable(): bool
    {
        return $this->isSharable;
    }
}

The isAllowOverride and isSharable methods are instruction methods for container.

The created result on Sharable service will be cached in container and on next request will not be created, somehow is also very similar to singleton objects.

When a service is not AllowOverride it simply means that after register the service no more service with the same name can be registered to the container.

Set Service

Each registered service in container should have a name. service can be triggered to create and attain created result by the given name.

service names inside the container are not case sensitive.

Here the simple Value service is used and registered in container.

$serviceManager = new Container;
$serviceManager->set('myService', new Value(function() {
    echo 'This is my registered service.';
}));

Delegating with container on registering service

A service object which is implementing iFeatureServiceEvent can delegate with container events system on registering and is able to attach any listener to the Event object.

Two specific event will be triggered on set a service into container, before registering service and after that.

interface iBeforeRegistrationListener
    extends iContainerListener
{
    /**
     * Trigger Before Registering Service To Container
     */
    function __invoke(
       string $serviceName, 
       iService $service, 
       ?iService $registeredService, 
       Container $container): void;
}
interface iAfterRegistrationListener
    extends iContainerListener
{
    /**
     * Trigger Before Registering Service To Container
     */
    function __invoke(
        string $serviceName,
        iService $service,
        Container $container): void;
}
interface iFeatureServiceEvent
    extends iService
    , iEventListenerAggregate
{
   /**
     * Attach Listeners To Container Events
     *
     * @param Events $containerEvents
     */
    function attachToEvents(Events $containerEvents): void;
}

Get Registered Service

The registered service can be attained from the container by the name which is given to the service upon registering. on getting service depends on the iService::isSharable result the service will be created for each request when it has false value which is so called fresh service otherwise the created service instance will be cached and the cached instance will be used on request.

By default, a service created is shared. This means that calling the get() method twice for a given service will return exactly the same service. Alternately, you can use the fresh() method instead of the get() method.

Here the factory service is used and the resulted service is value returned from calling the given callable on service creation.

$serviceManager = new Container;

$callable = function() { return time(); };
$sharedService = factory($callable)->setSharable();
$freshService  = factory($callable)->setSharable(false);

$serviceManager->set('mySharedService', $sharedService);
$serviceManager->set('myFreshService', $freshService);

print $serviceManager->get('mySharedService');
print $serviceManager->get('myFreshService');
// 1591713019
// 1591713019
sleep(3);
print $serviceManager->get('mySharedService');
print $serviceManager->get('myFreshService');
// 1591713019
// 1591713022

Get Fresh Service

The fresh() method works exactly the same as the get method, but never caches the service created, nor uses a previously cached instance for the service.

The registered services object has the ability to instruct container to how should behave when getting service instance from the container with iService::isSharable value. if in any time the fresh instance of service is required (new instance of service should be created) the fresh method from container can be used.

service created with refresh method will not get cached at all.

$sharedService = factory(function() {
    static $calls;
    return ++$calls;
})->setSharable();

$serviceManager = new Container;
$serviceManager->set('mySharedService', $sharedService);

// service will be retrieved and get cached
print $serviceManager->get('mySharedService');
// on second request the cached result will be used
print $serviceManager->get('mySharedService');
// create fresh instance by calling given factory method 
print $serviceManager->fresh('mySharedService');

// 1
// 1
// 2

Check Service Existence

The has method will check for the existence of a service or its alias name(s) in the container.

Note: Container by default will register itself as a service with name equal to FQN of container class and alias of iServicesContainer::class.

$serviceManager = new Container;
if ($serviceManager->has(Container::class))
    echo 'Container Registered Service Name: ' . Container::class;

if ($serviceManager->has(iServicesContainer::class))
    echo 'With Container Service Alias: ' . iServicesContainer::class;

// Container Registered Service Name: Poirot\ServiceManager\Container
// With Container Alias: Poirot\ServiceManager\Interfaces\iServicesContainer

Alias Names

The setAliases method provides an alternative name for a registered service. An alias can also be mapped to another alias (it will be resolved recursively).

In this example, stdAliasB will be resolved to stdAlias, then defined aliases stdAlias and std both will be resolved to stdClass::class, and finally will be created through Value service.

$serviceManager = new Container;

$serviceManager->set(\stdClass::class, new Value(new \stdClass));
$serviceManager->setAliases(\stdClass::class, 'stdAlias', 'std');
$serviceManager->setAliases('stdAlias', 'stdAliasB');

$serviceManager->get('stdAliasB');

Define Implementations

An implementation can be defined for a service name before the actual service get registered in container. The implementation can be an FQN to an existing interface or an abstract class or a class or object.

Implementation for a service should be defined before registering the actual service object.

$serviceManager = new Container;
$serviceManager->setImplementation('config', \Traversable::class);

$serviceManager->set('config', new Value(new ArrayIterator));
$serviceManager->get('config');

When implementation for a service already exists can only replaced with the extended interface or object of the current implementation:

$serviceManager = new Container;
$serviceManager->setImplementation('config', \Traversable::class);
$serviceManager->setImplementation('config', \Iterator::class);

$serviceManager->set('config', new Value(new ArrayIterator));

Each defined implementation will create an alias for the given service name to implementation FQN. in the example above both \Iterator::class and \Traversable::class are aliases for config service and can be used to attain service from container.

$serviceManager = new Container;
$serviceManager->setImplementation('config', \Traversable::class);
$serviceManager->setImplementation('config', \Iterator::class);

$serviceManager->set('config', new Value(new ArrayIterator));
$serviceManager->get(\Traversable::class);

Initialize services

An initializer is any callable or any class that implements the interface iInitializer. Initializers are executed with Container event system for each service the first time they are created, and can be used to configure service objects to inject additional setter dependencies or other type of configurations as the instance is accessible.

Initializers will called both on either Services object and created object resulted from each service both.

It's recommended to use initializers only to setup iService and use dependency injection on created services due to can rise code complexity and untestability.

In addition having more initializer will slow the code execution. an initializer is run for every instance you create through the service manager.

interface iInitializerOfServices
{
    /**
     * Initialize Service
     *
     * @param mixed          $instance
     * @param Container $container
     *
     * @return void
     */
    function __invoke($instance, Container $container): void;
}

For example, Container itself has default initializer which inject container instance to services objects or resulted created service implemented iServicesAware interface.

Events of the container is accessible through a method call:

$serviceManager = new Container;
$serviceManager->events()
  ->onInitialize(new InitializerFactory(function($instance, $container) {
    if ($instance instanceof iServicesAware)
        $instance->setServicesContainer($container);
}));

Make External Services

Any service object can be given to container to use benefit of container to make resulted service from given object.

In this example \DateTimeImmutable is instantiated using services registered in container to resolve dependencies which here is timezone argument.

$serviceManager = new Container;
$serviceManager->set('timezone', value(new \DateTimeZone('Europe/Helsinki')));

/** @var \DateTimeImmutable $time */
$time = $serviceManager->make(instance(\DateTimeImmutable::class, [
    'time' => 'now',
    'timezone' => get('timezone')
]));

echo $time->format('Y-m-d H:i:s');

Configure Service Manager

The Service Manager container can be configured with help of container Builder by passing an associative array to the builder constructor. then builder has all materials set and is ready to configure any service manager container.

The following keys are:

  • implementations: will define the services implementation. read more ...

  • aliases: set alias names for a service name. read more ...

  • initializer_aggregate: give InitializerAggregate object to container.

  • attached_initializers: attach initializers to initializer aggregator of container. read more ...

  • services: register named services to container. read more ...

Configuration source can be any iterable by default, and when it's not array should passed to Builder with Builder::parse method.

$builder = new Builder(Builder::parse($myNoneArraySource));

The config source can be imported from php file which should return parsable value.

// services.php
return [
    'services' => [
        'config' => [
            'local_time_zone' => 'Europe/Helsinki',
        ],
    ],
];
$builder = new Builder(Builder::from(__DIR__ . '/services.php'));

Overview

Here is an example of how you could configure a service manager:

$configuration = [
    'implementations' => [
        'timezone' => \DateTimeZone::class,
        'serverTime' => \DateTimeImmutable::class,
    ],
    'services' => [
        'config' => [
            'local_time_zone' => 'Europe/Helsinki',
        ],
        'timezone' => factory(function(iServicesContainer $container) {
            return new \DateTimeZone($container->get('config')['local_time_zone']);
        }),
        'serverTime' => instance(\DateTimeImmutable::class, [
            'time' => 'now',
            'timezone' => get('timezone'),
        ])->setSharable(false),
    ],
];

Configuration can be passed to builder with setter method:

$builder = new Builder();
$builder->setConfigs($configuration);

Set Configuration through constructor:

$builder = new Builder($configuration);

implementations

An array of map service name to an implementation FQN or an object, or an array of configuration Params object.

Read Implementations section if need to know more about what implementations are in general.

$conf = [
   'implementations' => [
      'serviceName' => \stdClass::class,
      new Params([
         'service_name' => 'serviceName',
         'implement' => \stdClass::class
      ])
   ],
];

aliases

An array of map service name to an alias name, or an array of configuration Params object.

Read Aliases section if need to know more about what aliases are in general.

$conf = [
  'aliases' => [
     'serviceName' => 'aliasName',
     new Params([
        'service_name' => 'serviceName',
        'aliases' => 'aliasName',
     ])
  ],
];

attached_events

An array of listener object, a resolver instantiator creating a listener or an array of configuration Params object. Priority of listener only can be set through configuration Params.

Read Initializer section if need to know more about what aliases are in general.

$conf = [
   'attached_initializers' => [
       new MyInitializer,
       new Params([
          'initializer' => new MyOtherInitializer,
          'priority' => 10,
       ])
   ],
];

Any resolver can be used as a parameter value to lazy load or instantiate the needed object, in the example below the class FQN is given to the instantiator and the object will be created on build time through this resolver.

use Poirot\Std\ArgumentsResolver\InstantiatorResolver;

$conf = [
   'attached_initializers' => [
      new InstantiatorResolver(DummyInitializer::class),
   ]
];

services

An array of map service name to a service object.

Read Service section if need to know more about what aliases are in general.

$conf = [
  'services' => [
     'serviceName' => new DummyService,
  ]
];

Any resolver can be used as a parameter value to lazy load or instantiate the needed object, in the example below the class FQN is given to the instantiator and the object will be created on build time through this resolver.

use Poirot\Std\ArgumentsResolver\InstantiatorResolver;

$conf = [
  'services' => [
     'serviceName' => new InstantiatorResolver(
        DummyService::class
     )
  ]
];

Built-in Services

The design logic with out-of-the-box services are that the developer itself is aware of how the services are dependent to each other and they meant to be created. so the services and how they defined the creation process are pulling out of container itself. when dependencies are changed or existing service for any reason not a fit for the application you may want to register this service with new service creator implementation. In nutshell it's not container who decide how to resolve service but it defined on service registration time by developer.

Factory

A factory get any callable that can be used to create resulted service, it will not do any auto resolving of dependencies. Each factory always receive a iServicesContainer argument (which is the Container requested creating service).

class MyObjectFactory
{
    function __invoke(iServicesContainer $container)
    {
        $dependency = $container->get(Dependency::class);
        return new MyObject($dependency);
    }
}

$container = new Container();
$container->set('MyService', new Factory(new MyObjectFactory));

to ease usage of it factory as a function can be used:

use function Poirot\ServiceManager\factory;

$container->set('MyService', factory(new MyObjectFactory));

Instance

When it's needed to resolve dependencies from container to class __construct to instantiate class or to a callable which is resulted to create a service the Instance service can be used.

You may type-hint the dependency in the constructor or use doc-notation to define dependencies. Dependencies are resolved when for each type-hinted or notation defined argument container has the same defined implementations, service or alias with same FQN as name of the type-hinted class or interface.

Resolve Type-Hinted service:

class ServiceWithTypeHintDependency
{
    public $resolvedDependency;

    function __construct(\stdClass $myStdArgument) {
        $this->resolvedDependency = $myStdArgument;
    }
}

$container = new Container;
$container->setImplementation('myStdClass', \stdClass::class);
$container->set('myStdClass', value(new stdClass));
$container->set('myDependentClass', instance(ServiceWithTypeHintDependency::class));

$container->get('myDependentClass');

same behaviour when there is an service name or alias for type-hinted FQN:

$container = new Container;
$container->set(\stdClass::class, value(new stdClass));
$container->set('myDependentClass', instance(ServiceWithTypeHintDependency::class));

$container->get('myDependentClass');

resolve to a factory method:

class ServiceWithTypeHintDependency
{
    public $resolvedDependency;

    static function create(\stdClass $myStdService) {
        return new self($myStdService);
    }

    function __construct(\stdClass $myStdArgument) {
        // \stdClass as dependency should be resolved to this class
        $this->resolvedDependency = $myStdArgument;
    }
}

$container = new Container;
$container->set('myStdClass', value(new stdClass));
$container->setAliases('myStdClass', \stdClass::class);
$container->set('myDependentClass', instance(
   [ServiceWithTypeHintDependency::class, 'create']
));

$container->get('myDependentClass');

Resolve Notation service:

The notation can be defined after @param on the method doc-block by @Service requiredServiceName notation mark. In the example below class construct argument $myStdArgument will be resolved by myStdImplementation.

class ServiceWithNotationDependency
{
    public $resolvedDependency;

    /**
     * Constructor
     *
     * @param \stdClass $myStdArgument @Service myStdImplementation
     */
    function __construct(\stdClass $myStdArgument) {
        $this->resolvedDependency = $myStdArgument;
    }
}

$container = new Container;
$container->set('myStdImplementation', value(new stdClass));
$container->set('myDependentClass', instance(ServiceWithNotationDependency::class));

$container->get('myDependentClass');

resolve to a factory method:

class ServiceWithNotationDependency
{
    public $resolvedDependency;

    /**
     * Factory
     *
     * @param \stdClass $myStdService @Service myStdImplementation
     * @return ServiceWithNotationDependency
     */
    static function create(\stdClass $myStdService) {
        return new self($myStdService);
    }

    /**
     * Constructor
     *
     * @param \stdClass $myStdArgument
     */
    function __construct(\stdClass $myStdArgument) {
        $this->resolvedDependency = $myStdArgument;
    }
}

$container = new Container;
$container->set('myStdImplementation', value(new stdClass));
$container->set('myDependentClass', instance(
  [ServiceWithNotationDependency::class, 'create']
));

$container->get('myDependentClass');

Resolve dependencies by given options:

When the instance service is using options can given to resolve the dependencies needed to create the service object. the given options is an array map of type-hinted argument or variable name to the value which is being resolved to given instance. the options could be consist of any other iService implementation which will be converted to the service result when it's needed.

here for example get service is used to retrieve an existing service from container and used as value of necessary arguments of construct method.

class ServiceWithNotationDependency
{
    public $resolvedDependency;

    function __construct(\stdClass $myStdArgument) {
        $this->resolvedDependency = $myStdArgument;
    }
}

$container = new Container;
$container->set('myStdImplementation', value(new stdClass));
$container->set('myDependentClass',
   instance(ServiceWithNotationDependency::class, [
       \stdClass::class => get('myStdImplementation'),
       // with lower priority argument name also can be used
       // 'myStdArgument' => fresh('myStdImplementation')
   ])
);

$container->get('myDependentClass');

Fresh

The fresh service will retrieve fresh version of given service name by creating new instance. This is the equivalent behaviour of Container::fresh() method.

The example here won't work to save request time in real case as requestTime service will not triggered and not cached till the first get() called:

there is some workaround events which is mitigated from this example to simplify that.

$container = new Container;
$container->set('requestTime', factory(function () {
    return time();
}));
$container->set('timeNow', fresh('requestTime'));

echo $container->get('requestTime');
sleep(3);
echo $container->get('timeNow');
// 1592160137
// 1592160140

Get

The get service will retrieve cached version of given service name if it's created before and create service and cached it if it's not. This is the equivalent behaviour of Container::get() method.

The example demonstrate how get can be used to define an alias to other service.

$container = new Container;
$container->set(\stdClass::class, value(new \stdClass));
$container->set('myStdClass', get(\stdClass::class));

$container->get('myStdClass');

Value

It's a simple service which return the given value as a resulted service.

Value here is the registered function invokable:

$container = new Container;
$container->set('dateFormatter', value(function($time) {
    return date('Y-m-d H:i:s', $time);
}));

print $container->get('dateFormatter')
   ->__invoke(time()); // 2020-06-14 19:20:06

Custom Service

When you have services which share a common creation pattern and the functionality is not achievable with default defined services or just simply wanted to create your own service there is always an options to create your service creation object by implementing iService or extending aService.

Also can read: Simple Container Service for more information.

Last updated

Was this helpful?