ioc-interop / interface
Interoperable IOC container interfaces for PHP.
Requires
- php: >=8.4
Requires (Dev)
- pds/composer-script-names: ^1.0
- pds/skeleton: ^1.0
- phpstan/phpstan: ^2.0
- pmjones/php-styler: 0.x@dev
- star-interop/stardoc: 1.x-dev
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-06 23:10:08 UTC
README
Ioc-Interop provides an interoperable package of standard interfaces for inversion-of-control (IOC) service container functionality. 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 attempts to adhere to the Package Development Standards approach to naming and versioning.
Interfaces
This package defines the following interfaces:
-
IocContainer affords obtaining services by name.
-
IocContainerFactory affords obtaining a new instance of IocContainer.
-
IocThrowable extends Throwable to mark an Exception as IOC-related.
-
IocTypeAliases provides custom PHPStan types to aid static analysis.
IocContainer
IocContainer affords obtaining services by name.
-
Notes:
- This interface does not afford service management. The container will need to create and retain services somehow, whether by itself, through a Service-Interop implementation, or some other means.
IocContainer Methods
-
public function hasService(ioc_service_name_string $serviceName) : bool;
-
Might the container return an object for the
$serviceName? -
Directives:
-
Implementations MUST return
falseonly when the container knows in advance thatgetService()will not be able to return an object for the$serviceName. -
Implementations MUST NOT produce a service, by instantiation or any other means, in order to answer.
-
-
Notes:
-
The logic for this method is expressly unspecified. The check may be accomplished by querying a service management subsystem, or by some other means.
-
A
falseresult is conclusive; atrueresult is not. Afalseresult means the container already knows it cannot produce an object for the$serviceName, sogetService()will throw IocThrowable. Atrueresult means only that the container cannot tell in advance whether it will fail to produce the$serviceName; for example, failures from a missing dependency or bad configuration surface only on the attempt. Either answer speaks only to the call that produced it; a container whose state changes may answer differently next time. -
Every path by which
getService()could succeed needs a matching check here. The directive binds this method to whatgetService()could do, andgetService()might do a great deal: look in a registry, consult a service management subsystem, autowire from a class name, and so on. Adding a path togetService()without adding a corresponding check inhasService()leaves the container returningfalsefor a service it can in fact produce.
-
-
-
public function getService( ioc_service_name_string $serviceName, ) : ioc_service_object;
-
Returns an object for the
$serviceName. -
Directives:
- Implementations MUST throw IocThrowable if the container
cannot return an object for the
$serviceName, regardless of the underlying cause.
- Implementations MUST throw IocThrowable if the container
cannot return an object for the
-
Notes:
-
The logic for this method is expressly unspecified. Retrieval may be accomplished via a service management subsystem, or by some other means.
-
The service name is arbitrary, but it can determine the return type. Any non-empty string will serve:
'db.replica'is as valid a name as\Foo\Bar::class. However, when the name is a class-string, theioc_service_objectreturn type resolves to that class rather than toobject. -
The narrowed return type is not a runtime guarantee. Nothing requires a service to be an instance of the class its name denotes; the narrowing is what static analysis infers, not what the container promises. A caller that needs the guarantee has
instanceoffor it. -
Static analysis treats a name that could be a class name as one. It looks for the class, not for the intent behind the label:
'logger'is a class-string wherever aLoggerclass exists, case notwithstanding. Where a service is not an instance of its namesake, a name that cannot be a class name avoids the narrowing; a dot, as in'db.replica', is enough. -
The returned instance may be new or shared. The retrieval logic defines the service lifetime, not the container (per se) and not the caller requesting the service.
-
Make the IocThrowable as informative as possible. Doing so helps to debug why the call failed. For one example, if the logic for producing an object results in an Error or Exception, consider setting that as the
$previousThrowable for the IocThrowable. For another example, consider describing the resolution path in the message itself to show where a circular or deeply-nested resolution failed.
-
-
IocContainerFactory
IocContainerFactory affords obtaining a new instance of IocContainer.
IocContainerFactory Methods
-
public function newContainer() : IocContainer;
-
Returns a new instance of IocContainer.
-
Directives:
- Every call MUST return a new instance of IocContainer.
-
Notes:
-
The logic for this method is expressly unspecified. Implementations might use providers, configuration files, attribute or annotation collection, or some other means to create and populate a container. Implementations might also choose to return a compiled or otherwise reconstituted container.
-
A new container instance does not necessarily contain new service instances. Whether two containers return the same object for a given service name is a matter of service lifetime, which the retrieval logic defines. A container drawing on a shared service registry, or delegating to a parent container, may hand out the same instance as another.
-
-
IocThrowable
IocThrowable extends Throwable to mark an Exception as IOC-related.
It adds no class members.
IocTypeAliases
IocTypeAliases provides custom PHPStan types to aid static analysis.
-
ioc_service_name_string non-empty-string- A
class-stringor non-emptystringname for a service.
- A
-
ioc_service_object ($serviceName is class-string<T> ? T : object)- The service
objectfor a given service name.
- The service
Implementations
Implementations MAY define additional class members not defined in these interfaces.
Notes:
- Reference implementations are available at https://github.com/ioc-interop/impl.
Q & A
How is Ioc-Interop different from PSR-11?
PSR-11 is an earlier recommendation that offers an interface to get
items from a container, and to see if that container has a particular item.
Ioc-Interop is functionally almost identical to PSR-11. However, Ioc-Interop
is intended to contain only services (object). PSR-11 is intended to contain
anything (mixed).
Ioc-Interop also offers an IocContainerFactory interface, whereas PSR-11 offers none.
Is Ioc-Interop compatible with PSR-11?
No, in the sense that the method names, signatures, and intents are different.
Yes, in the sense that both may be implemented on the same class; the method names are different, and so are non-conflicting.
Why does Ioc-Interop not afford service management?
Ioc-Interop is focused on the concerns around obtaining and consuming services. The affordances for managing and producing services are separate concerns.
Earlier drafts of Ioc-Interop were much more expansive, including a resolver subsystem and a service management subsystem. These have been extracted to separate standards, each of which is dependent on Ioc-Interop:
This separation helps to maintain a boundary between the needs of service consumers (afforded by Ioc-Interop) and service producers (afforded by Service-Interop and Resolver-Interop).
Note that Ioc-Interop is independent of Service-Interop and Resolver-Interop. Ioc-Interop implementations can use them, or avoid them, as implementors see fit.
Is IocContainer for Dependency Injection or is it a Service Locator?
IocContainer acts as a Service Locator only when it is used as a dependency in order to retrieve other dependencies from it.
Why does IocContainer disallow non-object values?
IocContainer is explicitly a service container, not a general config container for scalar or array values.
Limiting services to objects helps maintain consistent expectations regarding
service types and behavior. Of the researched projects, 11 return object, and
8 return mixed, so this restriction is consistent with the majority.
Ioc-Interop recognizes that implementors and consumers often want to make config values easily available, though Ioc-Interop questions what it means (or if it is possible) to get a "shared" scalar or array that works the same as a "shared" object.
With that in mind, Ioc-Interop encourages the use of one or more config services or value objects to make those values available, instead of storing config values directly inside a container.
Why does a class-string name not guarantee an instance of that class?
When the $serviceName is a class-string, the ioc_service_object return
type resolves to that class. It would seem to follow that getService()
should be required to return an instance of it.
It cannot be required, because whether a name is a class-string is not a
property of the name. Static analysis answers that question by looking for a
class of that name in the codebase being analyzed, case-insensitively. A
service labeled logger is a class-string in a project that happens to
define a Logger class, and a plain string in one that does not; the same
call means different things in different codebases. A directive whose
applicability varies that way cannot be implemented, because the container
has no way to know which case it is in.
Of the researched projects, Laravel's container and yiisoft/di narrow the
return type on a class-string name in exactly this way, and neither
requires the returned object to be an instance of it. Aura.Di and Nette DI
decline to narrow at all, returning object from their name-keyed methods.
Why does IocContainer define getService() and not just get()?
The vast majority of researched projects, whether PSR-11 conforming or not, use
the method name get(). Contra the research, Ioc-Interop asserts that get()
is too generic, and that the method name should hint at what is being gotten;
thus, getService().
Why does Ioc-Interop offer an IocContainerFactory?
Container-creation logic is a minority position among the researched projects: only four offer any way to create the container itself, and each does so with a different signature.
Contra the research, Ioc-Interop asserts that container creation is a separate concern from container use, and thus deserves an interface of its own. Separating them affords creating a container more than once: per request or per job in a long-running runtime, per test case, or per tenant.
Implementing IocContainerFactory is optional. Service consumers should typehint on IocContainer.
Why must every failure throw an IocThrowable?
None of the researched projects throw a single exception type for every failure. Every one of them lets an exception from a consumer-supplied factory or constructor propagate unchanged, and throws a container exception only for failures it detects itself, such as an unknown name or an unresolvable dependency.
Contra the research, Ioc-Interop asserts that a consumer calling getService()
should have exactly one thing to catch. A container that propagates arbitrary
throwables offers no contract at the call site: the consumer cannot know what
type to expect, and so must write against a concrete implementation instead of
a standard interface. PSR-11 asserts the same thing: it documents a
container exception for any error while retrieving an entry, though none of the
researched projects honor it.
A single throwable type need not lose information: an implementation can retain
the originating Error or Exception as the $previous
Throwable, so a consumer that needs the underlying cause can reach it.
Ioc-Interop recommends this but does not require it, since an implementation
that falls back from a failed means of producing the service to another isn't
obligated to account for the attempt it recovered from.