Skip to content

Interoperable interfaces for defining and providing services to containers.

License

Notifications You must be signed in to change notification settings

service-interop/interface

Repository files navigation

Service-Interop Standard Interface Package

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).

Interfaces

This package defines the following interfaces:

ServiceLifetime

ServiceLifetime affords constants defining service lifetimes.

ServiceLifetime Constants

  • public const string SCOPED = 'SCOPED';
    • Marks a service as shared, with a lifetime scoped to the current request.

    • Notes:

      • SCOPED is 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:

      • SINGLETON is a cross-request lifetime. In a "shared-nothing" execution environment, this is not substantially different from a SCOPED lifetime. However, in a long-running process, SINGLETON services are expected to stay shared across multiple requests, whereas the SCOPED services are expected to be unset at the end of a request.
  • public const string TRANSIENT = 'TRANSIENT';
    • Marks a service lifetime as unshared.

    • Notes:

      • TRANSIENT indicates a factoried service. Each retrieval of the service will return a new, unshared instance.

ServiceCollection

ServiceCollection affords registration of service instances, definitions, and aliases.

ServiceCollection Methods

  • public function hasInstance(service_name_string $serviceName) : bool;
    • Has a shared instance of the $serviceName been set?
  • 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 $serviceName is not available.
  • 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 $lifetime is ServiceLifetime::TRANSIENT.

      • Implementations MUST unset the $serviceName for lifetimes other than $lifetime.

  • public function unsetInstance(service_name_string $serviceName) : void;
    • Unsets the shared instance of the $serviceName.
  • public function unsetInstances(service_lifetime_string $lifetime) : void;
    • Unsets all shared instances with the specified lifetime.
  • public function hasDefinition(service_name_string $serviceName) : bool;
  • 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.
  • public function newDefinition(
        service_name_string $serviceName,
    ) : ServiceDefinition;
  • public function setDefinition(
        service_name_string $serviceName,
        ServiceDefinition $definition,
    ) : void;
  • public function unsetDefinition(service_name_string $serviceName) : void;
  • public function hasAlias(service_name_string $serviceName) : bool;
    • Has an alias for the $serviceName been set?
  • 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 $serviceName is 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.
  • public function setAlias(
        service_name_string $serviceName,
        service_name_string $alias,
    ) : void;
    • Sets the alias for one $serviceName to another service.

    • Directives:

      • Implementations MUST attempt to detect if adding the $alias would result in an infinite alias cycle; on detection, implementations MUST throw ServiceThrowable.
    • 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 $alias would end up back at itself.
  • public function unsetAlias(service_name_string $serviceName) : void;
    • Unsets the alias for the $serviceName.

ServiceDefinition

ServiceDefinition affords building a service instance, including both instantiation logic and extended post-instantiation logic.

ServiceDefinition Methods

  • 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.
  • 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 callable type allows for a wide range of implementations. Cf. the https://php.net/callable documentation for more.
  • 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 callable type allows for a wide range of implementations. Cf. the https://php.net/callable documentation for more.
  • 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::SCOPED if the lifetime is not otherwise set.
  • 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

ServiceProvider affords provision of service instances, definitions, and aliases to a ServiceCollection instance.

ServiceProvider Methods

  • 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.

ServiceThrowable

ServiceThrowable extends Throwable to mark an Exception as service-related. It adds no class members.

ServiceTypeAliases

ServiceTypeAliases defines PHPStan type aliases to aid static analysis.

  • service_extender_callable callable(object,IocContainer):object
    
    • A callable for service post-instantiation logic; e.g. to set a property, call a setter or initializer method, decorate the service, etc.
  • service_factory_callable callable(IocContainer):object
    
    • A callable for service instantiation logic.
  • service_lifetime_string ServiceLifetime::SCOPED|ServiceLifetime::SINGLETON|ServiceLifetime::TRANSIENT
    
    • A string indicating the lifetime of a service.
  • service_name_string class-string<T>|string
    
    • A class-string or string name for a service.
  • service_object ($serviceName is class-string<T> ? T : object)
    
    • The service object for a given service name.

Implementations

Q & A

Why is Service-Interop separate from [Container-Interop][]?

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.

Why TRANSIENT instead of PROTOTYPE for always-new services?

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.


About

Interoperable interfaces for defining and providing services to containers.

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages