Commit realizado el 12:13:52 08-04-2024
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface Conditional.
|
||||
*
|
||||
* @link https://www.mwpd.io/
|
||||
*
|
||||
* @copyright 2019 Alain Schlesser
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Original code modified for this project.
|
||||
*
|
||||
* @copyright 2021 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\Infrastructure;
|
||||
|
||||
/**
|
||||
* Something that can be instantiated conditionally.
|
||||
*
|
||||
* A class marked as being conditionally can be asked whether it should be
|
||||
* instantiated through a static method. An example would be a service that is
|
||||
* only available on the admin backend.
|
||||
*
|
||||
* This allows for a more systematic and automated optimization of how the
|
||||
* different parts of the plugin are enabled or disabled.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @since 1.6.0
|
||||
*/
|
||||
interface Conditional {
|
||||
|
||||
/**
|
||||
* Check whether the conditional object is currently needed.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @return bool Whether the conditional object is needed.
|
||||
*/
|
||||
public static function is_needed(): bool;
|
||||
}
|
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface Delayed.
|
||||
*
|
||||
* @link https://www.mwpd.io/
|
||||
*
|
||||
* @copyright 2019 Alain Schlesser
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Original code modified for this project.
|
||||
*
|
||||
* @copyright 2021 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\Infrastructure;
|
||||
|
||||
/**
|
||||
* Something that is delayed to a later point in the execution flow.
|
||||
*
|
||||
* A class marked as being delayed can return the action at which it requires
|
||||
* to be registered.
|
||||
*
|
||||
* This can be used to only register a given object after certain contextual
|
||||
* requirements are met, like registering a frontend rendering service only
|
||||
* after the loop has been set up.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @since 1.6.0
|
||||
*/
|
||||
interface Delayed {
|
||||
|
||||
/**
|
||||
* Get the action to use for registering the service.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @return string Registration action to use.
|
||||
*/
|
||||
public static function get_registration_action(): string;
|
||||
|
||||
|
||||
/**
|
||||
* Get the action priority to use for registering the service.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @return int Registration action priority to use.
|
||||
*/
|
||||
public static function get_registration_action_priority(): int;
|
||||
}
|
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface HasMeta.
|
||||
*
|
||||
* @link https://github.com/googleforcreators/web-stories-wp
|
||||
*
|
||||
* @copyright 2021 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\Infrastructure;
|
||||
|
||||
/**
|
||||
* Class registers meta.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @since 1.15.0
|
||||
*/
|
||||
interface HasMeta {
|
||||
/**
|
||||
* Register meta
|
||||
*
|
||||
* @since 1.15.0
|
||||
*/
|
||||
public function register_meta(): void;
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface HasRequirements.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2021 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\Infrastructure;
|
||||
|
||||
/**
|
||||
* Something that requires other services to be registered before it can be registered.
|
||||
*
|
||||
* A class marked as having requirements can return the list of services it requires
|
||||
* to be available before it can be registered.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @since 1.10.0
|
||||
*/
|
||||
interface HasRequirements {
|
||||
|
||||
/**
|
||||
* Get the list of service IDs required for this service to be registered.
|
||||
*
|
||||
* @since 1.11.0
|
||||
*
|
||||
* @return string[] List of required services.
|
||||
*/
|
||||
public static function get_requirements(): array;
|
||||
}
|
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface Injector.
|
||||
*
|
||||
* @link https://www.mwpd.io/
|
||||
*
|
||||
* @copyright 2019 Alain Schlesser
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Original code modified for this project.
|
||||
*
|
||||
* @copyright 2021 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\Infrastructure;
|
||||
|
||||
/**
|
||||
* The dependency injector should be the only piece of code doing actual
|
||||
* instantiations, with the following exceptions:
|
||||
* - Factories can instantiate directly.
|
||||
* - Value objects should be instantiated directly where they are being used.
|
||||
*
|
||||
* Through technical features like "binding" interfaces to classes or
|
||||
* "auto-wiring" to resolve all dependency of a class to be instantiated
|
||||
* automatically, the dependency injector allows for the largest part of the
|
||||
* code to adhere to the "Code against Interfaces, not Implementations"
|
||||
* principle.
|
||||
*
|
||||
* Finally, the dependency injector should be the only one to decide what
|
||||
* objects to "share" (always handing out the same instance) or not to share
|
||||
* (always returning a fresh new instance on each subsequent call). This
|
||||
* effectively gets rid of the dreaded Singletons.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @template T
|
||||
*/
|
||||
interface Injector extends Service {
|
||||
|
||||
/**
|
||||
* Make an object instance out of an interface or class.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $interface_or_class Interface or class to make an object instance out of.
|
||||
* @param string[] $arguments Optional. Additional arguments to pass to the constructor.
|
||||
* Defaults to an empty array.
|
||||
* @return C Instantiated object.
|
||||
*
|
||||
* @phpstan-param class-string<C> $interface_or_class Interface or class to make an object instance out of.
|
||||
* @phpstan-param class-string[] $arguments Optional. Additional arguments to pass to the constructor.
|
||||
* Defaults to an empty array.
|
||||
*
|
||||
* @phpstan-return C
|
||||
*
|
||||
* @template C
|
||||
*/
|
||||
public function make( string $interface_or_class, array $arguments = [] );
|
||||
|
||||
/**
|
||||
* Bind a given interface or class to an implementation.
|
||||
*
|
||||
* Note: The implementation can be an interface as well, as long as it can
|
||||
* be resolved to an instantiatable class at runtime.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $from Interface or class to bind an implementation to.
|
||||
* @param string $to Interface or class that provides the implementation.
|
||||
*
|
||||
* @phpstan-param class-string<T> $from Interface or class to bind an implementation to.
|
||||
* @phpstan-param class-string<T> $to Interface or class that provides the implementation.
|
||||
*/
|
||||
public function bind( string $from, string $to ): Injector;
|
||||
|
||||
/**
|
||||
* Bind an argument for a class to a specific value.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $interface_or_class Interface or class to bind an argument
|
||||
* for.
|
||||
* @param string $argument_name Argument name to bind a value to.
|
||||
* @param mixed $value Value to bind the argument to.
|
||||
*
|
||||
* @phpstan-param class-string<T> $interface_or_class Interface or class to bind an argument
|
||||
*/
|
||||
public function bind_argument(
|
||||
string $interface_or_class,
|
||||
string $argument_name,
|
||||
$value
|
||||
): Injector;
|
||||
|
||||
/**
|
||||
* Always reuse and share the same instance for the provided interface or
|
||||
* class.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $interface_or_class Interface or class to reuse.
|
||||
*
|
||||
* @phpstan-param class-string<T> $interface_or_class Interface or class to reuse.
|
||||
*/
|
||||
public function share( string $interface_or_class ): Injector;
|
||||
|
||||
/**
|
||||
* Delegate instantiation of an interface or class to a callable.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $interface_or_class Interface or class to delegate the
|
||||
* instantiation of.
|
||||
* @param callable $callback Callable to use for instantiation.
|
||||
*
|
||||
* @phpstan-param class-string<T> $interface_or_class Interface or class to delegate the
|
||||
* instantiation of.
|
||||
*/
|
||||
public function delegate( string $interface_or_class, callable $callback ): Injector;
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* Final class FallbackInstantiator.
|
||||
*
|
||||
* @link https://github.com/googleforcreators/web-stories-wp
|
||||
*
|
||||
* @copyright 2021 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* Original code modified for this project.
|
||||
*
|
||||
* @copyright 2021 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\Infrastructure\Injector;
|
||||
|
||||
use Google\Web_Stories\Infrastructure\Instantiator;
|
||||
|
||||
/**
|
||||
* Fallback instantiator to use in case none was provided.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @since 1.6.0
|
||||
*/
|
||||
final class FallbackInstantiator implements Instantiator {
|
||||
|
||||
/**
|
||||
* Make an object instance out of an interface or class.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param class-string $class_name Class to make an object instance out of.
|
||||
* @param array<int, mixed> $dependencies Optional. Dependencies of the class.
|
||||
* @return T Instantiated object.
|
||||
*
|
||||
* @template T
|
||||
*
|
||||
* @phpstan-param class-string<T> $class_name Class to make an object instance out of.
|
||||
*/
|
||||
public function instantiate( string $class_name, array $dependencies = [] ) {
|
||||
return new $class_name( ...$dependencies );
|
||||
}
|
||||
}
|
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
/**
|
||||
* Final class InjectionChain.
|
||||
*
|
||||
* @link https://www.mwpd.io/
|
||||
*
|
||||
* @copyright 2019 Alain Schlesser
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Original code modified for this project.
|
||||
*
|
||||
* @copyright 2021 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\Infrastructure\Injector;
|
||||
|
||||
/**
|
||||
* The injection chain is similar to a trace, keeping track of what we have done
|
||||
* so far and at what depth within the auto-wiring we currently are.
|
||||
*
|
||||
* It is used to detect circular dependencies, and can also be dumped for
|
||||
* debugging information.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @template T
|
||||
*/
|
||||
final class InjectionChain {
|
||||
|
||||
/**
|
||||
* Chain.
|
||||
*
|
||||
* @var string[]
|
||||
* @phpstan-var class-string<T>[]
|
||||
*/
|
||||
private array $chain = [];
|
||||
|
||||
/**
|
||||
* Resolutions.
|
||||
*
|
||||
* @var array<bool>
|
||||
* @phpstan-var array<class-string, bool>
|
||||
*/
|
||||
private array $resolutions = [];
|
||||
|
||||
/**
|
||||
* Add class to injection chain.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $class_name Class to add to injection chain.
|
||||
* @return self Modified injection chain.
|
||||
*
|
||||
* @phpstan-param class-string<T> $class_name
|
||||
*/
|
||||
public function add_to_chain( string $class_name ): self {
|
||||
$new_chain = clone $this;
|
||||
$new_chain->chain[] = $class_name;
|
||||
|
||||
return $new_chain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add resolution for circular reference detection.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $resolution Resolution to add.
|
||||
* @return self Modified injection chain.
|
||||
*
|
||||
* @phpstan-param class-string $resolution
|
||||
*/
|
||||
public function add_resolution( string $resolution ): self {
|
||||
$new_chain = clone $this;
|
||||
$new_chain->resolutions[ $resolution ] = true;
|
||||
|
||||
return $new_chain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last class that was pushed to the injection chain.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @throws \LogicException If the injection chain is accessed too early.
|
||||
*
|
||||
* @return string Last class pushed to the injection chain.
|
||||
*
|
||||
* @phpstan-return class-string<T>
|
||||
*/
|
||||
public function get_class(): string {
|
||||
if ( empty( $this->chain ) ) {
|
||||
throw new \LogicException(
|
||||
'Access to injection chain before any resolution was made.'
|
||||
);
|
||||
}
|
||||
|
||||
return \end( $this->chain ) ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the injection chain.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @return string[] Chain of injections.
|
||||
*
|
||||
* @phpstan-return class-string[]
|
||||
*/
|
||||
public function get_chain(): array {
|
||||
return \array_reverse( $this->chain );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the injection chain already has a given resolution.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $resolution Resolution to check for.
|
||||
* @return bool Whether the resolution was found.
|
||||
*
|
||||
* @phpstan-param class-string<T> $resolution
|
||||
*/
|
||||
public function has_resolution( string $resolution ): bool {
|
||||
return \array_key_exists( $resolution, $this->resolutions );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the injection chain already encountered a class.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $class_name Class to check.
|
||||
* @return bool Whether the given class is already part of the chain.
|
||||
*/
|
||||
public function is_in_chain( string $class_name ): bool {
|
||||
return \in_array( $class_name, $this->chain, true );
|
||||
}
|
||||
}
|
@@ -0,0 +1,540 @@
|
||||
<?php
|
||||
/**
|
||||
* Final class SimpleInjector.
|
||||
*
|
||||
* @link https://www.mwpd.io/
|
||||
*
|
||||
* @copyright 2019 Alain Schlesser
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Original code modified for this project.
|
||||
*
|
||||
* @copyright 2021 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\Infrastructure\Injector;
|
||||
|
||||
use Google\Web_Stories\Exception\FailedToMakeInstance;
|
||||
use Google\Web_Stories\Infrastructure\Injector;
|
||||
use Google\Web_Stories\Infrastructure\Instantiator;
|
||||
use ReflectionClass;
|
||||
use ReflectionNamedType;
|
||||
use ReflectionParameter;
|
||||
use function array_map;
|
||||
|
||||
/**
|
||||
* A simplified implementation of a dependency injector.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @template T
|
||||
*/
|
||||
final class SimpleInjector implements Injector {
|
||||
|
||||
/**
|
||||
* Special-case index key for handling globally defined named arguments.
|
||||
*/
|
||||
public const GLOBAL_ARGUMENTS = '__global__';
|
||||
|
||||
/**
|
||||
* Mappings.
|
||||
*
|
||||
* @var string[]
|
||||
* @phpstan-var class-string<T>[]
|
||||
*/
|
||||
private array $mappings = [];
|
||||
|
||||
/**
|
||||
* Shared instances
|
||||
*
|
||||
* @var array<T|null>
|
||||
*/
|
||||
private array $shared_instances = [];
|
||||
|
||||
/**
|
||||
* Delegates.
|
||||
*
|
||||
* @var callable[]
|
||||
*/
|
||||
private array $delegates = [];
|
||||
|
||||
/**
|
||||
* Argument mappings.
|
||||
*
|
||||
* @var array<string, array<mixed>>
|
||||
*/
|
||||
private array $argument_mappings = [
|
||||
self::GLOBAL_ARGUMENTS => [],
|
||||
];
|
||||
|
||||
/**
|
||||
* Instantiator.
|
||||
*
|
||||
* @var Instantiator<T>
|
||||
*/
|
||||
private Instantiator $instantiator;
|
||||
|
||||
/**
|
||||
* Instantiate a SimpleInjector object.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param Instantiator|null $instantiator Optional. Instantiator to use.
|
||||
*/
|
||||
public function __construct( ?Instantiator $instantiator = null ) {
|
||||
$this->instantiator = $instantiator ?? new FallbackInstantiator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an object instance out of an interface or class.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $interface_or_class Interface or class to make an object instance out of.
|
||||
* @param array<string, mixed> $arguments Optional. Additional arguments to pass to the constructor.
|
||||
* Defaults to an empty array.
|
||||
* @return T Instantiated object.
|
||||
*
|
||||
* @phpstan-param class-string<T> $interface_or_class Interface or class to make an object instance out of.
|
||||
* @phpstan-param array<string, mixed> $arguments Optional. Additional arguments to pass to the constructor.
|
||||
*/
|
||||
public function make( string $interface_or_class, array $arguments = [] ) {
|
||||
$injection_chain = $this->resolve(
|
||||
new InjectionChain(),
|
||||
$interface_or_class
|
||||
);
|
||||
|
||||
$class = $injection_chain->get_class();
|
||||
|
||||
if ( $this->has_shared_instance( $class ) ) {
|
||||
return $this->get_shared_instance( $class );
|
||||
}
|
||||
|
||||
if ( $this->has_delegate( $class ) ) {
|
||||
$delegate = $this->get_delegate( $class );
|
||||
$object = $delegate( $class );
|
||||
} else {
|
||||
$reflection = $this->get_class_reflection( $class );
|
||||
$this->ensure_is_instantiable( $reflection );
|
||||
|
||||
$dependencies = $this->get_dependencies_for(
|
||||
$injection_chain,
|
||||
$reflection,
|
||||
$arguments
|
||||
);
|
||||
|
||||
$object = $this->instantiator->instantiate( $class, $dependencies );
|
||||
}
|
||||
|
||||
if ( \array_key_exists( $class, $this->shared_instances ) ) {
|
||||
$this->shared_instances[ $class ] = $object;
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind a given interface or class to an implementation.
|
||||
*
|
||||
* Note: The implementation can be an interface as well, as long as it can
|
||||
* be resolved to an instantiatable class at runtime.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $from Interface or class to bind an implementation to.
|
||||
* @param string $to Interface or class that provides the implementation.
|
||||
*
|
||||
* @phpstan-param class-string<T> $from Interface or class to bind an implementation to.
|
||||
* @phpstan-param class-string<T> $to Interface or class that provides the implementation.
|
||||
*/
|
||||
public function bind( string $from, string $to ): Injector {
|
||||
$this->mappings[ $from ] = $to;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind an argument for a class to a specific value.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $interface_or_class Interface or class to bind an argument for.
|
||||
* @param string $argument_name Argument name to bind a value to.
|
||||
* @param mixed $value Value to bind the argument to.
|
||||
*/
|
||||
public function bind_argument(
|
||||
string $interface_or_class,
|
||||
string $argument_name,
|
||||
$value
|
||||
): Injector {
|
||||
$this->argument_mappings[ $interface_or_class ][ $argument_name ] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Always reuse and share the same instance for the provided interface or
|
||||
* class.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $interface_or_class Interface or class to reuse.
|
||||
*
|
||||
* @phpstan-param class-string<T> $interface_or_class Interface or class to reuse.
|
||||
*/
|
||||
public function share( string $interface_or_class ): Injector {
|
||||
$this->shared_instances[ $interface_or_class ] = null;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delegate instantiation of an interface or class to a callable.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $interface_or_class Interface or class to delegate the instantiation of.
|
||||
* @param callable $callback Callable to use for instantiation.
|
||||
*
|
||||
* @phpstan-param class-string<T> $interface_or_class Interface or class to delegate the instantiation of.
|
||||
*/
|
||||
public function delegate( string $interface_or_class, callable $callback ): Injector {
|
||||
$this->delegates[ $interface_or_class ] = $callback;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an object instance out of an interface or class.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param InjectionChain $injection_chain Injection chain to track resolutions.
|
||||
* @param string $interface_or_class Interface or class to make an object instance out of.
|
||||
* @return T Instantiated object.
|
||||
*
|
||||
* @phpstan-param class-string<T> $interface_or_class Interface or class to make an object instance out of.
|
||||
*/
|
||||
private function make_dependency(
|
||||
InjectionChain $injection_chain,
|
||||
string $interface_or_class
|
||||
) {
|
||||
$injection_chain = $this->resolve(
|
||||
$injection_chain,
|
||||
$interface_or_class
|
||||
);
|
||||
|
||||
$class = $injection_chain->get_class();
|
||||
|
||||
if ( $this->has_shared_instance( $class ) ) {
|
||||
return $this->get_shared_instance( $class );
|
||||
}
|
||||
|
||||
if ( $this->has_delegate( $class ) ) {
|
||||
$delegate = $this->get_delegate( $class );
|
||||
return $delegate( $class );
|
||||
}
|
||||
|
||||
$reflection = $this->get_class_reflection( $class );
|
||||
$this->ensure_is_instantiable( $reflection );
|
||||
|
||||
$dependencies = $this->get_dependencies_for(
|
||||
$injection_chain,
|
||||
$reflection
|
||||
);
|
||||
|
||||
$object = $this->instantiator->instantiate( $class, $dependencies );
|
||||
|
||||
if ( \array_key_exists( $class, $this->shared_instances ) ) {
|
||||
$this->shared_instances[ $class ] = $object;
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively resolve an interface to the class it should be bound to.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @throws FailedToMakeInstance If a circular reference was detected.
|
||||
*
|
||||
* @param InjectionChain $injection_chain Injection chain to track resolutions.
|
||||
* @param string $interface_or_class Interface or class to resolve.
|
||||
* @return InjectionChain Modified Injection chain
|
||||
*
|
||||
* @phpstan-param class-string<T> $interface_or_class Interface or class to resolve.
|
||||
*/
|
||||
private function resolve(
|
||||
InjectionChain $injection_chain,
|
||||
string $interface_or_class
|
||||
): InjectionChain {
|
||||
if ( $injection_chain->is_in_chain( $interface_or_class ) ) {
|
||||
// Circular reference detected, aborting.
|
||||
throw FailedToMakeInstance::for_circular_reference(
|
||||
$interface_or_class, // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
$injection_chain // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
);
|
||||
}
|
||||
|
||||
$injection_chain = $injection_chain->add_resolution( $interface_or_class );
|
||||
|
||||
if ( \array_key_exists( $interface_or_class, $this->mappings ) ) {
|
||||
return $this->resolve(
|
||||
$injection_chain,
|
||||
$this->mappings[ $interface_or_class ]
|
||||
);
|
||||
}
|
||||
|
||||
return $injection_chain->add_to_chain( $interface_or_class );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array of constructor dependencies for a given reflected class.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param InjectionChain $injection_chain Injection chain to track resolutions.
|
||||
* @param ReflectionClass $reflection Reflected class to get the dependencies for.
|
||||
* @param array<string, mixed> $arguments Associative array of directly provided arguments.
|
||||
* @return array<int, mixed> Array of dependencies that represent the arguments for the class' constructor.
|
||||
*/
|
||||
private function get_dependencies_for(
|
||||
InjectionChain $injection_chain,
|
||||
ReflectionClass $reflection,
|
||||
array $arguments = []
|
||||
): array {
|
||||
$constructor = $reflection->getConstructor();
|
||||
$class = $reflection->getName();
|
||||
|
||||
if ( null === $constructor ) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map(
|
||||
fn( ReflectionParameter $parameter ) => $this->resolve_argument(
|
||||
$injection_chain,
|
||||
$class,
|
||||
$parameter,
|
||||
$arguments
|
||||
),
|
||||
$constructor->getParameters()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that a given reflected class is instantiable.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @throws FailedToMakeInstance If the interface could not be resolved.
|
||||
*
|
||||
* @param ReflectionClass $reflection Reflected class to check.
|
||||
*/
|
||||
private function ensure_is_instantiable( ReflectionClass $reflection ): void {
|
||||
if ( ! $reflection->isInstantiable() ) {
|
||||
throw FailedToMakeInstance::for_unresolved_interface( $reflection->getName() ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a given reflected argument.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param InjectionChain $injection_chain Injection chain to track resolutions.
|
||||
* @param string $class_name Name of the class to resolve the arguments for.
|
||||
* @param ReflectionParameter $parameter Parameter to resolve.
|
||||
* @param array<string, mixed> $arguments Associative array of directly provided arguments.
|
||||
* @return mixed Resolved value of the argument.
|
||||
*/
|
||||
private function resolve_argument(
|
||||
InjectionChain $injection_chain,
|
||||
string $class_name,
|
||||
ReflectionParameter $parameter,
|
||||
array $arguments
|
||||
) {
|
||||
if ( ! $parameter->hasType() ) {
|
||||
return $this->resolve_argument_by_name(
|
||||
$class_name,
|
||||
$parameter,
|
||||
$arguments
|
||||
);
|
||||
}
|
||||
|
||||
$type = $parameter->getType();
|
||||
|
||||
// In PHP 8.0, the isBuiltin method was removed from the parent {@see ReflectionType} class.
|
||||
if ( null === $type || ( $type instanceof ReflectionNamedType && $type->isBuiltin() ) ) {
|
||||
return $this->resolve_argument_by_name(
|
||||
$class_name,
|
||||
$parameter,
|
||||
$arguments
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface or class.
|
||||
*
|
||||
* @var class-string<T> $type
|
||||
*/
|
||||
$type = $type instanceof ReflectionNamedType
|
||||
? $type->getName()
|
||||
: (string) $type;
|
||||
|
||||
return $this->make_dependency( $injection_chain, $type );
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a given reflected argument by its name.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @throws FailedToMakeInstance If the argument could not be resolved.
|
||||
*
|
||||
* @param string $class_name Class to resolve the argument for.
|
||||
* @param ReflectionParameter $parameter Argument to resolve by name.
|
||||
* @param array<string, mixed> $arguments Associative array of directly provided arguments.
|
||||
* @return mixed Resolved value of the argument.
|
||||
*/
|
||||
private function resolve_argument_by_name(
|
||||
string $class_name,
|
||||
ReflectionParameter $parameter,
|
||||
array $arguments
|
||||
) {
|
||||
$name = $parameter->getName();
|
||||
|
||||
// The argument was directly provided to the make() call.
|
||||
if ( \array_key_exists( $name, $arguments ) ) {
|
||||
return $arguments[ $name ];
|
||||
}
|
||||
|
||||
// Check if we have mapped this argument for the specific class.
|
||||
if ( \array_key_exists( $class_name, $this->argument_mappings )
|
||||
&& \array_key_exists( $name, $this->argument_mappings[ $class_name ] ) ) {
|
||||
$value = $this->argument_mappings[ $class_name ][ $name ];
|
||||
|
||||
// Closures are immediately resolved, to provide lazy resolution.
|
||||
if ( \is_callable( $value ) ) {
|
||||
$value = $value( $class_name, $parameter, $arguments );
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
// No argument found for the class, check if we have a global value.
|
||||
if ( \array_key_exists( $name, $this->argument_mappings[ self::GLOBAL_ARGUMENTS ] ) ) {
|
||||
return $this->argument_mappings[ self::GLOBAL_ARGUMENTS ][ $name ];
|
||||
}
|
||||
|
||||
// No provided argument found, check if it has a default value.
|
||||
try {
|
||||
if ( $parameter->isDefaultValueAvailable() ) {
|
||||
return $parameter->getDefaultValue();
|
||||
}
|
||||
} catch ( \Exception $exception ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
|
||||
// Just fall through into the FailedToMakeInstance exception.
|
||||
}
|
||||
|
||||
// Out of options, fail with an exception.
|
||||
throw FailedToMakeInstance::for_unresolved_argument( $name, $class_name ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a shared instance exists for a given class.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $class_name Class to check for a shared instance.
|
||||
* @return bool Whether a shared instance exists.
|
||||
*/
|
||||
private function has_shared_instance( string $class_name ): bool {
|
||||
return \array_key_exists( $class_name, $this->shared_instances )
|
||||
&& null !== $this->shared_instances[ $class_name ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shared instance for a given class.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @throws FailedToMakeInstance If an uninstantiated shared instance is requested.
|
||||
*
|
||||
* @param string $class_name Class to get the shared instance for.
|
||||
* @return T Shared instance.
|
||||
*
|
||||
* @phpstan-param class-string<T> $class_name Class to get the shared instance for.
|
||||
* @phpstan-return T Shared instance.
|
||||
*/
|
||||
private function get_shared_instance( string $class_name ) {
|
||||
if ( ! $this->has_shared_instance( $class_name ) ) {
|
||||
throw FailedToMakeInstance::for_uninstantiated_shared_instance( $class_name ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shared instance.
|
||||
*
|
||||
* @var T $instance
|
||||
*/
|
||||
$instance = $this->shared_instances[ $class_name ];
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a delegate exists for a given class.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $class_name Class to check for a delegate.
|
||||
* @return bool Whether a delegate exists.
|
||||
*/
|
||||
private function has_delegate( string $class_name ): bool {
|
||||
return \array_key_exists( $class_name, $this->delegates );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the delegate for a given class.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @throws FailedToMakeInstance If an invalid delegate is requested.
|
||||
*
|
||||
* @param string $class_name Class to get the delegate for.
|
||||
* @return callable Delegate.
|
||||
*/
|
||||
private function get_delegate( string $class_name ): callable {
|
||||
if ( ! $this->has_delegate( $class_name ) ) {
|
||||
throw FailedToMakeInstance::for_invalid_delegate( $class_name ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
}
|
||||
|
||||
return $this->delegates[ $class_name ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reflection for a class or throw an exception.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @throws FailedToMakeInstance If the class could not be reflected.
|
||||
*
|
||||
* @param string|class-string $class_name Class to get the reflection for.
|
||||
* @return ReflectionClass Class reflection.
|
||||
*/
|
||||
private function get_class_reflection( string $class_name ): ReflectionClass {
|
||||
if ( ! class_exists( $class_name ) ) {
|
||||
throw FailedToMakeInstance::for_unreflectable_class( $class_name ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
}
|
||||
|
||||
// There should be no ReflectionException happening because of the class existence check above.
|
||||
return new ReflectionClass( $class_name );
|
||||
}
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface Instantiator.
|
||||
*
|
||||
* @link https://www.mwpd.io/
|
||||
*
|
||||
* @copyright 2019 Alain Schlesser
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Original code modified for this project.
|
||||
*
|
||||
* @copyright 2021 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\Infrastructure;
|
||||
|
||||
/**
|
||||
* Interface to make the act of instantiation extensible/replaceable.
|
||||
*
|
||||
* This way, a more elaborate mechanism can be plugged in, like using
|
||||
* ProxyManager to instantiate proxies instead of actual objects.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @template T
|
||||
*/
|
||||
interface Instantiator {
|
||||
/**
|
||||
* Make an object instance out of an interface or class.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $class_name Class to make an object instance out of.
|
||||
* @param array<int, mixed> $dependencies Optional. Dependencies of the class.
|
||||
* @return T Instantiated object.
|
||||
*
|
||||
* @phpstan-param class-string<T> $class_name Class to make an object instance out of.
|
||||
*/
|
||||
public function instantiate( string $class_name, array $dependencies = [] );
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface Plugin
|
||||
*
|
||||
* @link https://www.mwpd.io/
|
||||
*
|
||||
* @copyright 2019 Alain Schlesser
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Original code modified for this project.
|
||||
*
|
||||
* @copyright 2021 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\Infrastructure;
|
||||
|
||||
/**
|
||||
* A plugin is basically nothing more than a convention on how manage the
|
||||
* lifecycle of a modular piece of code, so that you can:
|
||||
* 1. activate it,
|
||||
* 2. register it with the framework, and
|
||||
* 3. deactivate it again.
|
||||
*
|
||||
* This is what this interface represents, by assembling the separate,
|
||||
* segregated interfaces for each of these lifecycle actions.
|
||||
*
|
||||
* Additionally, we provide a means to get access to the plugin's container that
|
||||
* collects all the services it is made up of. This allows direct access to the
|
||||
* services to outside code if needed.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @since 1.6.0
|
||||
*/
|
||||
interface Plugin extends PluginActivationAware, PluginDeactivationAware, Registerable, SiteInitializationAware, SiteRemovalAware {
|
||||
|
||||
/**
|
||||
* Get the service container that contains the services that make up the
|
||||
* plugin.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @return ServiceContainer<Service> Service container of the plugin.
|
||||
*/
|
||||
public function get_container(): ServiceContainer;
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface PluginActivationAware.
|
||||
*
|
||||
* @link https://www.mwpd.io/
|
||||
*
|
||||
* @copyright 2019 Alain Schlesser
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Original code modified for this project.
|
||||
*
|
||||
* @copyright 2021 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\Infrastructure;
|
||||
|
||||
/**
|
||||
* Something that can be activated.
|
||||
*
|
||||
* By tagging a service with this interface, the system will automatically hook
|
||||
* it up to the WordPress activation hook.
|
||||
*
|
||||
* This way, we can just add the simple interface marker and not worry about how
|
||||
* to wire up the code to reach that part during the static activation hook.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @since 1.6.0
|
||||
*/
|
||||
interface PluginActivationAware {
|
||||
|
||||
/**
|
||||
* Act on plugin activation.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param bool $network_wide Whether the activation was done network-wide.
|
||||
*/
|
||||
public function on_plugin_activation( bool $network_wide ): void;
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface PluginDeactivationAware.
|
||||
*
|
||||
* @link https://www.mwpd.io/
|
||||
*
|
||||
* @copyright 2019 Alain Schlesser
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Original code modified for this project.
|
||||
*
|
||||
* @copyright 2021 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\Infrastructure;
|
||||
|
||||
/**
|
||||
* Something that can be deactivated.
|
||||
*
|
||||
* By tagging a service with this interface, the system will automatically hook
|
||||
* it up to the WordPress deactivation hook.
|
||||
*
|
||||
* This way, we can just add the simple interface marker and not worry about how
|
||||
* to wire up the code to reach that part during the static deactivation hook.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @since 1.6.0
|
||||
*/
|
||||
interface PluginDeactivationAware {
|
||||
|
||||
/**
|
||||
* Act on plugin deactivation.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param bool $network_wide Whether the deactivation was done network-wide.
|
||||
*/
|
||||
public function on_plugin_deactivation( bool $network_wide ): void;
|
||||
}
|
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface PluginUninstallAware.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Copyright 2022 Google LLC
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\Infrastructure;
|
||||
|
||||
/**
|
||||
* Something that can be uninstalled.
|
||||
*
|
||||
* By tagging a service with this interface, the system will automatically hook
|
||||
* it up to the WordPress uninstall hook.
|
||||
*
|
||||
* This way, we can just add the simple interface marker and not worry about how
|
||||
* to wire up the code to reach that part during the static uninstall hook.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @since 1.26.0
|
||||
*/
|
||||
interface PluginUninstallAware {
|
||||
|
||||
/**
|
||||
* Act on plugin uninstall.
|
||||
*
|
||||
* @since 1.26.0
|
||||
*/
|
||||
public function on_plugin_uninstall(): void;
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface Registerable.
|
||||
*
|
||||
* @link https://www.mwpd.io/
|
||||
*
|
||||
* @copyright 2019 Alain Schlesser
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Original code modified for this project.
|
||||
*
|
||||
* @copyright 2021 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\Infrastructure;
|
||||
|
||||
/**
|
||||
* Something that can be registered.
|
||||
*
|
||||
* For a clean code base, a class instantiation should never have side-effects,
|
||||
* only initialize the internals of the object so that it is ready to be used.
|
||||
*
|
||||
* This means, though, that the system does not have any knowledge of the
|
||||
* objects when they are merely instantiated.
|
||||
*
|
||||
* Registering such an object is the explicit act of making it known to the
|
||||
* overarching system.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @since 1.6.0
|
||||
*/
|
||||
interface Registerable {
|
||||
|
||||
/**
|
||||
* Register the service.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*/
|
||||
public function register(): void;
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface Service.
|
||||
*
|
||||
* @link https://www.mwpd.io/
|
||||
*
|
||||
* @copyright 2019 Alain Schlesser
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Original code modified for this project.
|
||||
*
|
||||
* @copyright 2021 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\Infrastructure;
|
||||
|
||||
/**
|
||||
* A conceptual service.
|
||||
*
|
||||
* Splitting the logic up into independent services makes the approach of
|
||||
* assembling a plugin more systematic and scalable and lowers the cognitive
|
||||
* load when the code base increases in size.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @since 1.6.0
|
||||
*/
|
||||
interface Service {
|
||||
|
||||
}
|
@@ -0,0 +1,919 @@
|
||||
<?php
|
||||
/**
|
||||
* Class ServiceBasedPlugin.
|
||||
*
|
||||
* @link https://www.mwpd.io/
|
||||
*
|
||||
* @copyright 2019 Alain Schlesser
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Original code modified for this project.
|
||||
*
|
||||
* @copyright 2021 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\Infrastructure;
|
||||
|
||||
use Google\Web_Stories\Exception\InvalidService;
|
||||
use Google\Web_Stories\Infrastructure\ServiceContainer\LazilyInstantiatedService;
|
||||
use WP_Site;
|
||||
use function add_action;
|
||||
use function apply_filters;
|
||||
use function did_action;
|
||||
use const WPCOM_IS_VIP_ENV;
|
||||
|
||||
/**
|
||||
* This abstract base plugin provides all the boilerplate code for working with
|
||||
* the dependency injector and the service container.
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
|
||||
*
|
||||
* @template C of Conditional
|
||||
* @template D of Delayed
|
||||
* @template H of HasRequirements
|
||||
* @template R of Registerable
|
||||
* @template S of Service
|
||||
*/
|
||||
abstract class ServiceBasedPlugin implements Plugin {
|
||||
|
||||
// Main filters to control the flow of the plugin from outside code.
|
||||
public const SERVICES_FILTER = 'services';
|
||||
public const BINDINGS_FILTER = 'bindings';
|
||||
public const ARGUMENTS_FILTER = 'arguments';
|
||||
public const SHARED_INSTANCES_FILTER = 'shared_instances';
|
||||
public const DELEGATIONS_FILTER = 'delegations';
|
||||
|
||||
// Service identifier for the injector.
|
||||
public const INJECTOR_ID = 'injector';
|
||||
|
||||
// WordPress action to trigger the service registration on.
|
||||
// Use false to register as soon as the code is loaded.
|
||||
public const REGISTRATION_ACTION = false;
|
||||
|
||||
// Whether to enable filtering by default or not.
|
||||
public const ENABLE_FILTERS_DEFAULT = true;
|
||||
|
||||
// Prefixes to use.
|
||||
public const HOOK_PREFIX = '';
|
||||
public const SERVICE_PREFIX = '';
|
||||
|
||||
// Pattern used for detecting capitals to turn PascalCase into snake_case.
|
||||
public const DETECT_CAPITALS_REGEX_PATTERN = '/[A-Z]([A-Z](?![a-z]))*/';
|
||||
|
||||
/**
|
||||
* Enable filters.
|
||||
*/
|
||||
protected bool $enable_filters;
|
||||
|
||||
/**
|
||||
* Injector.
|
||||
*/
|
||||
protected Injector $injector;
|
||||
|
||||
/**
|
||||
* ServiceContainer.
|
||||
*
|
||||
* @var ServiceContainer<Service>
|
||||
*/
|
||||
protected ServiceContainer $service_container;
|
||||
|
||||
/**
|
||||
* Instantiate a Theme object.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param bool|null $enable_filters Optional. Whether to enable filtering of the injector configuration.
|
||||
* @param Injector|null $injector Optional. Injector instance to use.
|
||||
* @param ServiceContainer<Service>|null $service_container Optional. Service container instance to use.
|
||||
*/
|
||||
public function __construct(
|
||||
?bool $enable_filters = null,
|
||||
?Injector $injector = null,
|
||||
?ServiceContainer $service_container = null
|
||||
) {
|
||||
/*
|
||||
* We use what is commonly referred to as a "poka-yoke" here.
|
||||
*
|
||||
* We need an injector and a container. We make them injectable so that
|
||||
* we can easily provide overrides for testing, but we also make them
|
||||
* optional and provide default implementations for easy regular usage.
|
||||
*/
|
||||
|
||||
$this->enable_filters = $enable_filters ?? static::ENABLE_FILTERS_DEFAULT;
|
||||
|
||||
$this->injector = $injector ?? new Injector\SimpleInjector();
|
||||
|
||||
$this->injector = $this->configure_injector( $this->injector );
|
||||
|
||||
$this->service_container = $service_container ?? new ServiceContainer\SimpleServiceContainer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Act on plugin activation.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param bool $network_wide Whether the activation was done network-wide.
|
||||
*/
|
||||
public function on_plugin_activation( bool $network_wide ): void {
|
||||
$this->register_services();
|
||||
|
||||
/**
|
||||
* Service ID.
|
||||
*
|
||||
* @var string $id
|
||||
*/
|
||||
foreach ( $this->service_container as $id => $service ) {
|
||||
// Using ->get() here to instantiate LazilyInstantiatedService too.
|
||||
$service = $this->service_container->get( $id );
|
||||
|
||||
if ( $service instanceof PluginActivationAware ) {
|
||||
$service->on_plugin_activation( $network_wide );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! \defined( '\WPCOM_IS_VIP_ENV' ) || false === WPCOM_IS_VIP_ENV ) {
|
||||
flush_rewrite_rules( false );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Act on plugin deactivation.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param bool $network_wide Whether the deactivation was done network-wide.
|
||||
*/
|
||||
public function on_plugin_deactivation( bool $network_wide ): void {
|
||||
$this->register_services();
|
||||
|
||||
/**
|
||||
* Service ID.
|
||||
*
|
||||
* @var string $id
|
||||
*/
|
||||
foreach ( $this->service_container as $id => $service ) {
|
||||
// Using ->get() here to instantiate LazilyInstantiatedService too.
|
||||
$service = $this->service_container->get( $id );
|
||||
|
||||
if ( $service instanceof PluginDeactivationAware ) {
|
||||
$service->on_plugin_deactivation( $network_wide );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! \defined( '\WPCOM_IS_VIP_ENV' ) || false === WPCOM_IS_VIP_ENV ) {
|
||||
flush_rewrite_rules( false );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Act on site initialization on Multisite.
|
||||
*
|
||||
* @since 1.11.0
|
||||
*
|
||||
* @param WP_Site $site The site being initialized.
|
||||
*/
|
||||
public function on_site_initialization( WP_Site $site ): void {
|
||||
$this->register_services();
|
||||
|
||||
$site_id = (int) $site->blog_id;
|
||||
|
||||
// phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.switch_to_blog_switch_to_blog
|
||||
switch_to_blog( $site_id );
|
||||
|
||||
/**
|
||||
* Service ID.
|
||||
*
|
||||
* @var string $id
|
||||
*/
|
||||
foreach ( $this->service_container as $id => $service ) {
|
||||
// Using ->get() here to instantiate LazilyInstantiatedService too.
|
||||
$service = $this->service_container->get( $id );
|
||||
|
||||
if ( $service instanceof SiteInitializationAware ) {
|
||||
$service->on_site_initialization( $site );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! \defined( '\WPCOM_IS_VIP_ENV' ) || false === WPCOM_IS_VIP_ENV ) {
|
||||
flush_rewrite_rules( false );
|
||||
}
|
||||
|
||||
restore_current_blog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Act on site removal on Multisite.
|
||||
*
|
||||
* @since 1.11.0
|
||||
*
|
||||
* @param WP_Site $site The site being removed.
|
||||
*/
|
||||
public function on_site_removal( WP_Site $site ): void {
|
||||
$this->register_services();
|
||||
|
||||
$site_id = (int) $site->blog_id;
|
||||
|
||||
// phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.switch_to_blog_switch_to_blog
|
||||
switch_to_blog( $site_id );
|
||||
|
||||
/**
|
||||
* Service ID.
|
||||
*
|
||||
* @var string $id
|
||||
*/
|
||||
foreach ( $this->service_container as $id => $service ) {
|
||||
// Using ->get() here to instantiate LazilyInstantiatedService too.
|
||||
$service = $this->service_container->get( $id );
|
||||
|
||||
if ( $service instanceof SiteRemovalAware ) {
|
||||
$service->on_site_removal( $site );
|
||||
}
|
||||
}
|
||||
|
||||
restore_current_blog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Act on site is uninstalled.
|
||||
*
|
||||
* @since 1.26.0
|
||||
*/
|
||||
public function on_site_uninstall(): void {
|
||||
$this->register_services();
|
||||
|
||||
/**
|
||||
* Service ID.
|
||||
*
|
||||
* @var string $id
|
||||
*/
|
||||
foreach ( $this->service_container as $id => $service ) {
|
||||
// Using ->get() here to instantiate LazilyInstantiatedService too.
|
||||
$service = $this->service_container->get( $id );
|
||||
|
||||
if ( $service instanceof PluginUninstallAware ) {
|
||||
$service->on_plugin_uninstall();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the plugin with the WordPress system.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @throws InvalidService If a service is not valid.
|
||||
*/
|
||||
public function register(): void {
|
||||
if ( false !== static::REGISTRATION_ACTION ) {
|
||||
add_action(
|
||||
static::REGISTRATION_ACTION,
|
||||
[ $this, 'register_services' ]
|
||||
);
|
||||
} else {
|
||||
$this->register_services();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the individual services of this plugin.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @throws InvalidService If a service is not valid.
|
||||
*/
|
||||
public function register_services(): void {
|
||||
// Bail early so we don't instantiate services twice.
|
||||
if ( \count( $this->service_container ) > 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the injector as the very first service.
|
||||
$this->service_container->put(
|
||||
static::SERVICE_PREFIX . static::INJECTOR_ID,
|
||||
$this->injector
|
||||
);
|
||||
|
||||
$services = $this->get_service_classes();
|
||||
|
||||
if ( $this->enable_filters ) {
|
||||
/**
|
||||
* Filter the default services that make up this plugin.
|
||||
*
|
||||
* This can be used to add services to the service container for
|
||||
* this plugin.
|
||||
*
|
||||
* @param array<string, string> $services Associative array of identifier =>
|
||||
* class mappings. The provided
|
||||
* classes need to implement the
|
||||
* Service interface.
|
||||
*/
|
||||
$filtered_services = apply_filters(
|
||||
static::HOOK_PREFIX . static::SERVICES_FILTER,
|
||||
$services
|
||||
);
|
||||
|
||||
$services = $this->validate_services( $filtered_services );
|
||||
}
|
||||
|
||||
while ( null !== key( $services ) ) {
|
||||
$id = $this->maybe_resolve( key( $services ) );
|
||||
|
||||
$class = $this->maybe_resolve( current( $services ) );
|
||||
|
||||
// Delay registering the service until all requirements are met.
|
||||
if (
|
||||
is_a( $class, HasRequirements::class, true )
|
||||
) {
|
||||
if ( ! $this->requirements_are_met( $id, $class, $services ) ) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$this->schedule_potential_service_registration( $id, $class );
|
||||
|
||||
next( $services );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the service container that contains the services that make up the
|
||||
* plugin.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @return ServiceContainer<Service> Service container of the plugin.
|
||||
*/
|
||||
public function get_container(): ServiceContainer {
|
||||
return $this->service_container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the priority for a given service based on its requirements.
|
||||
*
|
||||
* @since 1.13.0
|
||||
*
|
||||
* @throws InvalidService If the required service is not recognized.
|
||||
*
|
||||
* @param class-string $class_name Service FQCN of the service with requirements.
|
||||
* @param array<string, class-string<S>> $services List of services to be registered.
|
||||
* @return int The registration action priority for the service.
|
||||
*
|
||||
* @phpstan-param class-string<S> $class_name Service FQCN of the service with requirements.
|
||||
*/
|
||||
protected function get_registration_action_priority( string $class_name, array &$services ): int {
|
||||
$priority = 10;
|
||||
|
||||
if ( is_a( $class_name, Delayed::class, true ) ) {
|
||||
$priority = $class_name::get_registration_action_priority();
|
||||
}
|
||||
|
||||
if ( ! is_a( $class_name, HasRequirements::class, true ) ) {
|
||||
return $priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Service class.
|
||||
*
|
||||
* @phpstan-var class-string<H&S> $class_name
|
||||
*/
|
||||
$missing_requirements = $this->collect_missing_requirements( $class_name, $services );
|
||||
|
||||
foreach ( $missing_requirements as $missing_requirement ) {
|
||||
if ( is_a( $missing_requirement, Delayed::class, true ) ) {
|
||||
$action = $missing_requirement::get_registration_action();
|
||||
|
||||
if ( did_action( $action ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Missing requirement.
|
||||
*
|
||||
* @phpstan-var class-string<S> $missing_requirement
|
||||
*/
|
||||
$requirement_priority = $this->get_registration_action_priority( $missing_requirement, $services );
|
||||
|
||||
$priority = max( $priority, $requirement_priority + 1 );
|
||||
}
|
||||
}
|
||||
|
||||
return $priority;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the requirements for a service to be registered are met.
|
||||
*
|
||||
* This also hooks up another check in the future to the registration action(s) of its requirements.
|
||||
*
|
||||
* @since 1.10.0
|
||||
*
|
||||
* @throws InvalidService If the required service is not recognized.
|
||||
*
|
||||
* @param string $id Service ID of the service with requirements.
|
||||
* @param class-string $class_name Service FQCN of the service with requirements.
|
||||
* @param array<string, class-string<S>> $services List of services to be registered.
|
||||
* @return bool Whether the requirements for the service has been met.
|
||||
*
|
||||
* @phpstan-param class-string<H&S> $class_name Service FQCN of the service with requirements.
|
||||
*/
|
||||
protected function requirements_are_met( string $id, string $class_name, array &$services ): bool {
|
||||
$missing_requirements = $this->collect_missing_requirements( $class_name, $services );
|
||||
|
||||
if ( empty( $missing_requirements ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ( $missing_requirements as $missing_requirement ) {
|
||||
if ( is_a( $missing_requirement, Delayed::class, true ) ) {
|
||||
$action = $missing_requirement::get_registration_action();
|
||||
|
||||
if ( did_action( $action ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/*
|
||||
* If this service (A) has priority 10 but depends on another service (B) with same priority,
|
||||
* which itself depends on service (C) also with priority 10, this will ensure correct
|
||||
* order of registration by increasing priority for each step.
|
||||
*
|
||||
* The result will be:
|
||||
*
|
||||
* C: priority 10
|
||||
* B: priority 11
|
||||
* A: priority 12
|
||||
*/
|
||||
|
||||
$priority = $this->get_registration_action_priority( $class_name, $services );
|
||||
|
||||
/*
|
||||
* The current service depends on another service that is Delayed and hasn't been registered yet
|
||||
* and for which the registration action has not yet passed.
|
||||
*
|
||||
* Therefore, we postpone the registration of the current service up until the requirement's
|
||||
* action has passed.
|
||||
*
|
||||
* We don't register the service right away, though, we will first check whether at that point,
|
||||
* the requirements have been met.
|
||||
*
|
||||
* Note that badly configured requirements can lead to services that will never register at all.
|
||||
*/
|
||||
|
||||
add_action(
|
||||
$action,
|
||||
function () use ( $id, $class_name, $services ): void {
|
||||
if ( ! $this->requirements_are_met( $id, $class_name, $services ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->schedule_potential_service_registration( $id, $class_name );
|
||||
},
|
||||
$priority
|
||||
);
|
||||
|
||||
next( $services );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* The registration actions from all of the requirements were already processed. This means that the missing
|
||||
* requirement(s) are about to be registered, they just weren't encountered yet while traversing the services
|
||||
* map. Therefore, we skip registration for now and move this particular service to the end of the service map.
|
||||
*
|
||||
* Note: Moving the service to the end of the service map advances the internal array pointer to the next service.
|
||||
*/
|
||||
unset( $services[ $id ] );
|
||||
$services[ $id ] = $class_name;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect the list of missing requirements for a service which has requirements.
|
||||
*
|
||||
* @since 1.10.0
|
||||
*
|
||||
* @throws InvalidService If the required service is not recognized.
|
||||
*
|
||||
* @param class-string $class_name Service FQCN of the service with requirements.
|
||||
* @param array<string, class-string<S>> $services List of services to register.
|
||||
* @return array<string, class-string<S>> List of missing requirements as a $service_id => $service_class mapping.
|
||||
*
|
||||
* @phpstan-param class-string<H&S> $class_name Service FQCN of the service with requirements.
|
||||
*/
|
||||
protected function collect_missing_requirements( string $class_name, array $services ): array {
|
||||
/**
|
||||
* Requirements.
|
||||
*
|
||||
* @var string[] $requirements
|
||||
*/
|
||||
$requirements = $class_name::get_requirements();
|
||||
|
||||
/**
|
||||
* Missing requirements.
|
||||
*
|
||||
* @var array<string, class-string<S>>
|
||||
*/
|
||||
$missing = [];
|
||||
|
||||
foreach ( $requirements as $requirement ) {
|
||||
// Bail if it requires a service that is not recognized.
|
||||
if ( ! \array_key_exists( $requirement, $services ) ) {
|
||||
throw InvalidService::from_service_id( $requirement ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
}
|
||||
|
||||
if ( $this->get_container()->has( $requirement ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$missing[ $requirement ] = $services[ $requirement ];
|
||||
}
|
||||
|
||||
return $missing;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the services array to make sure it is in a usable shape.
|
||||
*
|
||||
* As the array of services could be filtered, we need to ensure it is
|
||||
* always in a state where it doesn't throw PHP warnings or errors.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param array<int|string, string|class-string> $services Services to validate.
|
||||
* @return string[] Validated array of service mappings.
|
||||
*/
|
||||
protected function validate_services( array $services ): array {
|
||||
// Make a copy so we can safely mutate while iterating.
|
||||
$services_to_check = $services;
|
||||
|
||||
foreach ( $services_to_check as $identifier => $fqcn ) {
|
||||
// Ensure we have valid identifiers we can refer to.
|
||||
// If not, generate them from the FQCN.
|
||||
if ( empty( $identifier ) || ! \is_string( $identifier ) ) {
|
||||
unset( $services[ $identifier ] );
|
||||
$identifier = $this->get_identifier_from_fqcn( $fqcn );
|
||||
$services[ $identifier ] = $fqcn;
|
||||
}
|
||||
|
||||
// Verify that the FQCN is valid and points to an existing class.
|
||||
// If not, skip this service.
|
||||
if ( empty( $fqcn ) || ! \is_string( $fqcn ) || ! class_exists( $fqcn ) ) {
|
||||
unset( $services[ $identifier ] );
|
||||
}
|
||||
}
|
||||
|
||||
return $services;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a valid identifier for a provided FQCN.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $fqcn FQCN to use as base to generate an identifier.
|
||||
* @return string Identifier to use for the provided FQCN.
|
||||
*/
|
||||
protected function get_identifier_from_fqcn( string $fqcn ): string {
|
||||
// Retrieve the short name from the FQCN first.
|
||||
$short_name = substr( $fqcn, strrpos( $fqcn, '\\' ) + 1 );
|
||||
|
||||
// Turn camelCase or PascalCase into snake_case.
|
||||
return strtolower(
|
||||
trim(
|
||||
(string) preg_replace( self::DETECT_CAPITALS_REGEX_PATTERN, '_$0', $short_name ),
|
||||
'_'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule the potential registration of a single service.
|
||||
*
|
||||
* This takes into account whether the service registration needs to be delayed or not.
|
||||
*
|
||||
* @since 1.12.0
|
||||
*
|
||||
* @param string $id ID of the service to register.
|
||||
* @param class-string $class_name Class of the service to register.
|
||||
*
|
||||
* @phpstan-param class-string<(D&S)|S> $class_name Class of the service to register.
|
||||
*/
|
||||
protected function schedule_potential_service_registration( string $id, string $class_name ): void {
|
||||
if ( is_a( $class_name, Delayed::class, true ) ) {
|
||||
$action = $class_name::get_registration_action();
|
||||
$priority = $class_name::get_registration_action_priority();
|
||||
|
||||
if ( did_action( $action ) ) {
|
||||
$this->maybe_register_service( $id, $class_name );
|
||||
} else {
|
||||
add_action(
|
||||
$action,
|
||||
function () use ( $id, $class_name ): void {
|
||||
$this->maybe_register_service( $id, $class_name );
|
||||
},
|
||||
$priority
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$this->maybe_register_service( $id, $class_name );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a single service, provided its conditions are met.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $id ID of the service to register.
|
||||
* @param string $class_name Class of the service to register.
|
||||
*
|
||||
* @phpstan-param class-string<S> $class_name Class of the service to register.
|
||||
*/
|
||||
protected function maybe_register_service( string $id, string $class_name ): void {
|
||||
// Ensure we don't register the same service more than once.
|
||||
if ( $this->service_container->has( $id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only instantiate services that are actually needed.
|
||||
if ( is_a( $class_name, Conditional::class, true )
|
||||
&& ! $class_name::is_needed() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$service = $this->instantiate_service( $class_name );
|
||||
|
||||
$this->service_container->put( $id, $service );
|
||||
|
||||
if ( $service instanceof Registerable ) {
|
||||
$service->register();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a single service.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @throws InvalidService If the service could not be properly instantiated.
|
||||
*
|
||||
* @param class-string|object $class_name Service class to instantiate.
|
||||
* @return Service Instantiated service.
|
||||
*
|
||||
* @phpstan-param class-string<S> $class_name Service class to instantiate.
|
||||
*/
|
||||
protected function instantiate_service( $class_name ): Service {
|
||||
/*
|
||||
* If the service is not registerable, we default to lazily instantiated
|
||||
* services here for some basic optimization.
|
||||
*
|
||||
* The services will be properly instantiated once they are retrieved
|
||||
* from the service container.
|
||||
*/
|
||||
if ( ! is_a( $class_name, Registerable::class, true ) ) {
|
||||
return new LazilyInstantiatedService(
|
||||
fn() => $this->injector->make( $class_name )
|
||||
);
|
||||
}
|
||||
|
||||
// The service needs to be registered, so instantiate right away.
|
||||
$service = $this->injector->make( $class_name );
|
||||
|
||||
if ( ! $service instanceof Service ) {
|
||||
throw InvalidService::from_service( $service ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
}
|
||||
|
||||
return $service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the provided injector.
|
||||
*
|
||||
* This method defines the mappings that the injector knows about, and the
|
||||
* logic it requires to make more complex instantiations work.
|
||||
*
|
||||
* For more complex plugins, this should be extracted into a separate
|
||||
* object
|
||||
* or into configuration files.
|
||||
*
|
||||
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param Injector $injector Injector instance to configure.
|
||||
* @return Injector Configured injector instance.
|
||||
*/
|
||||
protected function configure_injector( Injector $injector ): Injector {
|
||||
$bindings = $this->get_bindings();
|
||||
$shared_instances = $this->get_shared_instances();
|
||||
$arguments = $this->get_arguments();
|
||||
$delegations = $this->get_delegations();
|
||||
|
||||
if ( $this->enable_filters ) {
|
||||
/**
|
||||
* Filter the default bindings that are provided by the plugin.
|
||||
*
|
||||
* This can be used to swap implementations out for alternatives.
|
||||
*
|
||||
* @param array<string> $bindings Associative array of interface =>
|
||||
* implementation bindings. Both
|
||||
* should be FQCNs.
|
||||
*/
|
||||
$bindings = apply_filters(
|
||||
static::HOOK_PREFIX . static::BINDINGS_FILTER,
|
||||
$bindings
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter the default argument bindings that are provided by the
|
||||
* plugin.
|
||||
*
|
||||
* This can be used to override scalar values.
|
||||
*
|
||||
* @param array<class-string, mixed> $arguments Associative array of class =>
|
||||
* arguments mappings. The arguments
|
||||
* array maps argument names to
|
||||
* values.
|
||||
*/
|
||||
$arguments = apply_filters(
|
||||
static::HOOK_PREFIX . static::ARGUMENTS_FILTER,
|
||||
$arguments
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter the instances that are shared by default by the plugin.
|
||||
*
|
||||
* This can be used to turn objects that were added externally into
|
||||
* shared instances.
|
||||
*
|
||||
* @param array<string> $shared_instances Array of FQCNs to turn
|
||||
* into shared objects.
|
||||
*/
|
||||
$shared_instances = apply_filters(
|
||||
static::HOOK_PREFIX . static::SHARED_INSTANCES_FILTER,
|
||||
$shared_instances
|
||||
);
|
||||
|
||||
/**
|
||||
* Filter the instances that are shared by default by the plugin.
|
||||
*
|
||||
* This can be used to turn objects that were added externally into
|
||||
* shared instances.
|
||||
*
|
||||
* @param array<string, callable> $delegations Associative array of class =>
|
||||
* callable mappings.
|
||||
*/
|
||||
$delegations = apply_filters(
|
||||
static::HOOK_PREFIX . static::DELEGATIONS_FILTER,
|
||||
$delegations
|
||||
);
|
||||
}
|
||||
|
||||
foreach ( $bindings as $from => $to ) {
|
||||
$from = $this->maybe_resolve( $from );
|
||||
$to = $this->maybe_resolve( $to );
|
||||
|
||||
$injector = $injector->bind( $from, $to );
|
||||
}
|
||||
|
||||
/**
|
||||
* Argument map.
|
||||
*
|
||||
* @var array<class-string, array<string|callable|class-string>> $arguments
|
||||
*/
|
||||
foreach ( $arguments as $class => $argument_map ) {
|
||||
$class = $this->maybe_resolve( $class );
|
||||
|
||||
foreach ( $argument_map as $name => $value ) {
|
||||
// We don't try to resolve the $value here, as we might want to
|
||||
// pass a callable as-is.
|
||||
$name = $this->maybe_resolve( $name );
|
||||
|
||||
$injector = $injector->bind_argument( $class, $name, $value );
|
||||
}
|
||||
}
|
||||
|
||||
foreach ( $shared_instances as $shared_instance ) {
|
||||
$shared_instance = $this->maybe_resolve( $shared_instance );
|
||||
|
||||
$injector = $injector->share( $shared_instance );
|
||||
}
|
||||
|
||||
/**
|
||||
* Callable.
|
||||
*
|
||||
* @var callable $callable
|
||||
*/
|
||||
foreach ( $delegations as $class => $callable ) {
|
||||
// We don't try to resolve the $callable here, as we want to pass it
|
||||
// on as-is.
|
||||
$class = $this->maybe_resolve( $class );
|
||||
|
||||
$injector = $injector->delegate( $class, $callable );
|
||||
}
|
||||
|
||||
return $injector;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of services to register.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @return array<string, class-string<S>> Associative array of identifiers mapped to fully
|
||||
* qualified class names.
|
||||
*/
|
||||
protected function get_service_classes(): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the bindings for the dependency injector.
|
||||
*
|
||||
* The bindings let you map interfaces (or classes) to the classes that
|
||||
* should be used to implement them.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @return array<string, class-string<S>> Associative array of fully qualified class names.
|
||||
*/
|
||||
protected function get_bindings(): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the argument bindings for the dependency injector.
|
||||
*
|
||||
* The argument bindings let you map specific argument values for specific
|
||||
* classes.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @return array<class-string, mixed> Associative array of arrays mapping argument names
|
||||
* to argument values.
|
||||
*/
|
||||
protected function get_arguments(): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shared instances for the dependency injector.
|
||||
*
|
||||
* These classes will only be instantiated once by the injector and then
|
||||
* reused on subsequent requests.
|
||||
*
|
||||
* This effectively turns them into singletons, without any of the
|
||||
* drawbacks of the actual Singleton anti-pattern.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @return array<string> Array of fully qualified class names.
|
||||
*/
|
||||
protected function get_shared_instances(): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the delegations for the dependency injector.
|
||||
*
|
||||
* These are basically factories to provide custom instantiation logic for
|
||||
* classes.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @return array<callable> Associative array of callables.
|
||||
*
|
||||
* @phpstan-return array<class-string<S|H|C>, callable> Associative array of callables.
|
||||
*/
|
||||
protected function get_delegations(): array {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe resolve a value that is a callable instead of a scalar.
|
||||
*
|
||||
* Values that are passed through this method can optionally be provided as
|
||||
* callables instead of direct values and will be evaluated when needed.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string|callable|class-string $value Value to potentially resolve.
|
||||
* @return string|class-string Resolved or unchanged value.
|
||||
*
|
||||
* @phpstan-return class-string<C&D&H&R&S> Resolved or unchanged value.
|
||||
*/
|
||||
protected function maybe_resolve( $value ): string {
|
||||
if ( \is_callable( $value ) && ! ( \is_string( $value ) && \function_exists( $value ) ) ) {
|
||||
$value = $value( $this->injector, $this->service_container );
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface ServiceContainer.
|
||||
*
|
||||
* @link https://www.mwpd.io/
|
||||
*
|
||||
* @copyright 2019 Alain Schlesser
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Original code modified for this project.
|
||||
*
|
||||
* @copyright 2021 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\Infrastructure;
|
||||
|
||||
use ArrayAccess;
|
||||
use Countable;
|
||||
use Google\Web_Stories\Exception\InvalidService;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* The service container collects all services to manage them.
|
||||
*
|
||||
* This is based on PSR-11 and should extend that one if Composer dependencies
|
||||
* are being used. Relying on a standardized interface like PSR-11 means you'll
|
||||
* be able to easily swap out the implementation for something else later on.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @see https://www.php-fig.org/psr/psr-11/
|
||||
*/
|
||||
interface ServiceContainer extends Traversable, Countable, ArrayAccess {
|
||||
|
||||
/**
|
||||
* Find a service of the container by its identifier and return it.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @throws InvalidService If the service could not be found.
|
||||
*
|
||||
* @param string $id Identifier of the service to look for.
|
||||
* @return Service Service that was requested.
|
||||
*/
|
||||
public function get( string $id ): Service;
|
||||
|
||||
/**
|
||||
* Check whether the container can return a service for the given
|
||||
* identifier.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $id Identifier of the service to look for.
|
||||
*/
|
||||
public function has( string $id ): bool;
|
||||
|
||||
/**
|
||||
* Put a service into the container for later retrieval.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $id Identifier of the service to put into the
|
||||
* container.
|
||||
* @param Service $service Service to put into the container.
|
||||
*/
|
||||
public function put( string $id, Service $service ): void;
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
/**
|
||||
* Final class LazilyInstantiatedService.
|
||||
*
|
||||
* @link https://www.mwpd.io/
|
||||
*
|
||||
* @copyright 2019 Alain Schlesser
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Original code modified for this project.
|
||||
*
|
||||
* @copyright 2021 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\Infrastructure\ServiceContainer;
|
||||
|
||||
use Google\Web_Stories\Exception\InvalidService;
|
||||
use Google\Web_Stories\Infrastructure\Service;
|
||||
|
||||
/**
|
||||
* A service that only gets properly instantiated when it is actually being
|
||||
* retrieved from the container.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @since 1.6.0
|
||||
*/
|
||||
final class LazilyInstantiatedService implements Service {
|
||||
|
||||
/**
|
||||
* Instantiation of Class.
|
||||
*
|
||||
* @var callable
|
||||
*/
|
||||
private $instantiation;
|
||||
|
||||
/**
|
||||
* Instantiate a LazilyInstantiatedService object.
|
||||
*
|
||||
* @param callable $instantiation Instantiation callable to use.
|
||||
*/
|
||||
public function __construct( callable $instantiation ) {
|
||||
$this->instantiation = $instantiation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do the actual service instantiation and return the real service.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @throws InvalidService If the service could not be properly instantiated.
|
||||
*
|
||||
* @return Service Properly instantiated service.
|
||||
*/
|
||||
public function instantiate(): Service {
|
||||
$instantiation = $this->instantiation; // Because uniform variable syntax not supported in PHP 5.6.
|
||||
$service = $instantiation();
|
||||
|
||||
if ( ! $service instanceof Service ) {
|
||||
throw InvalidService::from_service( $service ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
}
|
||||
|
||||
return $service;
|
||||
}
|
||||
}
|
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
/**
|
||||
* Final class SimpleServiceContainer.
|
||||
*
|
||||
* @link https://www.mwpd.io/
|
||||
*
|
||||
* @copyright 2019 Alain Schlesser
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Original code modified for this project.
|
||||
*
|
||||
* @copyright 2021 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\Infrastructure\ServiceContainer;
|
||||
|
||||
use ArrayObject;
|
||||
use Google\Web_Stories\Exception\InvalidService;
|
||||
use Google\Web_Stories\Infrastructure\Service;
|
||||
use Google\Web_Stories\Infrastructure\ServiceContainer;
|
||||
|
||||
/**
|
||||
* A simplified implementation of a service container.
|
||||
*
|
||||
* We extend ArrayObject so we have default implementations for iterators and
|
||||
* array access.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @since 1.6.0
|
||||
*/
|
||||
final class SimpleServiceContainer extends ArrayObject implements ServiceContainer {
|
||||
|
||||
/**
|
||||
* Find a service of the container by its identifier and return it.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @throws InvalidService If the service could not be found.
|
||||
*
|
||||
* @param string $id Identifier of the service to look for.
|
||||
* @return Service Service that was requested.
|
||||
*/
|
||||
public function get( string $id ): Service {
|
||||
if ( ! $this->has( $id ) ) {
|
||||
throw InvalidService::from_service_id( $id ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
|
||||
}
|
||||
|
||||
/**
|
||||
* Service.
|
||||
*
|
||||
* @var Service $service Service.
|
||||
*/
|
||||
$service = $this->offsetGet( $id );
|
||||
|
||||
// Instantiate actual services if they were stored lazily.
|
||||
if ( $service instanceof LazilyInstantiatedService ) {
|
||||
$service = $service->instantiate();
|
||||
$this->put( $id, $service );
|
||||
}
|
||||
|
||||
return $service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the container can return a service for the given
|
||||
* identifier.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $id Identifier of the service to look for.
|
||||
*/
|
||||
public function has( string $id ): bool {
|
||||
return $this->offsetExists( $id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Put a service into the container for later retrieval.
|
||||
*
|
||||
* @since 1.6.0
|
||||
*
|
||||
* @param string $id Identifier of the service to put into the
|
||||
* container.
|
||||
* @param Service $service Service to put into the container.
|
||||
*/
|
||||
public function put( string $id, Service $service ): void {
|
||||
$this->offsetSet( $id, $service );
|
||||
}
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface SiteInitializationAware.
|
||||
*
|
||||
* @link https://github.com/googleforcreators/web-stories-wp
|
||||
*
|
||||
* @copyright 2021 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\Infrastructure;
|
||||
|
||||
use WP_Site;
|
||||
|
||||
/**
|
||||
* Something that acts on site creation on Multisite.
|
||||
*
|
||||
* By tagging a service with this interface, the system will automatically hook
|
||||
* it up to the 'wp_initialize_site' WordPress action.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @since 1.11.0
|
||||
*/
|
||||
interface SiteInitializationAware {
|
||||
|
||||
/**
|
||||
* Act on site initialization.
|
||||
*
|
||||
* @since 1.11.0
|
||||
*
|
||||
* @param WP_Site $site The site being initialized.
|
||||
*/
|
||||
public function on_site_initialization( WP_Site $site ): void;
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
/**
|
||||
* Interface SiteRemovalAware.
|
||||
*
|
||||
* @link https://github.com/googleforcreators/web-stories-wp
|
||||
*
|
||||
* @copyright 2021 Google LLC
|
||||
* @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
|
||||
*/
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Google\Web_Stories\Infrastructure;
|
||||
|
||||
use WP_Site;
|
||||
|
||||
/**
|
||||
* Something that acts on site removal on Multisite.
|
||||
*
|
||||
* By tagging a service with this interface, the system will automatically hook
|
||||
* it up to the 'wp_validate_site_deletion' WordPress action.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @since 1.11.0
|
||||
*/
|
||||
interface SiteRemovalAware {
|
||||
|
||||
/**
|
||||
* Act on site removal.
|
||||
*
|
||||
* @since 1.11.0
|
||||
*
|
||||
* @param WP_Site $site The site being removed.
|
||||
*/
|
||||
public function on_site_removal( WP_Site $site ): void;
|
||||
}
|
Reference in New Issue
Block a user