在WP插件中启动类的最佳方法?


88

我已经创建了一个插件,并且当然是我自己,我想采用一种不错的面向对象方法。现在,我一直在做的事情是创建此类,然后在下面创建此类的实例:

class ClassName {

    public function __construct(){

    }
}

$class_instance = new ClassName();  

我以一种更多的WP方式来启动该类,然后我碰到人们说他们更喜欢拥有一种init()功能而不是一种功能__construct()。同样,我发现一些人在使用以下钩子:

class ClassName {

    public function init(){

    }
}
add_action( 'load-plugins.php', array( 'ClassName', 'init' ) );

通常认为在负载下创建WP类实例并将其作为全局可访问变量的最佳方法是什么?

注意:作为一个有趣的观点,我注意到虽然register_activation_hook()可以从中调用__construct,但是不能init()使用第二个示例来调用。也许有人可以启发我这一点。

编辑:感谢所有的答案,关于如何在类本身中处理初始化问题,显然存在很多争论,但是我认为通常存在相当不错的共识,这add_action( 'plugins_loaded', ...);是真正开始初始化的最好方法...

编辑:只是使事情变得混乱,我也看到了这个用法(尽管我自己不会使用此方法,因为将一个很好的OO类转换为一个函数似乎违反了它的观点):

// Start up this plugin
add_action( 'init', 'ClassName' );
function ClassName() {
    global $class_name;
    $class_name = new ClassName();
}

1
关于上一次编辑,如果该编辑与该类包含在同一个插件文件中,它将变得毫无用处。您也可以按照我描述的方法实例化该类。即使将其放在单独的文件中,它也仍然毫无意义。我可以看到的唯一用例是,是否要创建包装函数,使您可以在插件文件之外,主题内等实例化类。即便如此,我还是要问这背后的逻辑是什么,因为正确使用条件和钩子应该可以对实例进行精细控制,从而使您可以专注于使用插件。
亚当

我有点同意,但是我认为值得一提,因为我在几个WP插件中找到了它。
kalpaitch 2012年

Answers:


60

好的问题,有很多方法,这取决于您要实现的目标。

我经常

add_action( 'plugins_loaded', array( 'someClassy', 'init' ));

class someClassy {

    public static function init() {
        $class = __CLASS__;
        new $class;
    }

    public function __construct() {
           //construct what you see fit here...
    }

    //etc...
}

WPSE成员toscho这个要点中可以看到一个更彻底的深入示例,该示例是由于最近在聊天室中对此主题进行的一些讨论而产生的。

空的构造方法。

这是摘自上述要点的优点/缺点,它完整地例示了空构造函数方法。

  • 好处:

    • 单元测试可以创建新实例,而无需自动激活任何挂钩。没有单身汉。

    • 不需要全局变量。

    • 任何想使用插件实例的人都可以调用T5_Plugin_Class_Demo :: get_instance()。

    • 易于停用。

    • 仍然是真实的面向对象操作:没有任何工作方法是静态的。

  • 坏处:

    • 也许更难阅读?

我认为缺点是缺点,这就是为什么它必须成为我的首选方法,而不是我使用的唯一方法的原因。实际上,其他几个重量级人物无疑很快就会对此主题产生兴趣,因为围绕该主题有一些好的意见值得提出。


注意:我需要从toscho中找到要点示例,该示例通过3或4个比较实例化了如何在一个插件中实例化一个类的实例,该类考察了每个实例的优缺点,上面的链接是这样做的首选方法,但是其他示例与此主题形成了很好的对比。希望toscho仍然有该文件。

注:WPSE回答这个话题相关的例子和比较。也是最好的解决方案,例如WordPress中的类。

add_shortcode( 'baztag', array( My_Plugin::get_instance(), 'foo' ) );
class My_Plugin {

    private $var = 'foo';

    protected static $instance = NULL;

    public static function get_instance() {

        // create an object
        NULL === self::$instance and self::$instance = new self;

        return self::$instance; // return the object
    }

    public function foo() {

        return $this->var; // never echo or print in a shortcode!
    }
}

add_action('plugins_loaded',...)之间有什么区别?和add_action('load-plugins.php',...); 这个例子我拿了使用后者
kalpaitch

1
据我了解,load-plugins.php虽然有效,但与核心update.php文件相关联,并且不是初始化时触发的事件序列时应依赖的通常默认操作的一部分,因此我更喜欢在这种情况下,使用那些确实适用的钩子plugins_loaded。这就是我通常所说的“动作参考”发生时的快速快照。我的解释还不完整。
亚当(

4
我喜欢这种单例方法。但是,我质疑使用plugins_loaded作为您的初始化动作挂钩。该挂钩应所有插件加载运行。通过钩住它,您就可以劫持该钩子,并且可能会陷入与其他插入到plugins_loaded的插件或主题的冲突或启动顺序问题。我不会执行任何操作来运行您的初始化方法。插件体系结构设计为内联运行,而不是在操作上运行。
Tom Auger 2012年

2
请注意,如果使用,则register_activation_hook()需要plugins_loaded在触发操作之前调用该函数。
盖尔特(Geert)

1
作为其他信息,请参阅来自@mikeschinkel的帖子和评论中的dicus。hardcorewp.com/2012/…–
bueltge

77

到达这里正好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当实例化其他两个类时,该类将“注入”,因此可以分离责任。

此外,AdminStuffFrontStuff类使用类型提示来声明它们需要实现的类ToolsInterface

通过这种方式,我们自己或使用我们代码的用户可以使用同一接口的不同实现,从而使我们的代码不耦合到具体的类,而是耦合到抽象:这正是依赖关系反转原理的含义。

但是,可以进一步改善上述示例。让我们看看如何。

自动装带器

编写具有更好可读性的OOP代码的一种好方法是不要类型(接口,类)定义与其他代码混合,也不要将每种类型放入其自己的文件中。

该规则也是PSR-1编码标准1之一

但是,这样做之前,需要一个包含类的文件,然后才能使用它。

这可能会让人不知所措,但是PHP提供了实用程序功能,可以根据需要使用根据名称加载文件的回调自动加载类。

使用名称空间变得非常容易,因为现在可以将文件夹结构与名称空间结构进行匹配。

这不仅可行,而且是另一个PSR标准(或更好的2:现在已弃用PSR-0PSR-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实用程序。


1
另外,PHP 5.3也即将终止。如果不是默认的话,负责任的房东将至少提供5.4作为选项
Tom J Nowell

2
“自2014年8月14日起,PHP 5.3寿终正寝。它肯定已经死了。” “ PHP 5.2适用于僵尸”下的第一行@TomJNowell
gmazzap

3
只是想知道,您会指出“很多东西”是什么样子?;-)
MikeSchinkel 2015年

为了避免全局变量,我喜欢带有功能和静态变量的注册表模式。第一次调用该函数将实例化注册表,在随后的调用中它将仅返回它。
Michael Ecklund '17

10

我使用以下结构:

Prefix_Example_Plugin::on_load();

/**
 * Example of initial class-based plugin load.
 */
class Prefix_Example_Plugin {

    /**
     * Hooks init (nothing else) and calls things that need to run right away.
     */
    static function on_load() {

        // if needed kill switch goes here (if disable constant defined then return)

        add_action( 'init', array( __CLASS__, 'init' ) );
    }

    /**
     * Further hooks setup, loading files, etc.
     *
     * Note that for hooked methods name equals hook (when possible).
     */
    static function init(  ) {


    }
}

笔记:

  • 已经为需要立即运行的事物定义了位置
  • 禁用/覆盖调整很容易(解开一种init方法)
  • 我认为我从未使用过/不需要插件类的对象-需要跟踪它,等等;这实际上是假冒的目的命名空间,而不是OOP(大多数情况下)

免责声明我还没有使用单元测试(在myplate上有很多东西),而且我听说static对于它们而言可能不太理想。如果需要进行单元测试,请对此进行研究。


3
我知道从事单元测试的人们真的不喜欢静态/单例解决方案。我认为,如果您完全了解要使用静态方法实现的目标,并且至少了解这样做的后果,那么实现此类方法就非常合适。在围绕这一过好主题堆栈溢出
亚当

这让我真正思考。那么,为什么不使用类,而不是返回简单的带前缀的函数呢?我们这样做仅仅是为了获得更简洁的功能/方法名称吗?我的意思是让它们与“静态” b4嵌套在一起是否更具可读性?如果您使用适当的前缀,或者我遗漏了某些东西,则名称冲突的可能性与单个类名称的可能性差不多。
詹姆斯·米奇

1
@JamesMitch是的,全静态方法通常只是WP中使用带有假名称空间的函数。但是,即使在这种情况下,类也比纯函数具有一些优势,例如自动加载和继承。最近,我已经从静态方法转向由依赖项注入容器组织的实际实例化对象。
2013年

3

这一切都取决于功能。

我曾经做过一个插件,该插件在调用构造函数时会注册脚本,因此我不得不将其挂在wp_enqueue_scripts钩子上。

如果要在functions.php加载文件时调用它,则最好自己创建一个实例$class_instance = new ClassName();

您可能需要考虑速度和内存使用情况。我什么都不知道,但是我可以想象在某些情况下有未使用的钩子。通过在该挂钩上创建实例,您可以节省一些服务器资源。


非常感谢,我想上述问题也有两点。另一个是__construct是否合适还是init()是否是初始化类的更好方法。
kalpaitch 2012年

1
好吧,我会去找一个静态init()方法,以便在类的作用域而不是在另一个可能覆盖现有变量的作用域内调用类实例。
Tim S.

0

我知道这已经有几年历史了,但是同时php 5.3支持匿名方法,所以我想到了:

add_action( 'plugins_loaded', function() { new My_Plugin(); } );

而且我最喜欢它。我可以使用常规的构造函数,而无需定义任何会弄乱我的OOP结构的“ init”或“ on_load”方法。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.