Reintroduce the dependency injection related code.

After the League's Container package has been reintroduced, all the
code that implements the dependency injection mechanism in woocommerce
can be brought back as well.
This commit is contained in:
Nestor Soriano 2020-09-21 16:24:24 +02:00
parent 97618d8fad
commit b71f876cba
24 changed files with 1646 additions and 12 deletions

View File

@ -912,4 +912,60 @@ final class WooCommerce {
public function is_wc_admin_active() { public function is_wc_admin_active() {
return function_exists( 'wc_admin_url' ); return function_exists( 'wc_admin_url' );
} }
/**
* Call a user function. This should be used to execute any non-idempotent function, especially
* those in the `includes` directory or provided by WordPress.
*
* This method can be useful for unit tests, since functions called using this method
* can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_function_mocks.
*
* @param string $function_name The function to execute.
* @param mixed ...$parameters The parameters to pass to the function.
*
* @return mixed The result from the function.
*
* @since 4.4
*/
public function call_function( $function_name, ...$parameters ) {
return wc_get_container()->get( LegacyProxy::class )->call_function( $function_name, ...$parameters );
}
/**
* Call a static method in a class. This should be used to execute any non-idempotent method in classes
* from the `includes` directory.
*
* This method can be useful for unit tests, since methods called using this method
* can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_static_mocks.
*
* @param string $class_name The name of the class containing the method.
* @param string $method_name The name of the method.
* @param mixed ...$parameters The parameters to pass to the method.
*
* @return mixed The result from the method.
*
* @since 4.4
*/
public function call_static( $class_name, $method_name, ...$parameters ) {
return wc_get_container()->get( LegacyProxy::class )->call_static( $class_name, $method_name, ...$parameters );
}
/**
* Gets an instance of a given legacy class.
* This must not be used to get instances of classes in the `src` directory.
*
* This method can be useful for unit tests, since objects obtained using this method
* can be easily mocked by using WC_Unit_Test_Case::register_legacy_proxy_class_mocks.
*
* @param string $class_name The name of the class to get an instance for.
* @param mixed ...$args Parameters to be passed to the class constructor or to the appropriate internal 'get_instance_of_' method.
*
* @return object The instance of the class.
* @throws \Exception The requested class belongs to the `src` directory, or there was an error creating an instance of the class.
*
* @since 4.4
*/
public function get_instance_of( string $class_name, ...$args ) {
return wc_get_container()->get( LegacyProxy::class )->get_instance_of( $class_name, ...$args );
}
} }

View File

@ -5,9 +5,8 @@
namespace Automattic\WooCommerce; namespace Automattic\WooCommerce;
use Psr\Container\ContainerExceptionInterface; use Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders\ProxiesServiceProvider;
use Psr\Container\ContainerInterface; use Automattic\WooCommerce\Internal\DependencyManagement\ExtendedContainer;
use Psr\Container\NotFoundExceptionInterface;
/** /**
* PSR11 compliant dependency injection container for WooCommerce. * PSR11 compliant dependency injection container for WooCommerce.
@ -26,19 +25,51 @@ use Psr\Container\NotFoundExceptionInterface;
* and those should go in the `src\Internal\DependencyManagement\ServiceProviders` folder unless there's a good reason * and those should go in the `src\Internal\DependencyManagement\ServiceProviders` folder unless there's a good reason
* to put them elsewhere. All the service provider class names must be in the `SERVICE_PROVIDERS` constant. * to put them elsewhere. All the service provider class names must be in the `SERVICE_PROVIDERS` constant.
*/ */
final class Container implements ContainerInterface { final class Container implements \Psr\Container\ContainerInterface {
/**
* The list of service provider classes to register.
*
* @var string[]
*/
private $service_providers = array(
ProxiesServiceProvider::class,
);
/**
* The underlying container.
*
* @var \League\Container\Container
*/
private $container;
/**
* Class constructor.
*/
public function __construct() {
$this->container = new ExtendedContainer();
// Add ourselves as the shared instance of ContainerInterface,
// register everything else using service providers.
$this->container->share( \Psr\Container\ContainerInterface::class, $this );
foreach ( $this->service_providers as $service_provider_class ) {
$this->container->addServiceProvider( $service_provider_class );
}
}
/** /**
* Finds an entry of the container by its identifier and returns it. * Finds an entry of the container by its identifier and returns it.
* *
* @param string $id Identifier of the entry to look for. * @param string $id Identifier of the entry to look for.
* *
* @throws NotFoundExceptionInterface No entry was found for **this** identifier. * @throws NotFoundExceptionInterface No entry was found for **this** identifier.
* @throws ContainerExceptionInterface Error while retrieving the entry. * @throws Psr\Container\ContainerExceptionInterface Error while retrieving the entry.
* *
* @return mixed Entry. * @return mixed Entry.
*/ */
public function get( $id ) { public function get( $id ) {
return null; return $this->container->get( $id );
} }
/** /**
@ -53,6 +84,6 @@ final class Container implements ContainerInterface {
* @return bool * @return bool
*/ */
public function has( $id ) { public function has( $id ) {
return false; return $this->container->has( $id );
} }
} }

View File

@ -0,0 +1,150 @@
<?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;
/**
* 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 \Automattic\WooCommerce\Vendor\League\Container\ServiceProvider\AbstractServiceProvider {
/**
* 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 = $argument->getClass();
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;
}
/**
* 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 );
}
}

View File

@ -0,0 +1,23 @@
<?php
/**
* ExtendedContainer class file.
*/
namespace Automattic\WooCommerce\Internal\DependencyManagement;
/**
* Class ContainerException.
* Used to signal error conditions related to the dependency injection container.
*/
class ContainerException extends \Exception {
/**
* Create a new instance of the class.
*
* @param null $message The exception message to throw.
* @param int $code The error code.
* @param Exception|null $previous The previous throwable used for exception chaining.
*/
public function __construct( $message = null, $code = 0, Exception $previous = null ) {
parent::__construct( $message, $code, $previous );
}
}

View File

@ -0,0 +1,39 @@
<?php
/**
* An extension to the Definition class to prevent constructor injection from being possible.
*/
namespace Automattic\WooCommerce\Internal\DependencyManagement;
use Automattic\WooCommerce\Vendor\League\Container\Definition\Definition as BaseDefinition;
/**
* An extension of the definition class that replaces constructor injection with method injection.
*/
class Definition extends BaseDefinition {
/**
* The standard method that we use for dependency injection.
*/
const INJECTION_METHOD = 'init';
/**
* Resolve a class using method injection instead of constructor injection.
*
* @param string $concrete The concrete to instantiate.
*
* @return object
*/
protected function resolveClass( string $concrete ) {
$resolved = $this->resolveArguments( $this->arguments );
$concrete = new $concrete();
// Constructor injection causes backwards compatibility problems
// so we will rely on method injection via an internal method.
if ( method_exists( $concrete, static::INJECTION_METHOD ) ) {
call_user_func_array( array( $concrete, static::INJECTION_METHOD ), $resolved );
}
return $concrete;
}
}

View File

@ -0,0 +1,152 @@
<?php
/**
* ExtendedContainer class file.
*/
namespace Automattic\WooCommerce\Internal\DependencyManagement;
use Automattic\WooCommerce\Utilities\StringUtil;
use Automattic\WooCommerce\Vendor\League\Container\Container as BaseContainer;
use Automattic\WooCommerce\Vendor\League\Container\Definition\DefinitionInterface;
/**
* This class extends the original League's Container object by adding some functionality
* that we need for WooCommerce.
*/
class ExtendedContainer extends BaseContainer {
/**
* The root namespace of all WooCommerce classes in the `src` directory.
*
* @var string
*/
private $woocommerce_namespace = 'Automattic\\WooCommerce\\';
/**
* Whitelist of classes that we can register using the container
* despite not belonging to the WooCommerce root namespace.
*
* In general we allow only the registration of classes in the
* WooCommerce root namespace to prevent registering 3rd party code
* (which doesn't really belong to this container) or old classes
* (which may be eventually deprecated, also the LegacyProxy
* should be used for those).
*
* @var string[]
*/
private $registration_whitelist = array(
\Psr\Container\ContainerInterface::class,
);
/**
* Register a class in the container.
*
* @param string $class_name Class name.
* @param mixed $concrete How to resolve the class with `get`: a factory callback, a concrete instance, another class name, or null to just create an instance of the class.
* @param bool|null $shared Whether the resolution should be performed only once and cached.
*
* @return DefinitionInterface The generated definition for the container.
* @throws ContainerException Invalid parameters.
*/
public function add( string $class_name, $concrete = null, bool $shared = null ) : DefinitionInterface {
if ( ! $this->is_class_allowed( $class_name ) ) {
throw new ContainerException( "You cannot add '$class_name', only classes in the {$this->woocommerce_namespace} namespace are allowed." );
}
$concrete_class = $this->get_class_from_concrete( $concrete );
if ( isset( $concrete_class ) && ! $this->is_class_allowed( $concrete_class ) ) {
throw new ContainerException( "You cannot add concrete '$concrete_class', only classes in the {$this->woocommerce_namespace} namespace are allowed." );
}
// We want to use a definition class that does not support constructor injection to avoid accidental usage.
if ( ! $concrete instanceof DefinitionInterface ) {
$concrete = new Definition( $class_name, $concrete );
}
return parent::add( $class_name, $concrete, $shared );
}
/**
* Replace an existing registration with a different concrete.
*
* @param string $class_name The class name whose definition will be replaced.
* @param mixed $concrete The new concrete (same as "add").
*
* @return DefinitionInterface The modified definition.
* @throws ContainerException Invalid parameters.
*/
public function replace( string $class_name, $concrete ) : DefinitionInterface {
if ( ! $this->has( $class_name ) ) {
throw new ContainerException( "The container doesn't have '$class_name' registered, please use 'add' instead of 'replace'." );
}
$concrete_class = $this->get_class_from_concrete( $concrete );
if ( isset( $concrete_class ) && ! $this->is_class_allowed( $concrete_class ) ) {
throw new ContainerException( "You cannot use concrete '$concrete_class', only classes in the {$this->woocommerce_namespace} namespace are allowed." );
}
return $this->extend( $class_name )->setConcrete( $concrete );
}
/**
* Reset all the cached resolutions, so any further "get" for shared definitions will generate the instance again.
*/
public function reset_all_resolved() {
foreach ( $this->definitions->getIterator() as $definition ) {
// setConcrete causes the cached resolved value to be forgotten.
$concrete = $definition->getConcrete();
$definition->setConcrete( $concrete );
}
}
/**
* Get an instance of a registered class.
*
* @param string $id The class name.
* @param bool $new True to generate a new instance even if the class was registered as shared.
*
* @return object An instance of the requested class.
* @throws ContainerException Attempt to get an instance of a non-namespaced class.
*/
public function get( $id, bool $new = false ) {
if ( false === strpos( $id, '\\' ) ) {
throw new ContainerException( "Attempt to get an instance of the non-namespaced class '$id' from the container, did you forget to add a namespace import?" );
}
return parent::get( $id, $new );
}
/**
* Gets the class from the concrete regardless of type.
*
* @param mixed $concrete The concrete that we want the class from..
*
* @return string|null The class from the concrete if one is available, null otherwise.
*/
protected function get_class_from_concrete( $concrete ) {
if ( is_object( $concrete ) && ! is_callable( $concrete ) ) {
if ( $concrete instanceof DefinitionInterface ) {
return $this->get_class_from_concrete( $concrete->getConcrete() );
}
return get_class( $concrete );
}
if ( is_string( $concrete ) && class_exists( $concrete ) ) {
return $concrete;
}
return null;
}
/**
* Checks to see whether or not a class is allowed to be registered.
*
* @param string $class_name The class to check.
*
* @return bool True if the class is allowed to be registered, false otherwise.
*/
protected function is_class_allowed( string $class_name ): bool {
return StringUtil::starts_with( $class_name, $this->woocommerce_namespace, false ) || in_array( $class_name, $this->registration_whitelist, true );
}
}

View File

@ -0,0 +1,34 @@
<?php
/**
* Proxies class file.
*/
namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Proxies\ActionsProxy;
/**
* Service provider for the classes in the Automattic\WooCommerce\Proxies namespace.
*/
class ProxiesServiceProvider extends AbstractServiceProvider {
/**
* The classes/interfaces that are serviced by this service provider.
*
* @var array
*/
protected $provides = array(
LegacyProxy::class,
ActionsProxy::class,
);
/**
* Register the classes.
*/
public function register() {
$this->share( ActionsProxy::class );
$this->share_with_auto_arguments( LegacyProxy::class );
}
}

View File

@ -1,8 +1,5 @@
# WooCommerce `src` files # WooCommerce `src` files
## Important note
The dependency injection container is disabled for now due to conflicts with plugins that use the same container package. Therefore all the content about registering and resolving classes, and interacting with legacy code, doesn't apply yet. It will be enabled at a later time.
## Table of contents ## Table of contents

View File

@ -25,6 +25,9 @@ final class DependencyManagementTestHook implements BeforeTestHook {
* @param string $test "TestClass::TestMethod". * @param string $test "TestClass::TestMethod".
*/ */
public function executeBeforeTest( string $test ): void { public function executeBeforeTest( string $test ): void {
// Reset the instance of MockableLegacyProxy that was registered during bootstrap,
// in order to start the test in a clean state (without anything mocked).
wc_get_container()->get( LegacyProxy::class )->reset();
} }
} }

View File

@ -69,6 +69,9 @@ class WC_Unit_Tests_Bootstrap {
// load WC testing framework. // load WC testing framework.
$this->includes(); $this->includes();
// re-initialize dependency injection, this needs to be the last operation after everything else is in place.
$this->initialize_dependency_injection();
} }
/** /**
@ -118,6 +121,35 @@ class WC_Unit_Tests_Bootstrap {
CodeHacker::enable(); CodeHacker::enable();
} }
/**
* Re-initialize the dependency injection engine.
*
* The dependency injection engine has been already initialized as part of the Woo initialization, but we need
* to replace the registered read-only container with a fully configurable one for testing.
* To this end we hack a bit and use reflection to grab the underlying container that the read-only one stores
* in a private property.
*
* Additionally, we replace the legacy/function proxies with mockable versions to easily replace anything
* in tests as appropriate.
*
* @throws \Exception The Container class doesn't have a 'container' property.
*/
private function initialize_dependency_injection() {
try {
$inner_container_property = new \ReflectionProperty( \Automattic\WooCommerce\Container::class, 'container' );
} catch ( ReflectionException $ex ) {
throw new \Exception( "Error when trying to get the private 'container' property from the " . \Automattic\WooCommerce\Container::class . ' class using reflection during unit testing bootstrap, has the property been removed or renamed?' );
}
$inner_container_property->setAccessible( true );
$inner_container = $inner_container_property->getValue( wc_get_container() );
$inner_container->replace( LegacyProxy::class, MockableLegacyProxy::class );
$inner_container->reset_all_resolved();
$GLOBALS['wc_container'] = $inner_container;
}
/** /**
* Load WooCommerce. * Load WooCommerce.
* *

View File

@ -183,7 +183,7 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
* @return mixed The instance. * @return mixed The instance.
*/ */
public function get_instance_of( string $class_name ) { public function get_instance_of( string $class_name ) {
return null; return wc_get_container()->get( $class_name );
} }
/** /**
@ -195,7 +195,7 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
* @return mixed The instance. * @return mixed The instance.
*/ */
public function get_legacy_instance_of( string $class_name ) { public function get_legacy_instance_of( string $class_name ) {
return null; return wc_get_container()->get( LegacyProxy::class )->get_instance_of( $class_name );
} }
/** /**
@ -204,12 +204,14 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
* This may be needed when registering mocks for already resolved shared classes. * This may be needed when registering mocks for already resolved shared classes.
*/ */
public function reset_container_resolutions() { public function reset_container_resolutions() {
wc_get_container()->reset_all_resolved();
} }
/** /**
* Reset the mock legacy proxy class so that all the registered mocks are unregistered. * Reset the mock legacy proxy class so that all the registered mocks are unregistered.
*/ */
public function reset_legacy_proxy_mocks() { public function reset_legacy_proxy_mocks() {
wc_get_container()->get( LegacyProxy::class )->reset();
} }
/** /**
@ -220,6 +222,7 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
* @throws \Exception Invalid parameter. * @throws \Exception Invalid parameter.
*/ */
public function register_legacy_proxy_function_mocks( array $mocks ) { public function register_legacy_proxy_function_mocks( array $mocks ) {
wc_get_container()->get( LegacyProxy::class )->register_function_mocks( $mocks );
} }
/** /**
@ -230,6 +233,7 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
* @throws \Exception Invalid parameter. * @throws \Exception Invalid parameter.
*/ */
public function register_legacy_proxy_static_mocks( array $mocks ) { public function register_legacy_proxy_static_mocks( array $mocks ) {
wc_get_container()->get( LegacyProxy::class )->register_static_mocks( $mocks );
} }
/** /**
@ -240,5 +244,6 @@ class WC_Unit_Test_Case extends WP_HTTP_TestCase {
* @throws \Exception Invalid parameter. * @throws \Exception Invalid parameter.
*/ */
public function register_legacy_proxy_class_mocks( array $mocks ) { public function register_legacy_proxy_class_mocks( array $mocks ) {
wc_get_container()->get( LegacyProxy::class )->register_class_mocks( $mocks );
} }
} }

View File

@ -0,0 +1,232 @@
<?php
/**
* AbstractServiceProviderTests class file.
*/
namespace Automattic\WooCommerce\Tests\Internal\DependencyManagement;
use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\Internal\DependencyManagement\ContainerException;
use Automattic\WooCommerce\Internal\DependencyManagement\Definition;
use Automattic\WooCommerce\Internal\DependencyManagement\ExtendedContainer;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\ClassWithInjectionMethodArgumentWithoutTypeHint;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\ClassWithDependencies;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\ClassWithNonFinalInjectionMethod;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\ClassWithPrivateInjectionMethod;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\ClassWithScalarInjectionMethodArgument;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\DependencyClass;
use Automattic\WooCommerce\Vendor\League\Container\Definition\DefinitionInterface;
/**
* Tests for AbstractServiceProvider.
*/
class AbstractServiceProviderTest extends \WC_Unit_Test_Case {
/**
* The system under test.
*
* @var AbstractServiceProvider
*/
private $sut;
/**
* The container used for tests.
*
* @var ExtendedContainer
*/
private $container;
/**
* Runs before each test.
*/
public function setUp() {
$this->container = new ExtendedContainer();
$this->sut = new class() extends AbstractServiceProvider {
// phpcs:disable
/**
* Public version of add_with_auto_arguments, which is usually protected.
*/
public function add_with_auto_arguments( string $class_name, $concrete = null, bool $shared = false ) : DefinitionInterface {
return parent::add_with_auto_arguments( $class_name, $concrete, $shared );
}
/**
* The mandatory 'register' method (defined in the base class as abstract).
* Not implemented because this class is tested on its own, not as a service provider actually registered on a container.
*/
public function register() {}
// phpcs:enable
};
$this->sut->setContainer( $this->container );
}
/**
* Runs before all the tests of the class.
*/
public static function setUpBeforeClass() {
/**
* Return a new instance of ClassWithDependencies.
*
* @param DependencyClass $dependency The dependency to inject.
* @return ClassWithDependencies The new instance.
*/
function get_new_dependency_class( DependencyClass $dependency ) {
return new ClassWithDependencies( $dependency );
};
}
/**
* @testdox 'add_with_auto_arguments' should throw an exception if an invalid class name is passed as class name.
*/
public function test_add_with_auto_arguments_throws_on_non_class_passed_as_class_name() {
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "You cannot add 'foobar', only classes in the Automattic\WooCommerce\ namespace are allowed." );
$this->sut->add_with_auto_arguments( 'foobar' );
}
/**
* @testdox 'add_with_auto_arguments' should throw an exception if the passed class has a private injection method.
*/
public function test_add_with_auto_arguments_throws_on_class_private_method_injection() {
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "Method '" . Definition::INJECTION_METHOD . "' of class '" . ClassWithPrivateInjectionMethod::class . "' isn't 'public', instances can't be created." );
$this->sut->add_with_auto_arguments( ClassWithPrivateInjectionMethod::class );
}
/**
* @testdox 'add_with_auto_arguments' should throw an exception if the passed class has a non-final injection method.
*/
public function test_add_with_auto_arguments_throws_on_class_non_final_method_injection() {
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "Method '" . Definition::INJECTION_METHOD . "' of class '" . ClassWithNonFinalInjectionMethod::class . "' isn't 'final', instances can't be created." );
$this->sut->add_with_auto_arguments( ClassWithNonFinalInjectionMethod::class );
}
/**
* @testdox 'add_with_auto_arguments' should throw an exception if the passed concrete is a class with a private injection method.
*/
public function test_add_with_auto_arguments_throws_on_concrete_private_method_injection() {
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "Method '" . Definition::INJECTION_METHOD . "' of class '" . ClassWithPrivateInjectionMethod::class . "' isn't 'public', instances can't be created." );
$this->sut->add_with_auto_arguments( ClassWithDependencies::class, ClassWithPrivateInjectionMethod::class );
}
/**
* @testdox 'add_with_auto_arguments' should throw an exception if the passed concrete is a class with a non-final injection method.
*/
public function test_add_with_auto_arguments_throws_on_concrete_non_final_method_injection() {
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "Method '" . Definition::INJECTION_METHOD . "' of class '" . ClassWithNonFinalInjectionMethod::class . "' isn't 'final', instances can't be created." );
$this->sut->add_with_auto_arguments( ClassWithDependencies::class, ClassWithNonFinalInjectionMethod::class );
}
/**
* @testdox 'add_with_auto_arguments' should throw an exception if the passed class has a method argument without type hint.
*/
public function test_add_with_auto_arguments_throws_on_method_argument_without_type_hint() {
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "Argument 'argument_without_type_hint' of class '" . ClassWithInjectionMethodArgumentWithoutTypeHint::class . "' doesn't have a type hint or has one that doesn't specify a class." );
$this->sut->add_with_auto_arguments( ClassWithInjectionMethodArgumentWithoutTypeHint::class );
}
/**
* @testdox 'add_with_auto_arguments' should throw an exception if the passed class has a method argument with a scalar type hint.
*/
public function test_add_with_auto_arguments_throws_on_method_argument_with_scalar_type_hint() {
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "Argument 'scalar_argument_without_default_value' of class '" . ClassWithScalarInjectionMethodArgument::class . "' doesn't have a type hint or has one that doesn't specify a class." );
$this->sut->add_with_auto_arguments( ClassWithScalarInjectionMethodArgument::class );
}
/**
* @testdox 'add_with_auto_arguments' should properly register the supplied class when no concrete is passed.
*
* @testWith [true, 1]
* [false, 2]
*
* @param bool $shared Whether to register the test class as shared or not.
* @param int $expected_constructions_count Expected number of times that the test class will have been instantiated.
*/
public function test_add_with_auto_arguments_works_as_expected_with_no_concrete( bool $shared, int $expected_constructions_count ) {
ClassWithDependencies::$instances_count = 0;
$this->container->share( DependencyClass::class );
$this->sut->add_with_auto_arguments( ClassWithDependencies::class, null, $shared );
$this->container->get( ClassWithDependencies::class );
$resolved = $this->container->get( ClassWithDependencies::class );
// A new instance is created for each resolution or not, depending on $shared.
$this->assertEquals( $expected_constructions_count, ClassWithDependencies::$instances_count );
// Arguments with default values are honored.
$this->assertEquals( ClassWithDependencies::SOME_NUMBER, $resolved->some_number );
// Method arguments are filled as expected.
$this->assertSame( $this->container->get( DependencyClass::class ), $resolved->dependency_class );
}
/**
* @testdox 'add_with_auto_arguments' should properly register the supplied class when a concrete representing a class name is passed.
*/
public function test_add_with_auto_arguments_works_as_expected_when_concrete_is_class_name() {
$this->sut->add_with_auto_arguments( ClassWithDependencies::class, DependencyClass::class );
$resolved = $this->container->get( ClassWithDependencies::class );
$this->assertInstanceOf( DependencyClass::class, $resolved );
}
/**
* @testdox 'add_with_auto_arguments' should properly register the supplied class when a concrete that is an object is passed.
*/
public function test_add_with_auto_arguments_works_as_expected_when_concrete_is_object() {
$object = new DependencyClass();
$this->sut->add_with_auto_arguments( ClassWithDependencies::class, $object );
$resolved = $this->container->get( ClassWithDependencies::class );
$this->assertSame( $object, $resolved );
}
/**
* @testdox 'add_with_auto_arguments' should properly register the supplied class when a concrete that is a closure is passed.
*/
public function test_add_with_auto_arguments_works_as_expected_when_concrete_is_a_closure() {
$this->container->share( DependencyClass::class );
$callable = function( DependencyClass $dependency ) {
return new ClassWithDependencies( $dependency );
};
$this->sut->add_with_auto_arguments( ClassWithDependencies::class, $callable );
$resolved = $this->container->get( ClassWithDependencies::class );
$this->assertInstanceOf( ClassWithDependencies::class, $resolved );
}
/**
* @testdox 'add_with_auto_arguments' should properly register the supplied class when a concrete that is a function name is passed.
*/
public function test_add_with_auto_arguments_works_as_expected_when_concrete_is_a_function_name() {
$this->container->share( DependencyClass::class );
$this->sut->add_with_auto_arguments( ClassWithDependencies::class, __NAMESPACE__ . '\get_new_dependency_class' );
$resolved = $this->container->get( ClassWithDependencies::class );
$this->assertInstanceOf( ClassWithDependencies::class, $resolved );
}
}

View File

@ -0,0 +1,52 @@
<?php
/**
* ClassWithDependencies class file.
*/
namespace Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses;
/**
* An example of a class with dependencies that are supplied via constructor arguments.
*/
class ClassWithDependencies {
/**
* Default value for $some_number argument.
*/
const SOME_NUMBER = 34;
/**
* Count of instances of the class created so far.
*
* @var int
*/
public static $instances_count = 0;
/**
* Value supplied to constructor in $some_number argument.
*
* @var int
*/
public $some_number = 0;
/**
* Value supplied to constructor in $dependency_class argument.
*
* @var DependencyClass
*/
public $dependency_class = null;
/**
* Initialize the class instance.
*
* @internal
*
* @param DependencyClass $dependency_class A class we depend on.
* @param int $some_number Some number we need for some reason.
*/
final public function init( DependencyClass $dependency_class, int $some_number = self::SOME_NUMBER ) {
self::$instances_count++;
$this->dependency_class = $dependency_class;
$this->some_number = self::SOME_NUMBER;
}
}

View File

@ -0,0 +1,24 @@
<?php
/**
* ClassWithInjectionMethodArgumentWithoutTypeHint class file.
*
* @package Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses
*/
namespace Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses;
/**
* An example class that has a injector method argument without type hint.
*/
class ClassWithInjectionMethodArgumentWithoutTypeHint {
/**
* Initialize the class instance.
*
* @internal
*
* @param mixed $argument_without_type_hint Anything, really.
*/
final public function init( $argument_without_type_hint ) {
}
}

View File

@ -0,0 +1,24 @@
<?php
/**
* ClassWithNonFinalInjectionMethod class file.
*
* @package Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses
*/
namespace Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses;
/**
* An example of a class with a private injection method.
*/
class ClassWithNonFinalInjectionMethod {
// phpcs:disable WooCommerce.Functions.InternalInjectionMethod.MissingFinal
/**
* Initialize the class instance.
*
* @internal
*/
public function init() {
}
}

View File

@ -0,0 +1,24 @@
<?php
/**
* ClassWithPrivateInjectionMethod class file.
*
* @package Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses
*/
namespace Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses;
/**
* An example of a class with a private injection method.
*/
class ClassWithPrivateInjectionMethod {
// phpcs:disable WooCommerce.Functions.InternalInjectionMethod.MissingPublic
/**
* Initialize the class instance.
*
* @internal
*/
final private function init() {
}
}

View File

@ -0,0 +1,26 @@
<?php
/**
* ClassWithScalarInjectionMethodArgument class file.
*
* @package Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses
*/
namespace Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses;
/**
* An example class that has an injector method argument with a scalar type but without a default value.
*/
class ClassWithScalarInjectionMethodArgument {
// phpcs:disable Squiz.Commenting.FunctionComment.InvalidTypeHint
/**
* Initialize the class instance.
*
* @internal
*
* @param mixed $scalar_argument_without_default_value Anything, really.
*/
final public function init( int $scalar_argument_without_default_value ) {
}
}

View File

@ -0,0 +1,37 @@
<?php
/**
* ClassWithSingleton class file.
*/
// This class is in the root namespace on purpose, since it simulates being a legacy class in the 'includes' directory.
/**
* An example of a class that holds a singleton instance.
*/
class ClassWithSingleton {
/**
* @var ClassWithSingleton The singleton instance of the class.
*/
public static $instance;
/**
* @var array The arguments supplied to 'instance'.
*/
public static $instance_args;
/**
* Gets the singleton instance of the class.
*
* @param mixed ...$args Any arguments required by the method.
*
* @return ClassWithSingleton The singleton instance of the class.
*/
public static function instance( ...$args ) {
if ( is_null( self::$instance ) ) {
self::$instance = new ClassWithSingleton();
self::$instance_args = $args;
}
return self::$instance;
}
}

View File

@ -0,0 +1,23 @@
<?php
/**
* DependencyClass class file.
*/
namespace Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses;
/**
* An example of a class other classes depend on.
*/
class DependencyClass {
/**
* Concatenates the supplied string parts just for fun.
*
* @param mixed ...$parts The parts.
*
* @return string The resulting concatenated string.
*/
public static function concat( ...$parts ) {
return 'Parts: ' . join( ', ', $parts );
}
}

View File

@ -0,0 +1,126 @@
<?php
/**
* ExtendedContainerTests class file.
*/
namespace Automattic\WooCommerce\Tests\Internal\DependencyManagement;
use Automattic\WooCommerce\Internal\DependencyManagement\ContainerException;
use Automattic\WooCommerce\Internal\DependencyManagement\ExtendedContainer;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\ClassWithDependencies;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\DependencyClass;
/**
* Tests for ExtendedContainer.
*/
class ExtendedContainerTest extends \WC_Unit_Test_Case {
/**
* The system under test.
*
* @var ExtendedContainer
*/
private $sut;
/**
* Runs before each test.
*/
public function setUp() {
$this->sut = new ExtendedContainer();
}
/**
* @testdox 'add' should throw an exception when trying to register a class not in the WooCommerce root namespace.
*/
public function test_add_throws_when_trying_to_register_class_in_forbidden_namespace() {
$external_class = \WooCommerce::class;
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "You cannot add '$external_class', only classes in the Automattic\WooCommerce\ namespace are allowed." );
$this->sut->add( $external_class );
}
/**
* @testdox 'add' should throw an exception when trying to register a concrete class not in the WooCommerce root namespace.
*/
public function test_add_throws_when_trying_to_register_concrete_class_in_forbidden_namespace() {
$external_class = \WooCommerce::class;
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "You cannot add concrete '$external_class', only classes in the Automattic\WooCommerce\ namespace are allowed." );
$this->sut->add( DependencyClass::class, $external_class );
}
/**
* @testdox 'add' should allow registering classes in the WooCommerce root namespace.
*/
public function test_add_allows_registering_classes_in_woocommerce_root_namespace() {
$instance = new DependencyClass();
$this->sut->add( DependencyClass::class, $instance, true );
$resolved = $this->sut->get( DependencyClass::class );
$this->assertSame( $instance, $resolved );
}
/**
* @testdox 'replace' should throw an exception when trying to replace a class that has not been previously registered.
*/
public function test_replace_throws_if_class_has_not_been_registered() {
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "The container doesn't have '" . DependencyClass::class . "' registered, please use 'add' instead of 'replace'." );
$this->sut->replace( DependencyClass::class, null );
}
/**
* @testdox 'replace'
*/
public function test_replace_throws_if_concrete_not_in_woocommerce_root_namespace() {
$instance = new DependencyClass();
$this->sut->add( DependencyClass::class, $instance, true );
$external_class = \WooCommerce::class;
$this->expectException( ContainerException::class );
$this->expectExceptionMessage( "You cannot use concrete '$external_class', only classes in the Automattic\WooCommerce\ namespace are allowed." );
$this->sut->replace( DependencyClass::class, $external_class );
}
/**
* @testdox 'replace' should allow to replace existing registrations.
*/
public function test_replace_allows_replacing_existing_registrations() {
$instance_1 = new DependencyClass();
$instance_2 = new DependencyClass();
$this->sut->add( DependencyClass::class, $instance_1, true );
$this->assertSame( $instance_1, $this->sut->get( DependencyClass::class ) );
$this->sut->replace( DependencyClass::class, $instance_2, true );
$this->assertSame( $instance_2, $this->sut->get( DependencyClass::class ) );
}
/**
* @testdox 'reset_all_resolved' should discard cached resolutions for classes registered as 'shared'.
*/
public function test_reset_all_resolved_discards_cached_shared_resolutions() {
$this->sut->add( DependencyClass::class );
$this->sut->add( ClassWithDependencies::class, null, true )->addArgument( DependencyClass::class );
ClassWithDependencies::$instances_count = 0;
$this->sut->get( ClassWithDependencies::class );
$this->assertEquals( 1, ClassWithDependencies::$instances_count );
$this->sut->get( ClassWithDependencies::class );
$this->assertEquals( 1, ClassWithDependencies::$instances_count );
$this->sut->reset_all_resolved();
$this->sut->get( ClassWithDependencies::class );
$this->assertEquals( 2, ClassWithDependencies::$instances_count );
$this->sut->get( ClassWithDependencies::class );
$this->assertEquals( 2, ClassWithDependencies::$instances_count );
}
}

View File

@ -0,0 +1,126 @@
<?php
/**
* ClassThatDependsOnLegacyCodeTest class file
*/
namespace Automattic\WooCommerce\Tests\Proxies;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Tests\Proxies\ExampleClasses\ClassThatDependsOnLegacyCode;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\DependencyClass;
/**
* Tests for a class that depends on legacy code
*/
class ClassThatDependsOnLegacyCodeTest extends \WC_Unit_Test_Case {
/**
* The system under test.
*
* @var LegacyProxy
*/
private $sut;
/**
* Runs before each test.
*/
public function setUp() {
$container = wc_get_container();
$container->add( ClassThatDependsOnLegacyCode::class )->addArgument( LegacyProxy::class );
$this->sut = $container->get( ClassThatDependsOnLegacyCode::class );
}
/**
* Legacy proxy's 'call_function' can be used from both an injected LegacyProxy and from 'WC()->call_function'
*
* @param string $method_to_use Method in the tested class to use.
*
* @testWith ["call_legacy_function_using_injected_proxy"]
* ["call_legacy_function_using_woocommerce_class"]
*/
public function test_call_function_can_be_invoked_via_injected_legacy_proxy_and_woocommerce_object( $method_to_use ) {
$this->assertEquals( 255, $this->sut->$method_to_use( 'hexdec', 'FF' ) );
}
/**
* Function mocks can be used from both an injected LegacyProxy and from 'WC()->call_function'
*
* @param string $method_to_use Method in the tested class to use.
*
* @testWith ["call_legacy_function_using_injected_proxy"]
* ["call_legacy_function_using_woocommerce_class"]
*/
public function test_function_mocks_can_be_used_via_injected_legacy_proxy_and_woocommerce_object( $method_to_use ) {
$this->register_legacy_proxy_function_mocks(
array(
'hexdec' => function( $hex_string ) {
return "Mocked hexdec for $hex_string";
},
)
);
$this->assertEquals( 'Mocked hexdec for FF', $this->sut->$method_to_use( 'hexdec', 'FF' ) );
}
/**
* Legacy proxy's 'call_static' can be used from both an injected LegacyProxy and from 'WC()->call_function'
*
* @param string $method_to_use Method in the tested class to use.
*
* @testWith ["call_static_method_using_injected_proxy"]
* ["call_static_method_using_woocommerce_class"]
*/
public function test_call_static_can_be_invoked_via_injected_legacy_proxy_and_woocommerce_object( $method_to_use ) {
$result = $this->sut->$method_to_use( DependencyClass::class, 'concat', 'foo', 'bar', 'fizz' );
$this->assertEquals( 'Parts: foo, bar, fizz', $result );
}
/**
* Static method mocks can be used from both an injected LegacyProxy and from 'WC()->call_function'
*
* @param string $method_to_use Method in the tested class to use.
*
* @testWith ["call_static_method_using_injected_proxy"]
* ["call_static_method_using_woocommerce_class"]
*/
public function test_static_mocks_can_be_used_via_injected_legacy_proxy_and_woocommerce_object( $method_to_use ) {
$this->register_legacy_proxy_static_mocks(
array(
DependencyClass::class => array(
'concat' => function( ...$parts ) {
return "I'm returning concat of these parts: " . join( ' ', $parts );
},
),
)
);
$expected = "I'm returning concat of these parts: foo bar fizz";
$result = $this->sut->$method_to_use( DependencyClass::class, 'concat', 'foo', 'bar', 'fizz' );
$this->assertEquals( $expected, $result );
}
/**
* Legacy proxy's 'get_instance_of' can be used from both an injected LegacyProxy and from 'WC()->call_function'
*
* @param string $method_to_use Method in the tested class to use.
*
* @testWith ["get_instance_of_using_injected_proxy"]
* ["get_instance_of_using_woocommerce_class"]
*/
public function test_get_instance_of_can_be_used_via_injected_legacy_proxy_and_woocommerce_object( $method_to_use ) {
$instance = $this->sut->$method_to_use( \WC_Queue_Interface::class, 34 );
$this->assertInstanceOf( \WC_Action_Queue::class, $instance );
}
/**
* Legacy object mocks can be used from both an injected LegacyProxy and from 'WC()->call_function'
*
* @param string $method_to_use Method in the tested class to use.
*
* @testWith ["get_instance_of_using_injected_proxy"]
* ["get_instance_of_using_woocommerce_class"]
*/
public function test_class_mocks_can_be_used_via_injected_legacy_proxy_and_woocommerce_object( $method_to_use ) {
$mock = new \stdClass();
$this->register_legacy_proxy_class_mocks( array( \WC_Query::class => $mock ) );
$this->assertSame( $mock, $this->sut->$method_to_use( \WC_Query::class ) );
}
}

View File

@ -0,0 +1,106 @@
<?php
/**
* ClassThatDependsOnLegacyCode class file
*/
namespace Automattic\WooCommerce\Tests\Proxies\ExampleClasses;
use Automattic\WooCommerce\Proxies\LegacyProxy;
/**
* An example class that uses the legacy proxy both from a dependency injected proxy and from the helper methods in the WooCommerce class.
*/
class ClassThatDependsOnLegacyCode {
/**
* The injected LegacyProxy.
*
* @var LegacyProxy
*/
private $legacy_proxy;
/**
* Initialize the class instance.
*
* @internal
*
* @param LegacyProxy $legacy_proxy The instance of LegacyProxy to use.
*/
final public function init( LegacyProxy $legacy_proxy ) {
$this->legacy_proxy = $legacy_proxy;
}
/**
* Use proxy's 'call_function' from the injected proxy.
*
* @param string $function Function to call.
* @param mixed ...$parameters Parameters to pass to the function.
*
* @return mixed The result from the function.
*/
public function call_legacy_function_using_injected_proxy( $function, ...$parameters ) {
return $this->legacy_proxy->call_function( $function, ...$parameters );
}
/**
* Use proxy's 'call_function' using 'WC()->call_function'.
*
* @param string $function Function to call.
* @param mixed ...$parameters Parameters to pass to the function.
*
* @return mixed The result from the function.
*/
public function call_legacy_function_using_woocommerce_class( $function, ...$parameters ) {
return WC()->call_function( $function, ...$parameters );
}
/**
* Use proxy's 'call_static' from the injected proxy.
*
* @param string $class_name Class containing the static method to call.
* @param string $method_name Static method to call.
* @param mixed ...$parameters Parameters to pass to the method.
*
* @return mixed The result from the method.
*/
public function call_static_method_using_injected_proxy( $class_name, $method_name, ...$parameters ) {
return $this->legacy_proxy->call_static( $class_name, $method_name, ...$parameters );
}
/**
* Use proxy's 'call_static' using 'WC()->call_function'.
*
* @param string $class_name Class containing the static method to call.
* @param string $method_name Static method to call.
* @param mixed ...$parameters Parameters to pass to the method.
*
* @return mixed The result from the method.
*/
public function call_static_method_using_woocommerce_class( $class_name, $method_name, ...$parameters ) {
return WC()->call_static( $class_name, $method_name, ...$parameters );
}
/**
* Use proxy's 'get_instance_of' from the injected proxy.
*
* @param string $class_name The name of the class to get an instance of.
* @param mixed ...$args Extra arguments for 'get_instance_of'.
*
* @return object The instance obtained.
*/
public function get_instance_of_using_injected_proxy( string $class_name, ...$args ) {
return $this->legacy_proxy->get_instance_of( $class_name, ...$args );
}
/**
* Use proxy's 'get_instance_of' using 'WC()->call_function'.
*
* @param string $class_name The name of the class to get an instance of.
* @param mixed ...$args Extra arguments for 'get_instance_of'.
*
* @return object The instance obtained.
*/
public function get_instance_of_using_woocommerce_class( string $class_name, ...$args ) {
return WC()->get_instance_of( $class_name, ...$args );
}
}

View File

@ -0,0 +1,86 @@
<?php
/**
* LegacyProxyTests class file
*/
namespace Automattic\WooCommerce\Tests\Proxies;
use Automattic\WooCommerce\Internal\DependencyManagement\Definition;
use Automattic\WooCommerce\Proxies\LegacyProxy;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\DependencyClass;
/**
* Tests for LegacyProxy
*/
class LegacyProxyTest extends \WC_Unit_Test_Case {
/**
* The system under test.
*
* @var LegacyProxy
*/
private $sut;
/**
* Runs before each test.
*/
public function setUp() {
$this->sut = new LegacyProxy();
}
/**
* @testdox 'get_instance_of' throws an exception when trying to use it to get an instance of a namespaced class.
*/
public function test_get_instance_of_throws_when_trying_to_get_a_namespaced_class() {
$this->expectException( \Exception::class );
$this->expectExceptionMessage( 'The LegacyProxy class is not intended for getting instances of classes in the src directory, please use ' . Definition::INJECTION_METHOD . ' method injection or the instance of Psr\Container\ContainerInterface for that.' );
$this->sut->get_instance_of( DependencyClass::class );
}
/**
* @testdox 'get_instance_of' can be used to get an instance of a class by using its constructor and passing constructor arguments.
*/
public function test_get_instance_of_can_be_used_to_get_a_non_namespaced_class_with_constructor_parameters() {
$instance = $this->sut->get_instance_of( \WC_Data_Exception::class, 1234, 'Error!', 432 );
$this->assertInstanceOf( \WC_Data_Exception::class, $instance );
$this->assertEquals( 1234, $instance->getErrorCode() );
$this->assertEquals( 'Error!', $instance->getMessage() );
$this->assertEquals( 432, $instance->getCode() );
}
/**
* @testdox 'get_instance_of' uses the 'instance' static method in classes that implement it, passing the supplied arguments.
*/
public function test_get_instance_of_class_with_instance_method_gets_an_instance_of_the_appropriate_class() {
// ClassWithSingleton is in the root namespace and thus can't be autoloaded.
require_once dirname( __DIR__ ) . '/Internal/DependencyManagement/ExampleClasses/ClassWithSingleton.php';
$instance = $this->sut->get_instance_of( \ClassWithSingleton::class, 'foo', 'bar' );
$this->assertSame( \ClassWithSingleton::$instance, $instance );
$this->assertEquals( array( 'foo', 'bar' ), \ClassWithSingleton::$instance_args );
}
/**
* @testdox 'get_instance_of' can be used to get an instance of a class implementing WC_Queue_Interface.
*/
public function test_get_instance_of_wc_queue_interface_gets_an_instance_of_the_appropriate_class() {
$instance = $this->sut->get_instance_of( \WC_Queue_Interface::class, 34 );
$this->assertInstanceOf( \WC_Action_Queue::class, $instance );
}
/**
* @testdox 'call_function' can be used to invoke any standalone function.
*/
public function test_call_function_can_be_used_to_invoke_functions() {
$result = $this->sut->call_function( 'substr', 'foo bar fizz', 4, 3 );
$this->assertEquals( 'bar', $result );
}
/**
* @testdox 'call_static' can be used to invoke any public static class method.
*/
public function test_call_static_can_be_used_to_invoke_public_static_methods() {
$result = $this->sut->call_static( DependencyClass::class, 'concat', 'foo', 'bar', 'fizz' );
$this->assertEquals( 'Parts: foo, bar, fizz', $result );
}
}

View File

@ -0,0 +1,226 @@
<?php
/**
* MockableLegacyProxyTests class file
*/
namespace Automattic\WooCommerce\Tests\Proxies;
use Automattic\WooCommerce\Testing\Tools\DependencyManagement\MockableLegacyProxy;
use Automattic\WooCommerce\Tests\Internal\DependencyManagement\ExampleClasses\DependencyClass;
/**
* Tests for MockableLegacyProxy
*/
class MockableLegacyProxyTest extends \WC_Unit_Test_Case {
/**
* The system under test.
*
* @var MockableLegacyProxy
*/
private $sut;
/**
* Runs before each test.
*/
public function setUp() {
$this->sut = new MockableLegacyProxy();
}
/**
* @testdox 'get_instance_of' works as in LegacyProxy if no class mocks are registered.
*/
public function test_get_instance_of_works_as_regular_legacy_proxy_if_no_mock_registered() {
$instance = $this->sut->get_instance_of( \WC_Data_Exception::class, 1234, 'Error!', 432 );
$this->assertInstanceOf( \WC_Data_Exception::class, $instance );
$this->assertEquals( 1234, $instance->getErrorCode() );
$this->assertEquals( 'Error!', $instance->getMessage() );
$this->assertEquals( 432, $instance->getCode() );
}
/**
* The data provider for test_register_class_mocks_throws_if_invalid_parameters_supplied.
*
* @return array[]
*/
public function data_provider_for_test_register_class_mocks_throws_if_invalid_parameters_supplied() {
return array(
array( 1234, new \stdClass() ),
array( 'SomeClassName', 1234 ),
);
}
/**
* @testdox 'register_class_mocks' throws an exception if an invalid parameter is supplied (not an array of class name => object or factory callback).
*
* @dataProvider data_provider_for_test_register_class_mocks_throws_if_invalid_parameters_supplied
*
* @param string $class_name The name of the class to mock.
* @param object $mock The mock.
*/
public function test_register_class_mocks_throws_if_invalid_parameters_supplied( $class_name, $mock ) {
$this->expectException( \Exception::class );
$this->expectExceptionMessage( 'MockableLegacyProxy::register_class_mocks: $mocks must be an associative array of class_name => object or factory callback.' );
$this->sut->register_class_mocks( array( $class_name => $mock ) );
}
/**
* @testdox 'register_class_mocks' can be used to return class mocks by passing fixed mock instances.
*/
public function test_register_class_mocks_can_be_used_so_that_get_instance_of_returns_a_fixed_instance_mock() {
$mock = new \stdClass();
$this->sut->register_class_mocks( array( \WC_Query::class => $mock ) );
$this->assertSame( $mock, $this->sut->get_instance_of( \WC_Query::class ) );
}
/**
* @testdox 'register_class_mocks' can be used to return class mocks by passing mock factory callbacks.
*/
public function test_register_class_mocks_can_be_used_so_that_get_instance_of_uses_a_factory_function_to_return_the_instance() {
$mock_factory = function( $code, $message, $http_status_code = 400, $data = array() ) {
return "$code, $message, $http_status_code";
};
$this->sut->register_class_mocks( array( \WC_Data_Exception::class => $mock_factory ) );
$this->assertEquals( '1234, Error!, 432', $this->sut->get_instance_of( \WC_Data_Exception::class, 1234, 'Error!', 432 ) );
}
/**
* @testdox 'call_function' works as in LegacyProxy if no function mocks are registered.
*/
public function test_call_function_works_as_regular_legacy_proxy_if_no_mocks_registered() {
$result = $this->sut->call_function( 'substr', 'foo bar fizz', 4, 3 );
$this->assertEquals( 'bar', $result );
}
/**
* The data provider for test_register_function_mocks_throws_if_invalid_parameters_supplied.
*
* @return array[]
*/
public function data_provider_for_test_register_function_mocks_throws_if_invalid_parameters_supplied() {
return array(
array( 1234, function() {} ),
array( 'SomeClassName', 1234 ),
);
}
/**
* @testdox 'register_function_mocks' throws an exception if an invalid parameter is supplied (not an array of function name => mock function).
*
* @dataProvider data_provider_for_test_register_function_mocks_throws_if_invalid_parameters_supplied
*
* @param string $function_name The name of the function to mock.
* @param callable $mock The mock.
*/
public function test_register_function_mocks_throws_if_invalid_parameters_supplied( $function_name, $mock ) {
$this->expectException( \Exception::class );
$this->expectExceptionMessage( 'MockableLegacyProxy::register_function_mocks: The supplied mocks array must have function names as keys and function replacement callbacks as values.' );
$this->sut->register_function_mocks( array( $function_name => $mock ) );
}
/**
* @testdox 'register_function_mocks' can be used to register mocks for any function.
*/
public function test_register_function_mocks_can_be_used_so_that_call_function_calls_mock_functions() {
$this->sut->register_function_mocks(
array(
'substr' => function( $string, $start, $length ) {
return "I'm returning substr of '$string' from $start with length $length";
},
)
);
$expected = "I'm returning substr of 'foo bar fizz' from 4 with length 3";
$result = $this->sut->call_function( 'substr', 'foo bar fizz', 4, 3 );
$this->assertEquals( $expected, $result );
}
/**
* @testdox 'call_static' works as in LegacyProxy if no static method mocks are registered.
*/
public function test_call_static_works_as_regular_legacy_proxy_if_no_mocks_registered() {
$result = $this->sut->call_static( DependencyClass::class, 'concat', 'foo', 'bar', 'fizz' );
$this->assertEquals( 'Parts: foo, bar, fizz', $result );
}
/**
* The data provider for test_register_static_mocks_throws_if_invalid_parameters_supplied.
*
* @return array[]
*/
public function data_provider_for_test_register_static_mocks_throws_if_invalid_parameters_supplied() {
return array(
array( 1234, array( 'some_method' => function(){} ) ),
array( 'SomeClassName', 1234 ),
array( 'SomeClassName', array( 1234 => function(){} ) ),
array( 'SomeClassName', array( 'the_method' => 1234 ) ),
);
}
/**
* @testdox
*
* @dataProvider data_provider_for_test_register_function_mocks_throws_if_invalid_parameters_supplied
*
* @param string $class_name The name of the class whose static methods we want to mock.
* @param array $mocks The mocks.
*/
public function test_register_static_mocks_throws_if_invalid_parameters_supplied( $class_name, $mocks ) {
$this->expectException( \Exception::class );
$this->expectExceptionMessage( 'MockableLegacyProxy::register_static_mocks: $mocks must be an associative array of class name => associative array of method name => callable.' );
$this->sut->register_static_mocks( array( $class_name => $mocks ) );
}
/**
* @testdox 'register_static_mocks' can be used to register mocks for any static method.
*/
public function test_register_static_mocks_can_be_used_so_that_call_function_calls_mock_functions() {
$this->sut->register_static_mocks(
array(
DependencyClass::class => array(
'concat' => function( ...$parts ) {
return "I'm returning concat of these parts: " . join( ' ', $parts );
},
),
)
);
$expected = "I'm returning concat of these parts: foo bar fizz";
$result = $this->sut->call_static( DependencyClass::class, 'concat', 'foo', 'bar', 'fizz' );
$this->assertEquals( $expected, $result );
}
/**
* @testdox 'reset' can be used to revert the instance to its original state, in which nothing is mocked.
*/
public function test_reset_can_be_used_to_unregister_all_mocks() {
$this->sut->register_class_mocks( array( \WC_Query::class => new \stdClass() ) );
$this->sut->register_function_mocks(
array(
'substr' => function( $string, $start, $length ) {
return null;
},
)
);
$this->sut->register_static_mocks(
array(
DependencyClass::class => array(
'concat' => function( ...$parts ) {
return null;
},
),
)
);
$this->sut->reset();
$this->test_call_function_works_as_regular_legacy_proxy_if_no_mocks_registered();
$this->test_call_static_works_as_regular_legacy_proxy_if_no_mocks_registered();
$this->test_call_static_works_as_regular_legacy_proxy_if_no_mocks_registered();
}
}