benpoulson/woocommerce-core

此包的最新版本(dev-main)没有可用的许可信息。

dev-main 2021-05-18 18:55 UTC

This package is auto-updated.

Last update: 2024-09-19 02:27:24 UTC


README

目录

此目录是 Automattic\WooCommerce 命名空间下的新 WooCommerce 类文件的家,使用 PSR-4 文件命名。这是为了充分利用自动加载。

理想情况下,所有 WooCommerce 的新代码都应该由遵循 PSR-4 命名并在该目录中存在的类组成,而 includes 目录 中的代码应只进行必要的更改以修复错误。这可能并不总是可能的,但这是指导原则。

该目录中类的注册和解析使用 PSR-11 容器进行,并通过使用 依赖注入 模式进行。已经有一些工具可以轻松编写单元测试,以便与旧代码(以及通常在 src 目录之外的代码)交互。

安装Composer

Composer 用于生成此目录文件的自动加载类映射。WooCommerce 的稳定版本附带自动加载器,但是如果您正在运行开发版本,则需要使用 Composer。

如果您尚未安装 Composer,请访问Composer 安装说明,然后继续此处。

更新自动加载类映射

如果您向 WooCommerce 添加了一个类,您需要运行以下命令以确保它包含在自动加载类映射中

composer dump-autoload

安装包

要安装 WooCommerce 所需的包,从主目录运行

composer install

要更新包,运行

composer update

容器

WooCommerce 使用一个 PSR-11 兼容的容器,通过使用 依赖注入 模式来注册和解析此目录中的所有类。更具体地说,我们使用 The PHP League 的容器;这在注册类时是相关的,但在解析类时不相关。使用的容器的完整类名为 Automattic\WooCommerce\Container(它使用 PHP League 的容器作为底层)。

**解析** 一个类意味着请求容器提供一个类的实例(或接口)。**注册** 一个类意味着告诉容器如何解析该类。

原则上,容器应用于注册和解析 src 目录中的所有类。例外可能是可以通过旧方式(使用纯 new 语句)创建的数据只类;但作为指导原则,容器始终应该被使用。

根据它们从哪里解析,有两种方法可以解决已注册的类。

  • src 目录中的类将它们的依赖项指定为 init 参数,这些参数在解析类时由容器自动提供(这被称为 依赖注入)。
  • 对于在 includes 目录中的代码,有一个 wc_get_container 函数,它将返回容器,然后可以使用其 get 方法解析任何类。

解析类

根据需要解析的地方,有两种方法可以解决注册的类。

1. 其他 src 目录中的类

src 目录中的类依赖于同一目录中的其他类时,它应该使用方法注入。这意味着将这些依赖项指定为 init 方法中的参数,并带有适当的类型提示,并将它们存储在私有变量中,以便在需要时使用。

use TheService1Namespace\Service1;
use TheService2Namespace\Service2;

class TheClassWithDependencies {
    private $service1;

    private $service2;

    public function init( Service1Class $service1, Service2Class $service2 ) {
        $this->$service1 = $service1;
        $this->$service2 = $service2;
    }

    public function method_that_needs_service_1() {
        $this->service1->do_something();
    }
}

每次容器准备解析 TheClassWithDependencies 时,它也会解析 Service1ClassService2Class,并将它们作为方法参数传递给请求的类。如果这些服务类还有方法参数,那么这些参数也将递归地适当地解决。

如果需要,也可以采取“延迟”方法:您可以将容器本身指定为方法参数(使用 \Psr\Container\ContainerInterface 作为类型提示),并使用它的 get 方法在适当的时间获取所需的实例。

use TheService1Namespace\Service1;

class TheClassWithDependencies {
    private $container;

    public function init( \Psr\Container\ContainerInterface $container ) {
        $this->$container = $container;
    }

    public function method_that_needs_service_1() {
        $this->container->get( Service1::class )->do_something();
    }
}

然而,通常方法注入是首选的,并且仅在必要时才应使用延迟方法。

2. includes 目录中的代码

当您需要在 includes 目录中的旧代码中使用在 src 目录中定义的类时,使用 wc_get_container 函数获取容器实例,然后使用 get 解析所需的类。

use TheService1Namespace\Service1;

function wc_function_that_needs_service_1() {
    $service = wc_get_container()->get( Service1::class );
    $service->do_something();
}

在将代码从 includes 移动到 src 的同时保留旧代码的现有入口点以保持兼容性时,这也被推荐为一种方法。

值得注意的是:当请求解析尚未注册的类时,容器将抛出 ContainerException。所有类在解决之前都需要被注册。

注册类

为了使用容器解析一个类,它需要已经在同一容器中预先注册。

Container 是“只读”的,因为它有一个 get 方法来解析类,但没有注册类的方法。相反,类注册是通过使用 服务提供者 来完成的。这就是创建新类时的整个过程。

首先,在适当的命名空间(以及相应的文件夹)中创建类(记住,src 目录中类的基命名空间是 Atuomattic\WooCommerce)。如果类依赖于 src 中的其他类,请按上述方式将这些依赖项指定为 init 参数。

此类的一个示例

namespace Automattic\WooCommerce\TheClassNamespace;
use Automattic\WooCommerce\TheDependencyNamespace\TheDependencyClass;


class TheClass {
    private $the_dependency;
    
    public function init( TheDependencyClass $dependency ) {
        $this->the_dependency = $dependency;
    }
            
}

然后,在 src/Internal/DependencyManagement/ServiceProviders 文件夹(以及相应的命名空间)中创建一个 <class name>ServiceProvider 类,如下所示

namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;

use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\TheClassNamespace\TheClass;
use Automattic\WooCommerce\TheDependencyNamespace\TheDependencyClass;

class TheClassServiceProvider extends AbstractServiceProvider {

	protected $provides = array(
		TheClass::class
	);

	public function register() {
		$this->add( TheClass::class )->addArgument( TheDependencyClass::class );
	}
}

最后(但同样重要的是,不要忘记这一步!),将服务提供者的类名添加到 Container 类的 $service_providers 属性中。

值得注意的是

  • 在示例中,服务提供者用于注册一个类,但服务提供者可以用于注册一组相关的类。必须包含提供者可以注册的所有类的名称的 $provides 属性。
  • 当解析 $provides 中的任何类时,容器将首次调用提供者的 register 方法。
  • 如果您查看服务提供商文档,您会发现类是通过this->getContainer()->add进行注册的。WooCommerce的AbstractServiceProvider自身添加了一个add工具方法,它具有相同的作用。
  • 您可以使用share代替add来注册单实例类(该类只实例化一次并缓存,因此每次解析类时都返回相同的实例)。

如果正在注册的类有init参数,则必须跟随与所需数量相同的addArguments调用。WooCommerce的AbstractServiceProvider添加了一个工具方法add_with_auto_arguments(以及一个兄弟方法share_with_auto_arguments),它使用反射来确定并注册所有需要类型提示的init参数。请注意,使用此辅助方法时可能产生的性能损失。

一个服务提供商的替代版本,用于注册类及其依赖项,并利用add_with_auto_arguments,可能如下所示

namespace Automattic\WooCommerce\Internal\DependencyManagement\ServiceProviders;

use Automattic\WooCommerce\Internal\DependencyManagement\AbstractServiceProvider;
use Automattic\WooCommerce\TheClassNamespace\TheClass;
use Automattic\WooCommerce\TheDependencyNamespace\TheDependencyClass;

class TheClassServiceProvider extends AbstractServiceProvider {

	protected $provides = array(
		TheClass::class,
        TheDependencyClass::class
	);

	public function register() {
        $this->share( TheDependencyClass::class );
		$this->share_with_auto_arguments( ActionsProxy::class );
	}
}

使用具体类

默认情况下,addshare方法指示容器通过使用new创建类的实例来解析注册的类。但这些方法接受一个可选的$concrete参数,可以用来告诉容器以不同的方式解析类。$concrete可能是以下之一

  • 一个类名

当解析注册的类名时,将实例化提供的类名。这特别适用于注册接口,例如

$this->add( TheInterface::class, TheClassImplementingTheInterface::class );
  • 一个对象

当解析注册的类名时,将返回提供的对象。例如

$instance = new TheClass();
$this->add( TheClass::class, $instance );
  • 一个闭包

当解析注册的类名时,将执行闭包并返回结果值。例如

$factory = function( TheDependencyClass $dependency ) {
    return new TheClass( $dependency );
};

$this->add( TheClass::class, $factory );

请注意,如果闭包定义为具有参数的函数,则还将解析提供的参数。

关于旧类的一些说明

容器旨在仅用于注册src文件夹中的类。有一个检查可以防止在根Automattic\Woocommerce命名空间之外的类被注册。

这意味着src之外的类不能被依赖注入,因此不应用作init参数的类型提示。有机制可以与“外部”代码(包括来自includes文件夹和第三方代码的代码)交互,这使得编写单元测试变得容易。

Internal 命名空间

虽然开发者选择任何新创建的类的适当命名空间取决于他们,但这些命名空间在语义上应该是合理的,但有一个命名空间具有特殊含义:Automattic\WooCommerce\Internal

Automattic\WooCommerce\Internal中的类旨在成为可能会在未来版本中更改的WooCommerce基础设施代码。换句话说,对于该命名空间内的代码,公共表面的向后兼容性不能保证:未来的版本可能包括破坏性更改,包括重命名或重命名类、重命名或删除公共方法或更改公共方法的签名。此命名空间中的代码被认为是“内部”的,而src中的所有其他代码被认为是“公共”的。

这对于您作为开发者的含义取决于您做出的贡献类型

  • 如果您正在开发WooCommerce核心:当您需要添加一个新类时,请仔细考虑该类是否对插件有用。如果您真的这样认为,请将其添加到以Automattic\WooCommerce为根的适当命名空间。如果不,请将其添加到适当的命名空间,但其根为Automattic\WooCommerce\Internal

    • 如有疑问,请始终将代码内部化。如果后来认为内部类值得公开,更改可以轻松进行(只需更改类命名空间即可),而且不会出现任何问题。相反,将公开类转换为内部类则是不可能的,因为这可能会破坏现有的插件。
  • 如果您是插件开发者:您绝对不应该在插件中使用来自 Automattic\WooCommerce\Internal 命名空间中的代码。这样做可能会导致您的插件在 WooCommerce 的未来版本中出现问题。

与旧代码交互

在这里,“遗留代码”主要指的是 includes 目录中的旧 WooCommerce 代码,但本节中描述的机制对于处理 src 目录之外的任何代码都很有用。

src 目录中的代码当然可以直接与遗留代码交互。需要调用一个函数?就调用它。需要一个对象的实例?就实例化它。问题是这会使代码难以测试:很难模拟函数(除非您使用 技巧,或者直接使用 new 或通过 TheClass::instance() 方法获取实例的对象)。

但我们希望 WooCommerce 代码库(特别是 src 中的代码)被单元测试很好地覆盖,因此已经有一些机制可以在保持代码可测试的同时与遗留代码交互。

LegacyProxy

LegacyProxy 是一个类,它包含三个公开方法,旨在允许与遗留代码交互

  • get_instance_of:检索遗留(非 src)类的实例。
  • call_function:调用独立函数。
  • call_static:调用类中的静态方法。

每当 src 类需要获取遗留类的实例、调用函数或从另一个类调用静态方法,并且这将使代码难以测试时,它应该使用 LegacyProxy 方法。

但是,使用 LegacyProxy 如何有助于使代码可测试?技巧在于当测试运行时,注册的是 MockableLegacyProxy 的实例,而不是 LegacyProxy,这是一个具有相同公开表面的类,但具有允许轻松模拟遗留类、函数和静态方法的方法。

使用旧代理

LegacyProxy 是一个在容器中注册的类,就像其他任何类一样,因此可以通过依赖注入获得其实例

use Automattic\WooCommerce\Proxies\LegacyProxy;

class TheClass {
    private $legacy_proxy;

    public function init( LegacyProxy $legacy_proxy ) {
        $this->legacy_proxy = $legacy_proxy;            
    }

    public function do_something_using_some_function() {
        $this->legacy_proxy->call_function( 'the_function_name', 'param1', 'param2' );
    }
}

然而,推荐的方法(尤其是当不需要其他依赖项进行依赖注入时)是通过 WooCommerce 类中的等效方法使用,通过 WC() 辅助工具,如下所示

class TheClass {
    public function do_something_using_some_function() {
        WC()->call_function( 'the_function_name', 'param1', 'param2' );
    }
}

两种方式在底层是完全等效的,因为辅助方法只是在 wc_get_container()->get( LegacyProxy::class )->... 中进行操作。

在测试中使用可模拟的代理

当单元测试运行时,容器将在解决 LegacyProxy 时返回 MockableLegacyProxy 的实例。这个类具有与 LegacyProxy 相同的公开方法,但还具有以下方法

  • register_class_mocks:为通过 get_instance_of 获取的类定义模拟。
  • register_function_mocks:为通过 call_function 调用的函数定义模拟。
  • register_static_mocks:为通过 call_static 调用的函数定义模拟。

这些方法可以通过 wc_get_container()->get( LegacyProxy::class )->register... 直接从测试中访问,但首选的方法是使用 WC_Unit_Test_Case 类提供的等效辅助方法:register_legacy_proxy_class_mocksregister_legacy_proxy_function_mocksregister_legacy_proxy_static_mocks

以下是如何定义函数模拟的示例

// In this context '$this' is a class that extends WC_Unit_Test_Case

$this->register_legacy_proxy_function_mocks(
	array(
		'the_function_name' => function( $param1, $param2 ) {
			return "I'm the mock of the_function_name and I was invoked with $param1 and $param2.";
		},
	)
);

当然,对于没有定义模拟的情况,MockableLegacyProxy 的工作方式与 LegacyProxy 相同。

请参阅MockableLegacyProxy类的代码及其单元测试示例,以获取更详细的用法说明。

get_instance_of 是如何工作的?

我们使用容器来解析位于src目录中的类实例,但旧版代理的get_instance_of如何知道如何解析旧版类呢?

这主要是一个临时过程。当一个类有特殊的实例化或检索方式(例如,一个静态的instance方法),则使用该方式;否则,方法回退到简单地使用new创建类的新的实例。

这意味着get_instance_of方法很可能需要随着时间的推移而发展,以覆盖更多的特殊情况。请查看LegacyProxy方法中的代码,以了解如何正确修改该方法。

创建专用代理

虽然使用旧版代理有助于代码的可测试性,但它可能会使代码的阅读或维护变得有些困难,因此应谨慎使用,并且仅在真正需要使其代码可正确测试时使用。

话虽如此,另一个折衷方案是创建更多针对常用旧版代码的专用案例,例如

class ActionsProxy {
	public function did_action( $tag ) {
		return did_action( $tag );
	}

	public function apply_filters( $tag, $value, ...$parameters ) {
		return apply_filters( $tag, $value, ...$parameters );
	}
}

然而,请注意,此类必须显式地进行依赖注入(除非在WooCommerce类中定义了额外的辅助方法),并且您需要创建一个配对模拟类(例如MockableActionsProxy),并使用wc_get_container()->replace( ActionsProxy::class, MockableActionsProxy::class )替换原始注册。

定义新的操作和过滤器

WordPress的钩子(动作和过滤器)是一个非常强大的扩展机制,它是允许WooCommerce扩展进行开发的核心理工工具。然而,它经常被(滥用)在WooCommerce核心代码库中用来驱动内部逻辑,例如,一个动作在一个类或函数内部触发,假设某个地方有其他类或函数将处理它并继续应有的处理。

为了使代码尽可能易于阅读和维护,不应使用钩子来驱动WooCommerce的内部逻辑和流程。如果您需要特定类或函数的服务,请直接调用这些服务(通过使用依赖注入或旧版代理以获得适当的访问权限)。仅当钩子提供对插件有价值的扩展点时,才应引入新的钩子

像往常一样,可能会有合理的例外情况;但请记住,在考虑创建新钩子时始终牢记此规则。

编写单元测试

单元测试是保持代码可靠性和相对安全的回归错误的基本工具。为此,任何添加到WooCommerce代码库中的新代码,尤其是到src目录的代码,都应该合理地由此类测试覆盖。

如果您是WooCommerce核心团队成员或其他Automattic团队的贡献者:请编写单元测试来覆盖您对src目录(顺便说一句,理想情况下是includes目录)所做的任何代码添加或修改。总会有合理的例外,但通常规则是所有代码都应该由测试覆盖。

如果您是外部贡献者:当在WooCommerce代码库中添加或更改代码,尤其是在src目录中时,添加单元测试是推荐的但不是强制的:不会因为缺少单元测试而拒绝任何贡献。然而,请尝试至少通过尊重容器和依赖注入机制,并在需要时使用旧版代理与旧版代码交互,使代码易于测试。如果您这样做,WooCommerce团队或其他贡献者将能够添加缺失的测试。

模拟依赖项

由于本目录中类的所有依赖项都是通过依赖注入或通过直接访问容器来懒加载的,因此可以通过手动创建一个具有相同公开表面的模拟类,或者使用PHPUnit的测试双胞胎来轻松模拟它们。

$dependency_mock = somehow_create_mock();
$sut = new TheClassToTest( $dependency_mock ); //sut = System Under Test
$result = $sut->do_something();
$this->assertEquals( $result, 'the expected result' );

然而,虽然这在简单场景下效果良好,但在现实世界中,依赖项往往还有其他的依赖,因此实例化所有所需的中间对象将会变得复杂。为了简化问题,当测试运行时,将Container类替换为具有几个额外方法的ExtendedContainer类。

  • replace:允许为给定的类注册定义一个新的替换具体实现。
  • reset_all_resolved:丢弃所有缓存的解决方案。当模拟已定义为共享的类时,您可能需要它。

值得注意的是,在单元测试会话启动时,会调用一次reset_all_resolved来重置在WC安装期间创建的任何缓存解决方案,并且使用replace来交换LegacyProxyMockableLegacyProxy

使用replace的相同示例

$dependency_mock = somehow_create_mock();
$container = wc_get_container();
$container->reset_all_resolved(); //if either the SUT or the dependency are shared
$container->replace( TheDependencyClass::class, $dependency_mock );
$sut = $container->get( TheClassToTest::class );
$result = $sut->do_something();
$this->assertEquals( $result, 'the expected result' );

注意:当然,所有这些也适用于src目录中的依赖项;对于模拟旧依赖项,应使用MockableLegacyProxy而不是MockableLegacyProxy