Service-Interop provides an interoperable package of standard interfaces for defining and providing services to containers. It reflects, refines, and reconciles the common practices identified within several pre-existing projects.
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174).
This package defines the following interfaces:
-
ServiceLifetime affords constants defining service lifetimes.
-
ServiceCollection affords registration of service instances, definitions, and aliases.
-
ServiceDefinition affords building a service instance, including both instantiation logic and extended post-instantiation logic.
-
ServiceProvider affords provision of service instances, definitions, and aliases to a ServiceCollection instance.
-
ServiceThrowable extends Throwable to mark an Exception as service-related. It adds no class members.
-
ServiceTypeAliases defines PHPStan type aliases to aid static analysis.
ServiceLifetime affords constants defining service lifetimes.
-
public const string SCOPED = 'SCOPED';
-
Marks a service as shared, with a lifetime scoped to the current request.
-
Notes:
SCOPEDis the default lifetime. A request-scoped service is intended to be unset at the end of the request. This is the normal case for the PHP "shared-nothing" execution environment.
-
-
public const string SINGLETON = 'SINGLETON';
-
Marks a service as shared, with a lifetime across all requests.
-
Notes:
SINGLETONis a cross-request lifetime. In a "shared-nothing" execution environment, this is not substantially different from aSCOPEDlifetime. However, in a long-running process,SINGLETONservices are expected to stay shared across multiple requests, whereas theSCOPEDservices are expected to be unset at the end of a request.
-
-
public const string TRANSIENT = 'TRANSIENT';
-
Marks a service lifetime as unshared.
-
Notes:
TRANSIENTindicates a factoried service. Each retrieval of the service will return a new, unshared instance.
-
ServiceCollection affords registration of service instances, definitions, and aliases.
-
public function hasInstance(service_name_string $serviceName) : bool;
- Has a shared instance of the
$serviceNamebeen set?
- Has a shared instance of the
-
public function getInstance(service_name_string $serviceName) : service_object;
-
Returns the shared instance of the
$serviceName. -
Directives:
- Implementations MUST throw ServiceThrowable if a shared
instance of the
$serviceNameis not available.
- Implementations MUST throw ServiceThrowable if a shared
instance of the
-
-
public function setInstance( service_name_string $serviceName, service_object $instance, service_lifetime_string $lifetime = 'SCOPED', ) : void;
-
Sets the shared instance of the
$serviceName. -
Directives:
-
Implementations MUST throw ServiceThrowable if the
$lifetimeisServiceLifetime::TRANSIENT. -
Implementations MUST unset the
$serviceNamefor lifetimes other than$lifetime.
-
-
-
public function unsetInstance(service_name_string $serviceName) : void;
- Unsets the shared instance of the
$serviceName.
- Unsets the shared instance of the
-
public function unsetInstances(service_lifetime_string $lifetime) : void;
- Unsets all shared instances with the specified lifetime.
-
public function hasDefinition(service_name_string $serviceName) : bool;
- Has a ServiceDefinition for the
$serviceNamebeen set?
- Has a ServiceDefinition for the
-
public function getDefinition( service_name_string $serviceName, ) : ServiceDefinition;
-
Returns the ServiceDefinition for the
$serviceName, instantiating it if needed. -
Notes:
- Create a new definition if necessary. In practice, this
likely means calling
newDefinition($serviceName)and then retaining that instance for later retrieval.
- Create a new definition if necessary. In practice, this
likely means calling
-
-
public function newDefinition( service_name_string $serviceName, ) : ServiceDefinition;
- Returns a new ServiceDefinition for the
$serviceName.
- Returns a new ServiceDefinition for the
-
public function setDefinition( service_name_string $serviceName, ServiceDefinition $definition, ) : void;
- Sets the ServiceDefinition for the
$serviceName.
- Sets the ServiceDefinition for the
-
public function unsetDefinition(service_name_string $serviceName) : void;
- Unsets the ServiceDefinition for the
$serviceName.
- Unsets the ServiceDefinition for the
-
public function hasAlias(service_name_string $serviceName) : bool;
- Has an alias for the
$serviceNamebeen set?
- Has an alias for the
-
public function getAlias( service_name_string $serviceName, ) : service_name_string;
-
Returns the alias for the
$serviceName. -
Directives:
-
Implementations MUST throw ServiceThrowable if an alias for the
$serviceNameis not available. -
Implementations MUST return the final alias in the alias chain for the
$serviceName.
-
-
Notes:
- Chained aliases are allowed. That is, one alias can lead to
another, and that one to yet another, and so on. This means
implementations will have to track through those aliases to arrive
at a final or terminal alias for the
$serviceName.
- Chained aliases are allowed. That is, one alias can lead to
another, and that one to yet another, and so on. This means
implementations will have to track through those aliases to arrive
at a final or terminal alias for the
-
-
public function setAlias( service_name_string $serviceName, service_name_string $alias, ) : void;
-
Sets the alias for one
$serviceNameto another service. -
Directives:
- Implementations MUST attempt to detect if adding the
$aliaswould result in an infinite alias cycle; on detection, implementations MUST throw ServiceThrowable.
- Implementations MUST attempt to detect if adding the
-
Notes:
- Chained aliases are allowed. That is, one alias can lead to
another, and that one to yet another, and so on. To prevent an
infinite loop, implementations will have to track through the
aliases to find if the
$aliaswould end up back at itself.
- Chained aliases are allowed. That is, one alias can lead to
another, and that one to yet another, and so on. To prevent an
infinite loop, implementations will have to track through the
aliases to find if the
-
-
public function unsetAlias(service_name_string $serviceName) : void;
- Unsets the alias for the
$serviceName.
- Unsets the alias for the
ServiceDefinition affords building a service instance, including both instantiation logic and extended post-instantiation logic.
-
public function getServiceName() : string;
-
Returns the name of this service.
-
Notes:
- The service name is an arbitrary string. Typically the service
name is an instantiable class name, but not always. It could be any
string at all: an interface name, an abstract class name, a label
such as
'db.replica', and so on.
- The service name is an arbitrary string. Typically the service
name is an instantiable class name, but not always. It could be any
string at all: an interface name, an abstract class name, a label
such as
-
-
public function hasFactory() : bool;
- Is there a factory that instantiates the service?
-
public function getFactory() : service_factory_callable;
-
Returns the factory that instantiates the service.
-
Directives:
- Implementations MUST throw ServiceThrowable if there is no factory for the service.
-
-
public function setFactory(callable $factory) : $this;
-
Sets the factory that instantiates the service.
-
Notes:
- The
callabletype allows for a wide range of implementations. Cf. the https://php.net/callable documentation for more.
- The
-
-
public function unsetFactory() : $this;
- Unsets the factory that instantiates the service.
-
public function hasClass() : bool;
- Does this service resolve to a class other than the service name?
-
public function getClass() : class-string;
-
Returns the class this service resolves to.
-
Directives:
- Implementations MUST throw ServiceThrowable if there is no class for the service.
-
-
public function setClass(class-string $class) : $this;
- Sets the class this service resolves to.
-
public function unsetClass() : $this;
- Unsets the class this service resolves to.
-
public function hasExtenders() : bool;
- Are there any post-instantiation extenders for the service?
-
public function getExtenders() : service_extender_callable[];
- Returns the post-instantiation extenders for the service.
-
public function setExtenders(service_extender_callable[] $extenders) : $this;
- Sets all post-instantiation extenders for the service.
-
public function unsetExtenders() : $this;
- Unsets all post-instantiation extenders for the service.
-
public function addExtender(service_extender_callable $extender) : $this;
-
Adds a single post-instantiation extender for the service.
-
Notes:
- The
callabletype allows for a wide range of implementations. Cf. the https://php.net/callable documentation for more.
- The
-
-
public function setLifetime(service_lifetime_string $lifetime) : $this;
- Sets the lifetime of the service.
-
public function getLifetime() : service_lifetime_string;
-
Gets the lifetime of the service.
-
Directives:
- Implementations MUST return
ServiceLifetime::SCOPEDif the lifetime is not otherwise set.
- Implementations MUST return
-
-
public function buildService(IocInterop\Interface\IocContainer $ioc) : object;
-
Builds a new instance of the service.
-
Directives:
-
Implementations MUST return a new instance.
-
Implementations MUST choose instantiation logic in this order:
-
If a factory is set, implementations MUST instantiate the a service object using that factory.
-
Otherwise, if a class is set, implementations MUST instantiate a service object of that class.
-
Otherwise, implementations MUST instantiate a service object using the service name as the class.
-
-
Implementations MUST apply the extenders to the new service object.
-
-
Notes:
-
Always return a new instance. This method always builds an instance, regardless of its lifetime. It is up to the caller (typically ServiceCollection) to take the lifetime into account.
-
Factory instantiation takes precedence over resolver instantiation. Further, resolving to an explicit class takes precedence over resolving to the class implied by the service name.
-
-
ServiceProvider affords provision of service instances, definitions, and aliases to a ServiceCollection instance.
-
public function provide(ServiceCollection $services) : void;
-
Provides service instances, definitions, and aliases to the
$services. -
Notes:
- Provision includes a wide range of activity. The implementation
can set, unset, replace, modify, etc. the instances, definitions,
and aliases in the
$services.
- Provision includes a wide range of activity. The implementation
can set, unset, replace, modify, etc. the instances, definitions,
and aliases in the
-
ServiceThrowable extends Throwable to mark an Exception as service-related. It adds no class members.
ServiceTypeAliases defines PHPStan type aliases to aid static analysis.
-
service_extender_callable callable(object,IocContainer):object- A
callablefor service post-instantiation logic; e.g. to set a property, call a setter or initializer method, decorate the service, etc.
- A
-
service_factory_callable callable(IocContainer):object- A
callablefor service instantiation logic.
- A
-
service_lifetime_string ServiceLifetime::SCOPED|ServiceLifetime::SINGLETON|ServiceLifetime::TRANSIENT- A
stringindicating the lifetime of a service.
- A
-
service_name_string class-string<T>|string- A
class-stringorstringname for a service.
- A
-
service_object ($serviceName is class-string<T> ? T : object)- The service
objectfor a given service name.
- The service
-
Directives:
- Implementations MAY define additional class members not defined in these interfaces.
-
Notes:
- Reference implementations may be found at https://github.com/Service-Interop/impl.
Whereas [Container-Interop][] is for obtaining instances, Service-Interop is for managing the instances, definitions, and aliases involved in producing the services to be obtained.
This separation allows for containers that are fully "open" by implementing both IocContainer and ServiceCollection on the same class, and for containers that are "closed" in the sense that the ServiceCollection is encapsulated by but not publicly available through the IocContainer.
Why a ServiceDefinition at all?
Whereas it's possible to set a pre-created service instance into a ServiceCollection, very often it's preferred to set a factory to create that instance only when needed. Further, sometimes that new instance may need to be modified after instantiation with custom extender logic. The factory might be more generalized instead of service-specific, as with autowiring resolvers. Finally, the service lifetime might be shared, or always-new.
Some projects place all that functionality directly on a container. However, that results in a very large API surface area. Other projects collect that functionality onto a "builder" object, typically called a "definition."
Service-Interop adopts the latter approach, not only because it separates the concerns of building from retrieval, but also because it gives implementors a natural extension point for custom building behaviors.
Why does ServiceDefinition not support property or setter injection?
Some projects functionality support the ability to set properties on the newly-instantiated service. Others support the ability to call "setter" or other methods on the newly-instantiated service.
However, the APIs around this kind of functionality are different enough from each other that it is difficult to discern a standard. In addition, is can be difficult to lazily acquire the values or arguments to property-inject or setter-inject; the different projects support these in very different ways.
As such, ServiceDefinition does not directly support property injection, setter injection, and so on. Implementors are encouraged to add support as desired to their implementations.
However, note that ServiceDefinition does support alternative injection strategies indirectly via extenders. For example:
/** @var ServiceCollection $services */
$services->getDefinition(Foo::class)
->setExtender(function (IocContainer $ioc, Foo $foo) : Foo {
// property injection
$foo->bar = 'bar';
// setter injection
$foo->setBaz('baz');
// done
return $foo;
});Why does ServiceDefinition not support contextual or environmental binding?
Sometimes two different classes need different implementations of the same interface. Functionality to specify different services to inject on the same typehints is relatively rare; only 2 of the researched projects support it.
Another variation on this is when a class needs different implementations in different environments (e.g. "web" vs "cli" vs "test"). This too is relatively rare among the researched projects.
As such, Ioc-Interop finds little to standardize on as far as an API. Implementors are encouraged to implement IocParameterResolver attributes to note the specific service to inject for a specific parameter.
Neither term appears prominently in the research; "prototype" appears only once, and "transient" never.
However, other research indicates that the term "transient" is more associated with lifetimes, and "prototype" more with scopes. As Ioc-Interop uses lifetime terms, "transient" is more appropriate.