*/ private array $bindings = array(); /** * Identifiers that resolve to a single shared instance. * * @var array */ private array $shared = array(); /** * Already resolved shared instances. * * @var array */ private array $instances = array(); /** * Identifiers currently being resolved, used to spot cycles. * * @var array */ private array $building = array(); /** * Registers a service. * * @param string $id Service identifier, usually an interface name. * @param Closure|string|null $concrete Optional. Factory or class name. Defaults to the identifier. * @param bool $shared Optional. Whether to reuse one instance. Default false. */ public function bind( string $id, Closure|string|null $concrete = null, bool $shared = false ): void { unset( $this->instances[ $id ] ); $this->bindings[ $id ] = $concrete ?? $id; $this->shared[ $id ] = $shared; } /** * Registers a service that resolves to one shared instance. * * @param string $id Service identifier, usually an interface name. * @param Closure|string|null $concrete Optional. Factory or class name. Defaults to the identifier. */ public function singleton( string $id, Closure|string|null $concrete = null ): void { $this->bind( $id, $concrete, true ); } /** * Registers an object that has already been built. * * @param string $id Service identifier. * @param object $instance Object to return for that identifier. */ public function instance( string $id, object $instance ): void { $this->instances[ $id ] = $instance; $this->shared[ $id ] = true; } /** * Determines whether an identifier can be resolved. * * @param string $id Service identifier. * * @return bool True when the container knows the identifier or can autowire it. */ public function has( string $id ): bool { return isset( $this->bindings[ $id ] ) || isset( $this->instances[ $id ] ) || class_exists( $id ); } /** * Returns a service, building it on first use. * * @param string $id Service identifier. * * @throws ContainerException When the service cannot be built. * * @return object The resolved service. */ public function get( string $id ): object { if ( isset( $this->instances[ $id ] ) ) { return $this->instances[ $id ]; } $object = $this->resolve( $id ); if ( ! empty( $this->shared[ $id ] ) ) { $this->instances[ $id ] = $object; } return $object; } /** * Builds a service, ignoring any shared instance. * * @param string $id Service identifier. * @param array $parameters Optional. Constructor arguments by name. Default empty. * * @throws ContainerException When the service cannot be built. * * @return object A new instance. */ public function make( string $id, array $parameters = array() ): object { return $this->resolve( $id, $parameters ); } /** * Forgets a resolved instance. * * @param string $id Service identifier. */ public function forget( string $id ): void { unset( $this->instances[ $id ] ); } /** * Resolves an identifier to an object. * * @param string $id Service identifier. * @param array $parameters Constructor arguments by name. * * @throws ContainerException When the service cannot be built. * * @return object The resolved service. */ private function resolve( string $id, array $parameters = array() ): object { if ( in_array( $id, $this->building, true ) ) { throw new ContainerException( sprintf( 'Circular dependency while resolving %s.', esc_html( $id ) ) ); } $this->building[] = $id; try { $concrete = $this->bindings[ $id ] ?? $id; if ( $concrete instanceof Closure ) { return $concrete( $this, $parameters ); } return $this->build( $concrete, $parameters ); } finally { array_pop( $this->building ); } } /** * Instantiates a class, resolving the dependencies it declares. * * @param string $class_name Class to build. * @param array $parameters Constructor arguments by name. * * @throws ContainerException When the class cannot be built. * * @return object The new instance. */ private function build( string $class_name, array $parameters ): object { if ( ! class_exists( $class_name ) ) { throw new ContainerException( sprintf( 'Service %s is not registered and is not a class.', esc_html( $class_name ) ) ); } $reflection = new ReflectionClass( $class_name ); if ( ! $reflection->isInstantiable() ) { throw new ContainerException( sprintf( 'Service %s cannot be instantiated. Bind it to a concrete class.', esc_html( $class_name ) ) ); } $constructor = $reflection->getConstructor(); if ( null === $constructor ) { return new $class_name(); } $arguments = array(); foreach ( $constructor->getParameters() as $parameter ) { $arguments[] = $this->argument( $parameter, $parameters, $class_name ); } return $reflection->newInstanceArgs( $arguments ); } /** * Works out the value of a single constructor argument. * * @param ReflectionParameter $parameter Argument being resolved. * @param array $parameters Arguments supplied by the caller. * @param string $class_name Class being built, used in error messages. * * @throws ContainerException When the argument cannot be resolved. * * @return mixed The argument value. */ private function argument( ReflectionParameter $parameter, array $parameters, string $class_name ): mixed { $name = $parameter->getName(); if ( array_key_exists( $name, $parameters ) ) { return $parameters[ $name ]; } $type = $parameter->getType(); if ( $type instanceof ReflectionNamedType && ! $type->isBuiltin() ) { $dependency = $type->getName(); if ( $this->has( $dependency ) ) { try { return $this->get( $dependency ); } catch ( ContainerException $exception ) { /* * A class that exists but cannot be built on its own, such as * one whose constructor wants credentials. Fall through to the * declared default rather than failing the whole graph. */ if ( ! $parameter->isDefaultValueAvailable() && ! $type->allowsNull() ) { throw $exception; } } } if ( $parameter->isDefaultValueAvailable() ) { return $parameter->getDefaultValue(); } if ( $type->allowsNull() ) { return null; } } if ( $parameter->isDefaultValueAvailable() ) { return $parameter->getDefaultValue(); } if ( $parameter->allowsNull() ) { return null; } throw new ContainerException( sprintf( 'Cannot resolve argument $%1$s of %2$s.', esc_html( $name ), esc_html( $class_name ) ) ); } }