function(...); * ); * * StaticWrapper::set_mock_functions_for('MyClass', $functions); * * You can use 2 and 3 at the same time, functions have precedence over mock class methods in case of conflict. * * @package Automattic\WooCommerce\Testing\Tools\CodeHacking */ abstract class StaticWrapper { // phpcs:disable Squiz.Commenting.VariableComment.Missing protected static $mock_class_name = null; protected static $methods_in_mock_class = array(); protected static $mocking_functions = array(); private static $wrapper_suffix = '_Wrapper'; // phpcs:enable Squiz.Commenting.VariableComment.Missing /** * Define a new class that inherits from StaticWrapper, to be used as a static wrapper for a given class. * * @param string $class_name Class for which the static wrapper will be generated. * * @return string Name of the generated class. */ public static function define_for( $class_name ) { $wrapper_name = self::wrapper_for( $class_name ); // phpcs:ignore Squiz.PHP.Eval.Discouraged eval( 'class ' . $wrapper_name . ' extends ' . __CLASS__ . ' {}' ); return $wrapper_name; } /** * Returns the name of the wrapper class that define_for will create for a given class. * * @param string $class_name Class to return the wrapper name for. * * @return string Name of the class that define_for defines. */ public static function wrapper_for( $class_name ) { return $class_name . self::$wrapper_suffix; } /** * Registers the class that will be used to mock the original class. * Static methods called in the wrapper class that also exist in the mock class will be redirected * to the mock class. * * @param string $mock_class_name Mock class whose methods will be invoked. * * @throws \ReflectionException Error when creating ReflectionClass or ReflectionMethod. */ public static function set_mock_class( $mock_class_name ) { self::$mock_class_name = $mock_class_name; $rc = new ReflectionClass( $mock_class_name ); $static_methods = $rc->getMethods( ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_STATIC ); $static_methods = array_map( function( $item ) { return $item->getName(); }, $static_methods ); self::$methods_in_mock_class = $static_methods; } /** * This method is the same as set_mock_class, but it's intended to be invoked directly on StaticWrapper, * passing the original class name as a parameter. * * @param string $class_name Class name whose set_mock_class method will be invoked. * @param string $mock_class_name Parameter that will be passed to set_mock_class method in $class_name. */ public static function set_mock_class_for( $class_name, $mock_class_name ) { ( self::wrapper_for( $class_name ) )::set_mock_class( $mock_class_name ); } /** * Registers an array of functions that will be used to mock the original class. It must be an associative * array of name => function. Static methods called in the wrapper class having a corresponding key in the array * will be redirected to the corresponding function. * * @param array $mocking_functions Associative array where keys are method names and values are functions. */ public static function set_mock_functions( $mocking_functions ) { self::$mocking_functions = $mocking_functions; } /** * This method is the same as set_mock_functions, but it's intended to be invoked directly on StaticWrapper, * passing the original class name as a parameter. * * @param string $class_name Class name whose set_mock_functions method will be invoked. * @param array $mocking_functions Parameter that will be passed to set_mock_functions method in $class_name. */ public static function set_mock_functions_for( $class_name, $mocking_functions ) { ( self::wrapper_for( $class_name ) )::set_mock_functions( $mocking_functions ); } /** * Intercepts calls to undefined static methods in the wrapper and redirects them to the mock class/function * if available, or to the original class otherwise. * * @param string $name Method name. * @param array $arguments Method arguments. * * @return mixed Return value from the invoked method. */ public static function __callStatic( $name, $arguments ) { if ( array_key_exists( $name, self::$mocking_functions ) ) { return call_user_func( self::$mocking_functions[ $name ], ...$arguments ); } elseif ( ! is_null( self::$mock_class_name ) && in_array( $name, self::$methods_in_mock_class, true ) ) { return ( self::$mock_class_name )::$name( ...$arguments ); } else { $original_class_name = preg_replace( '/' . self::$wrapper_suffix . '$/', '', get_called_class() ); return $original_class_name::$name( ...$arguments ); } } }