到达这里正好2年,原来的问题被问之后,有几件事情我想指出。(永远不要让我指出很多事情)。
正确的挂钩
要实例化插件类,应使用适当的钩子。它没有通用的规则,因为它取决于类的作用。
使用像这样的早期钩子"plugins_loaded"
通常是没有意义的,因为这样的钩子是针对admin,frontend和AJAX请求而触发的,但是通常情况下,后面的钩子要好得多,因为它只允许在需要时实例化插件类。
例如,可以在上实例化为模板填充内容的类"template_redirect"
。
一般来说,很少需要在启动之前实例化一个类"wp_loaded"
。
没有神级
在较早的答案中用作示例的所有类中的大多数都使用名为like "Prefix_Example_Plugin"
或"My_Plugin"
... 的类。这表明该插件可能有一个主类。
好吧,除非一个插件是由一个类构成的(在这种情况下,以插件名称命名是绝对合理的),否则,要创建一个类来管理整个插件(例如,添加一个插件需要的所有钩子或实例化所有其他插件类) )被视为不当行为,例如以神物为例。
在面向对象的编程中,代码应倾向于SOLID,其中“ S”代表“单一责任原则”。
这意味着每个班级都应该做一件事情。在WordPress插件开发中,这意味着开发人员应避免使用单个挂钩实例化主插件类,但根据类职责,应使用不同的挂钩实例化不同的类。
避免在构造函数中使用钩子
这里在其他答案中已经引入了该论点,但是我想说明一下这个概念,并将这个其他答案链接到在单元测试的范围内已经得到相当广泛解释的地方。
差不多2015年:PHP 5.2适用于僵尸
自2014年8月14日起,PHP 5.3寿终正寝。肯定死了。2015年全年将支持PHP 5.4,这意味着我正在撰写此书。
但是,WordPress仍然支持PHP 5.2,但是没有人可以编写一行支持该版本的代码,尤其是当代码为OOP时。
有不同的原因:
- PHP 5.2很久以前就死了,没有针对它的安全修复程序发布,这意味着它不安全
- PHP 5.3增加了很多功能,PHP,匿名函数和命名空间 高于一切
- 新版本的PHP 更快。PHP是免费的。免费更新。如果可以免费使用更快,更安全的版本,为什么还要使用速度较慢,不安全的版本?
如果您不想使用PHP 5.4+代码,请至少使用5.3+
例
现在是时候根据我在这里所说的内容来回顾较早的答案了。
一旦我们不再关心5.2,就可以并且应该使用名称空间。
为了更好地解释单一职责原则,我的示例将使用3个类,一个类在前端做某事,一个在后端做事,第三个在两种情况下都使用。
管理员班:
namespace GM\WPSE\Example;
class AdminStuff {
private $tools;
function __construct( ToolsInterface $tools ) {
$this->tools = $tools;
}
function setup() {
// setup class, maybe add hooks
}
}
前端类:
namespace GM\WPSE\Example;
class FrontStuff {
private $tools;
function __construct( ToolsInterface $tools ) {
$this->tools = $tools;
}
function setup() {
// setup class, maybe add hooks
}
}
工具界面:
namespace GM\WPSE\Example;
interface ToolsInterface {
function doSomething();
}
还有另外两个使用的Tools类:
namespace GM\WPSE\Example;
class Tools implements ToolsInterface {
function doSomething() {
return 'done';
}
}
有了此类,我可以使用适当的钩子实例化它们。就像是:
require_once plugin_dir_path( __FILE__ ) . 'src/ToolsInterface.php';
require_once plugin_dir_path( __FILE__ ) . 'src/Tools.php';
add_action( 'admin_init', function() {
require_once plugin_dir_path( __FILE__ ) . 'src/AdminStuff.php';
$tools = new GM\WPSE\Example\Tools;
global $admin_stuff; // this is not ideal, reason is explained below
$admin_stuff = new GM\WPSE\Example\AdminStuff( $tools );
} );
add_action( 'template_redirect', function() {
require_once plugin_dir_path( __FILE__ ) . 'src/FrontStuff.php';
$tools = new GM\WPSE\Example\Tools;
global $front_stuff; // this is not ideal, reason is explained below
$front_stuff = new GM\WPSE\Example\FrontStuff( $tools );
} );
依赖倒置和依赖注入
在上面的示例中,我使用了名称空间和匿名函数在不同的钩子上实例化了不同的类,并将上述内容付诸实践。
注意名称空间如何允许创建没有任何前缀的命名类。
我应用了上面间接提到的另一个概念:依赖注入,这是应用依赖倒置原则(SOLID首字母缩略词“ D” )的一种方法。
Tools
当实例化其他两个类时,该类将“注入”,因此可以分离责任。
此外,AdminStuff
和FrontStuff
类使用类型提示来声明它们需要实现的类ToolsInterface
。
通过这种方式,我们自己或使用我们代码的用户可以使用同一接口的不同实现,从而使我们的代码不耦合到具体的类,而是耦合到抽象:这正是依赖关系反转原理的含义。
但是,可以进一步改善上述示例。让我们看看如何。
自动装带器
编写具有更好可读性的OOP代码的一种好方法是不要将类型(接口,类)定义与其他代码混合,也不要将每种类型放入其自己的文件中。
该规则也是PSR-1编码标准1之一。
但是,这样做之前,需要一个包含类的文件,然后才能使用它。
这可能会让人不知所措,但是PHP提供了实用程序功能,可以根据需要使用根据名称加载文件的回调自动加载类。
使用名称空间变得非常容易,因为现在可以将文件夹结构与名称空间结构进行匹配。
这不仅可行,而且是另一个PSR标准(或更好的2:现在已弃用PSR-0和PSR-4)。
遵循该标准,可以使用处理自动加载的不同工具,而不必编写自定义自动加载器。
我不得不说WordPress编码标准对文件的命名有不同的规则。
因此,在为WordPress核心编写代码时,开发人员必须遵循WP规则,但是在编写自定义代码时,这是开发人员的选择,但是使用PSR标准更容易使用已编写的工具2。
全局访问,注册表和服务定位器模式。
在WordPress中实例化插件类时,最大的问题之一是如何从代码的各个部分访问它们。
WordPress本身使用全局方法:变量保存在全局范围内,使它们可在任何地方访问。每个WP开发人员global
在其职业生涯中都会键入数千个单词。
这也是我在上面的示例中使用的方法,但这是邪恶的。
这个答案已经太长了,无法让我进一步解释原因,但是阅读SERP中“全局变量邪恶”的第一个结果是一个很好的起点。
但是如何避免全局变量呢?
有不同的方法。
这里的一些较旧的答案使用静态实例方法。
public static function instance() {
if ( is_null( self::$instance ) ) {
self::$instance = new self;
}
return self::$instance;
}
这很简单,也很不错,但是它会强制为我们要访问的每个类实现该模式。
而且,很多时候这种方法都陷入了神类问题,因为开发人员使用此方法可访问主类,然后使用它来访问所有其他类。
我已经解释了上帝类是多么糟糕,所以当插件只需要使一个或两个类可访问时,静态实例方法是一个不错的选择。
这并不意味着它只能用于只有几个类的插件,实际上,当正确使用依赖注入原理时,就可以创建相当复杂的应用程序,而无需使大量全局访问对象。
但是,有时插件需要使某些类可访问,在这种情况下,静态实例方法不堪重负。
另一种可能的方法是使用注册表模式。
这是一个非常简单的实现:
namespace GM\WPSE\Example;
class Registry {
private $storage = array();
function add( $id, $class ) {
$this->storage[$id] = $class;
}
function get( $id ) {
return array_key_exists( $id, $this->storage ) ? $this->storage[$id] : NULL;
}
}
使用此类,可以通过ID将对象存储在注册表对象中,因此可以访问注册表,而可以访问所有对象。当然,当第一次创建对象时,需要将其添加到注册表中。
例:
global $registry;
if ( is_null( $registry->get( 'tools' ) ) ) {
$tools = new GM\WPSE\Example\Tools;
$registry->add( 'tools', $tools );
}
if ( is_null( $registry->get( 'front' ) ) ) {
$front_stuff = new GM\WPSE\Example\FrontStuff( $registry->get( 'tools' ) );
$registry->add( 'front', front_stuff );
}
add_action( 'wp', array( $registry->get( 'front' ), 'wp' ) );
上面的示例清楚表明,要使注册表有用,需要全局访问。为的只是注册表中的全局变量是不是很糟糕,但是对于非全局纯粹主义者也可以实现对注册表,或可能与一个静态变量的函数的静态实例的方法:
function gm_wpse_example_registry() {
static $registry = NULL;
if ( is_null( $registry ) ) {
$registry = new GM\WPSE\Example\Registry;
}
return $registry;
}
第一次调用该函数将实例化注册表,在随后的调用中它将仅返回它。
使类可全局访问的另一种特定于WordPress的方法是从过滤器返回对象实例。像这样:
$registry = new GM\WPSE\Example\Registry;
add_filter( 'gm_wpse_example_registry', function() use( $registry ) {
return $registry;
} );
之后,到处都需要注册表:
$registry = apply_filters( 'gm_wpse_example_registry', NULL );
可以使用的另一种模式是服务定位器模式。它类似于注册表模式,但是服务定位器使用依赖注入传递给各种类。
这种模式的主要问题是它隐藏了类的依赖关系,使得代码难以维护和阅读。
DI容器
无论使用哪种方法使注册表或服务定位器可全局访问,都必须将对象存储在此,并且在存储之前,必须实例化它们。
在复杂的应用程序中,有很多类并且其中许多类具有多个依赖项,实例化类需要大量代码,因此可能会出现bug:不存在的代码不会包含bug。
近年来,出现了一些PHP库,这些库可帮助PHP开发人员轻松实例化和存储对象实例,从而自动解决其依赖关系。
该库被称为“依赖注入容器”,因为它们能够实例化解决依赖关系的类,并能够存储对象并在需要时返回它们,其作用类似于注册表对象。
通常,当使用DI容器时,开发人员必须为应用程序的每个类设置依赖关系,然后在第一次在代码中需要一个类时,将使用适当的依赖关系实例化该类,并在后续请求中一次又一次返回同一实例。 。
一些DI容器还能够自动发现依赖关系,而无需进行配置,而是使用PHP Reflection。
一些著名的DI容器是:
还有很多其他
我想指出的是,对于仅涉及很少类且类没有太多依赖关系的简单插件,可能不值得使用DI容器:静态实例方法或全局可访问注册表是很好的解决方案,但对于复杂插件DI容器的好处显而易见。
当然,甚至DI容器对象也必须可访问才能在应用程序中使用,并且为此目的,可以使用上面看到的方法之一,全局变量,静态实例变量,通过过滤器返回对象等。
作曲家
使用DI容器通常意味着使用第三方代码。如今,在PHP中,当我们需要使用一个外部库(不仅是DI容器,而且是不属于应用程序的任何代码)时,简单地将其下载并将其放入我们的应用程序文件夹中被认为不是一种好习惯。即使我们是另一段代码的作者。
将应用程序代码与外部依赖项分离是更好的组织,更好的可靠性和更好的代码完整性的标志。
Composer是PHP社区中事实上的标准,用于管理PHP依赖项。远处是主流的WP社区为好,它是一个工具,每个PHP和WordPress开发人员至少应该知道,如果不使用。
该答案的大小已经确定,可以进行进一步的讨论,并且在此处讨论Composer可能不在主题范围之内,仅为了完整性而提及。
欲了解更多信息,请访问网站的作曲家和它也是值得给予一读这个微型网站的策划@Rarst。
1 PSR是PHP Framework Interop Group发布的PHP标准规则
2 Composer(此答案中将提及的库)还包含一个autoloader实用程序。