woocommerce/src/Internal/DependencyManagement/AbstractServiceProvider.php

173 lines
7.1 KiB
PHP

<?php
/**
* AbstractServiceProvider class file.
*/
namespace Automattic\WooCommerce\Internal\DependencyManagement;
use Automattic\WooCommerce\Vendor\League\Container\Argument\RawArgument;
use Automattic\WooCommerce\Vendor\League\Container\Definition\DefinitionInterface;
use Automattic\WooCommerce\Vendor\League\Container\ServiceProvider\AbstractServiceProvider as BaseServiceProvider;
/**
* Base class for the service providers used to register classes in the container.
*
* See the documentation of the original class this one is based on (https://container.thephpleague.com/3.x/service-providers)
* for basic usage details. What this class adds is:
*
* - The `add_with_auto_arguments` method that allows to register classes without having to specify the injection method arguments.
* - The `share_with_auto_arguments` method, sibling of the above.
* - Convenience `add` and `share` methods that are just proxies for the same methods in `$this->getContainer()`.
*/
abstract class AbstractServiceProvider extends BaseServiceProvider {
/**
* Register a class in the container and use reflection to guess the injection method arguments.
*
* WARNING: this method uses reflection, so please have performance in mind when using it.
*
* @param string $class_name Class name to register.
* @param mixed $concrete The concrete to register. Can be a shared instance, a factory callback, or a class name.
* @param bool $shared Whether to register the class as shared (`get` always returns the same instance) or not.
*
* @return DefinitionInterface The generated container definition.
*
* @throws ContainerException Error when reflecting the class, or class injection method is not public, or an argument has no valid type hint.
*/
protected function add_with_auto_arguments( string $class_name, $concrete = null, bool $shared = false ) : DefinitionInterface {
$definition = new Definition( $class_name, $concrete );
$function = $this->reflect_class_or_callable( $class_name, $concrete );
if ( ! is_null( $function ) ) {
$arguments = $function->getParameters();
foreach ( $arguments as $argument ) {
if ( $argument->isDefaultValueAvailable() ) {
$default_value = $argument->getDefaultValue();
$definition->addArgument( new RawArgument( $default_value ) );
} else {
$argument_class = $this->get_class( $argument );
if ( is_null( $argument_class ) ) {
throw new ContainerException( "Argument '{$argument->getName()}' of class '$class_name' doesn't have a type hint or has one that doesn't specify a class." );
}
$definition->addArgument( $argument_class->name );
}
}
}
// Register the definition only after being sure that no exception will be thrown.
$this->getContainer()->add( $definition->getAlias(), $definition, $shared );
return $definition;
}
/**
* Gets the class of a parameter.
*
* This method is a replacement for ReflectionParameter::getClass,
* which is deprecated as of PHP 8.
*
* @param \ReflectionParameter $parameter The parameter to get the class for.
*
* @return \ReflectionClass|null The class of the parameter, or null if it hasn't any.
*/
private function get_class( \ReflectionParameter $parameter ) {
// TODO: Remove this 'if' block once minimum PHP version for WooCommerce is bumped to at least 7.1.
if ( version_compare( PHP_VERSION, '7.1', '<' ) ) {
return $parameter->getClass();
}
return $parameter->getType() && ! $parameter->getType()->isBuiltin()
? new \ReflectionClass( $parameter->getType()->getName() )
: null;
}
/**
* Check if a combination of class name and concrete is valid for registration.
* Also return the class injection method if the concrete is either a class name or null (then use the supplied class name).
*
* @param string $class_name The class name to check.
* @param mixed $concrete The concrete to check.
*
* @return \ReflectionFunctionAbstract|null A reflection instance for the $class_name injection method or $concrete injection method or callable; null otherwise.
* @throws ContainerException Class has a private injection method, can't reflect class, or the concrete is invalid.
*/
private function reflect_class_or_callable( string $class_name, $concrete ) {
if ( ! isset( $concrete ) || is_string( $concrete ) && class_exists( $concrete ) ) {
try {
$class = $concrete ?? $class_name;
$method = new \ReflectionMethod( $class, Definition::INJECTION_METHOD );
if ( ! isset( $method ) ) {
return null;
}
$missing_modifiers = array();
if ( ! $method->isFinal() ) {
$missing_modifiers[] = 'final';
}
if ( ! $method->isPublic() ) {
$missing_modifiers[] = 'public';
}
if ( ! empty( $missing_modifiers ) ) {
throw new ContainerException( "Method '" . Definition::INJECTION_METHOD . "' of class '$class' isn't '" . implode( ' ', $missing_modifiers ) . "', instances can't be created." );
}
return $method;
} catch ( \ReflectionException $ex ) {
return null;
}
} elseif ( is_callable( $concrete ) ) {
try {
return new \ReflectionFunction( $concrete );
} catch ( \ReflectionException $ex ) {
throw new ContainerException( "Error when reflecting callable: {$ex->getMessage()}" );
}
}
return null;
}
/**
* Register a class in the container and use reflection to guess the injection method arguments.
* The class is registered as shared, so `get` on the container always returns the same instance.
*
* WARNING: this method uses reflection, so please have performance in mind when using it.
*
* @param string $class_name Class name to register.
* @param mixed $concrete The concrete to register. Can be a shared instance, a factory callback, or a class name.
*
* @return DefinitionInterface The generated container definition.
*
* @throws ContainerException Error when reflecting the class, or class injection method is not public, or an argument has no valid type hint.
*/
protected function share_with_auto_arguments( string $class_name, $concrete = null ) : DefinitionInterface {
return $this->add_with_auto_arguments( $class_name, $concrete, true );
}
/**
* Register an entry in the container.
*
* @param string $id Entry id (typically a class or interface name).
* @param mixed|null $concrete Concrete entity to register under that id, null for automatic creation.
* @param bool|null $shared Whether to register the class as shared (`get` always returns the same instance) or not.
*
* @return DefinitionInterface The generated container definition.
*/
protected function add( string $id, $concrete = null, bool $shared = null ) : DefinitionInterface {
return $this->getContainer()->add( $id, $concrete, $shared );
}
/**
* Register a shared entry in the container (`get` always returns the same instance).
*
* @param string $id Entry id (typically a class or interface name).
* @param mixed|null $concrete Concrete entity to register under that id, null for automatic creation.
*
* @return DefinitionInterface The generated container definition.
*/
protected function share( string $id, $concrete = null ) : DefinitionInterface {
return $this->add( $id, $concrete, true );
}
}