问题描述

我创建了一个插件,当然是我,我想用一个漂亮的 OO 方法。现在我一直在做的是创建这个类,然后在下面创建一个这个类的实例:

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();
}

最佳解决方案

好的问题,有很多方法,它取决于你想要实现的。

我经常做

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 成员 toschothis gist 中可以看到一个更深入的一个深入的例子,这个例子是由聊天室内最近对这个话题进行的一些讨论的结果。

空构造方法。

这是从上述要点中获取的优点/缺点的摘录,其中充分说明了空构造方法。

  • Advantages:

    • Unit tests can create new instances without activating any hooks
      automatically. No Singleton.

    • No global variable needed.

    • Whoever wants to work with the plugin instance can just call
      T5_Plugin_Class_Demo::get_instance().

    • Easy to deactivate.

    • Still real OOP: no working methods are static.

  • Disadvantage:

    • Maybe harder to read?

在我看来,不利之处在于弱点,因此,这将是我最喜欢的方法,但不是我唯一使用的方法。事实上,其他几个重量无疑就会在这个话题上引起争议,因为这个话题有一些很好的意见,应该表达出来。


注意:我需要从 toscho 中找到一个重要的例子,该例子通过 3 或 4 个比较如何在一个插件中实例化一个类,在每一个优点和缺点之间进行比较,上述链接是有利的方法,但是其他例子与这个话题形成了很好的对比。希望 toscho 仍然有文件。

注意:本主题的 WPSE Answer 与相关示例和比较。也是 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!
    }
}

次佳解决方案

在原始问题提出两年之后到达这里,我想指出一些事情。 (不要让我指出很多事情,永远) 。

正确钩

要实例化一个插件类,应该使用适当的钩子。它不是一般规则,因为它取决于班级的作用。

使用非常早的钩子像"plugins_loaded"通常没有任何意义,因为这样的钩子被管理,前端和 AJAX 请求被触发,但是经常以后的钩子是更好的,因为它允许仅在需要时实例化插件类。

例如。可以在"template_redirect"上实例化用于模板的类。

一般来说,在"wp_loaded"被触发之前,需要实例化一个类是非常罕见的。

没有上帝阶级

大多数用作老答案中的示例的类使用一个名为"Prefix_Example_Plugin""My_Plugin"的类… 这表示插件可能有一个主类。

那么,除非插件是由一个单独的类 (在这种情况下命名它的插件名称是绝对合理的),创建一个管理整个插件的类 (例如,添加一个插件需要的所有钩子或实例化所有其他插件类) 可以被认为是不好的做法,作为 god object 的一个例子。

在面向对象的编程代码中应该倾向于 S.O.L.I.D.,其中”S” 代表“Single responsibility principle”

这意味着每个班级都应该做一件事情。在 WordPress 插件开发中,这意味着开发人员应该避免使用单个钩子来实例化一个主要的插件类,但根据课程负责,应该使用不同的钩子实例化不同的类。

避免构造函数中的钩子

这个论据已经在其他答案中介绍了,但是我想在这个概念中加上链接 this other answer,在单元测试的范围内已被很广泛的解释。

几乎 2015 年:PHP 5.2 是为僵尸

从 2014 年 8 月 14 日起,PHP 5.3 reached its end of life 。这绝对是死的 PHP 5.4 将在 2015 年全面支持,这意味着我正在写作的另一年。

然而,WordPress 仍然支持 PHP 5.2,但是没有人应该编写支持该版本的单行代码,特别是如果代码是 OOP 。

有不同的原因:

  • PHP 5.2 很久以前死了,没有安全解决方案被释放,这意味着它不安全
  • PHP 5.3 向 PHP 添加了很多功能,anonymous functionsnamespacesüberalles
  • PHP 的新版本要快很多。 PHP 是免费的更新它是免费的。为什么使用较慢,不安全的版本,如果您可以免费使用更快,更安全的版本?

如果您不想使用 PHP 5.4+代码,请至少使用 5.3+

例子

在这一点上,现在是根据我在这里所说的内容来回顾旧的答案的时候了。

一旦我们不再需要关心 5.2,我们可以而且应该使用命名空间。

为了更好地解释单一责任原则,我的例子将使用 3 个类,一个在前端做一些,一个在后端,另外一个在这两种情况下使用。

管理类:

namespace GMWPSEExample;

class AdminStuff {

   private $tools;

   function __construct( ToolsInterface $tools ) {
     $this->tools = $tools;
   }

   function setup() {
      // setup class, maybe add hooks
   }

}

前端类:

namespace GMWPSEExample;

class FrontStuff {

   private $tools;

   function __construct( ToolsInterface $tools ) {
     $this->tools = $tools;
   }

   function setup() {
      // setup class, maybe add hooks
   }

}

工具界面:

namespace GMWPSEExample;

interface ToolsInterface {

   function doSomething();

}

和一个 Tools 类,由其他两个使用:

namespace GMWPSEExample;

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 GMWPSEExampleTools;
   global $admin_stuff; // this is not ideal, reason is explained below
   $admin_stuff = new GMWPSEExampleAdminStuff( $tools );
} );

add_action( 'template_redirect', function() {

   require_once plugin_dir_path( __FILE__ ) . 'src/FrontStuff.php';
   $tools = new GMWPSEExampleTools;
   global $front_stuff; // this is not ideal, reason is explained below
   $front_stuff = new GMWPSEExampleFrontStuff( $tools );
} );

依赖性反转依赖注入

在上面的例子中,我使用命名空间和匿名函数在不同的钩子上实例化不同的类,实际上我在上面说过。

注意命名空间允许创建没有任何前缀的类。

我应用了另一个间接提到的概念:依赖注入,它是一种应用 Dependency Inversion Principle,”D” 在 SOLID 首字母缩略词中的方法。

Tools 类在其他两个类中被实例化时是”injected”,因此以这种方式可以分离责任。

另外,AdminStuffFrontStuff 类使用 type hinting 声明它们需要一个实现 ToolsInterface 的类。

以这种方式,我们自己或使用我们的代码的用户可能会使用同一个接口的不同实现,使得我们的代码没有耦合到一个具体的类,而是一个抽象:正是依赖关系反转原则。

然而,可以进一步改进上述示例。我们来看看怎么样

Autoloader

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

这个规则也是 PSR-1 coding standards 1 之一。

然而,这样做之前,在能够使用类之前需要需要包含它的文件。

这可能是压倒性的,但 PHP 提供了 utility functions 来在需要时自动加载类,使用基于其名称加载文件的回调。

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

这不仅是可能的,但它也是另一个 PSR 标准 (或更好的 2:PSR-0 现在已经不推荐使用,和 PSR-4) 。

遵循该标准,可以使用处理自动加载的不同工具,而无需编写自定义自动加载程序。

我不得不说 WordPress coding standards 对命名文件有不同的规则。

所以当编写 WordPress 核心的代码时,开发人员必须遵循 WP 规则,但是在编写自定义代码时,它是开发人员的选择,但使用 PSR 标准更容易使用已经编写的工具 2 。

全球访问,注册表和服务定位器模式。

在 WordPress 中实例化插件类的最大问题之一是如何从代码的各个部分访问它们。

WordPress 本身使用全局方法:变量保存在全局范围内,使得它们随处可见。每个 WP 开发人员在他们的职业生涯中,都会打出数千次的 global 这个词。

这也是我用于上述示例的方法,但它是邪恶的。

这个答案已经太久了,不能让我进一步解释为什么,但是阅读“global variables evil” 的 SERP 的第一个结果是一个很好的起点。

但是如何避免全局变量呢?

有不同的方法

这里的一些旧答案使用静态实例方法。

public static function instance() {

  if ( is_null( self::$instance ) ) {
    self::$instance = new self;
  }

  return self::$instance;
}

这很简单而且很好,但它强制实现我们想要访问的每个类的模式。

此外,这种方法很多时候会落在神级问题中,因为开发人员可以使用这种方法访问一个主类,然后使用它来访问所有其他类。

我已经解释了上帝类是多么糟糕,所以静态实例方法是一个很好的方法,当一个插件只需要使一到两个类可访问。

这并不意味着它只能用于只有几个类的插件,实际上,当依赖注入原则被正确使用时,可以创建相当复杂的应用程序,而不需要使全局可访问的数量大的对象。

然而,有时插件需要使一些类可访问,在这种情况下,静态实例方法是压倒性的。

另一种可能的方法是使用 registry pattern

这是一个非常简单的实现:

namespace GMWPSEExample;

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 GMWPSEExampleTools;
  $registry->add( 'tools', $tools );
}

if ( is_null( $registry->get( 'front' ) ) ) {
  $front_stuff = new GMWPSEExampleFrontStuff( $registry->get( 'tools' ) );
  $registry->add( 'front', front_stuff );
}

add_action( 'wp', array( $registry->get( 'front' ), 'wp' ) );

上面的例子清楚地表明,注册表需要在全球范围内有用。唯一注册表的全局变量不是很糟糕,但是对于 non-global 纯粹主义者,可以为注册表实现静态实例方法,也可以使用静态变量的函数:

function gm_wpse_example_registry() {
  static $registry = NULL;
  if ( is_null( $registry ) ) {
    $registry = new GMWPSEExampleRegistry;
  }
  return $registry;
}

第一次调用该函数时会实例化注册表,在后续的调用中它将返回它。

另一个使全局可访问的类的 WordPress-specific 方法是从过滤器返回一个对象实例。这样的事情

$registry = new GMWPSEExampleRegistry;

add_filter( 'gm_wpse_example_registry', function() use( $registry ) {
  return $registry;
} );

之后,需要注册表:

$registry = apply_filters( 'gm_wpse_example_registry', NULL );

可以使用的另一种模式是 service locator pattern 。它类似于注册表模式,但服务定位器使用依赖注入传递到各种类。

这种模式的主要问题是它隐藏类依赖性,使得代码更难维护和读取。

DI 容器

无论用于使注册表或服务定位器全局访问的方法,对象必须存储在那里,并且在存储之前需要进行实例化。

在复杂的应用程序中,有很多类,其中很多类有几个依赖关系,实例化类需要大量的代码,所以 bug 的可能性增加:不存在的代码不能有错误。

在过去的几年中,出现了一些 PHP 库,可帮助 PHP 开发人员轻松实例化和存储对象的实例,并自动解析它们的依赖关系。

这些库被称为依赖注入容器,因为它们能够实例化解析依赖关系的类,并且还可以存储对象并在需要时返回它们,与注册表对象类似。

通常,当使用 DI 容器时,开发人员必须为每个应用程序类设置依赖关系,然后在代码中第一次需要使用适当的依赖关系来实例化一个类,并且在后续请求中一次又一次返回相同的实例。

一些 DI 容器也能够自动发现依赖关系而无需配置,但使用 PHP reflection

一些众所周知的 DI 容器是:

和许多其他。

我想指出,对于简单的插件,只涉及到几个类和类没有很多依赖关系,可能不值得使用 DI 容器:静态实例方法或全局可访问的注册表是很好的解决方案,但对于复杂的插件 DI 容器的好处变得明显。

当然,即使 DI 容器对象也必须可以在应用程序中使用,为此,可以使用上述方法之一,全局变量,静态实例变量,通过过滤器返回对象等。

Composer

使用 DI 容器通常意味着使用第三方代码。现在,在 PHP 中,当我们需要使用一个外部的 lib(所以不仅仅是 DI 容器,而是任何不是应用程序一部分的代码),只需将它下载并将其放在我们的应用程序文件夹中就不是一个好习惯。即使我们是另一段代码的作者。

应用程序代码与外部依赖关系脱钩是组织更好,可靠性更好,代码更合理的标志。

 Composer 是 PHP 社区中用于管理 PHP 依赖关系的 de-facto 标准。远远成为 WP 社区的主流,它是一个工具,每个 PHP 和 WordPress 开发人员至少应该知道,如果不使用。

这个答案已经是 book-sized,允许进一步的讨论,而且在这里讨论的作曲者可能是脱离主题,仅为了完整而提及。

有关更多信息,请访问作曲家网站,并且还值得一读 @Rarst 策划的 minisite


1 PSR 是由 PHP Framework Interop Group 发布的 PHP 标准规则

2 Composer(这个答案中提到的 Library ) 等等还包含一个自动加载器实用程序。

第三种解决方案

我使用以下结构:

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(大部分时间)

免责声明我没有使用单元测试 (在板上有这么多东西),我听说静态可能不太适合他们。如果您需要对其进行单元测试,请对此进行研究。

第四种方案

这一切都取决于功能。

当我调用构造函数时,我曾经做过一个注册脚本的插件,所以我不得不把它挂在 wp_enqueue_scripts 钩子上。

如果要在加载 functions.php 文件时调用它,那么您也可以自己创建 $class_instance = new ClassName(); 实例。

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

参考文献

注:本文内容整合自 Google/Baidu/Bing 辅助翻译的英文资料结果。如果您对结果不满意,可以加入我们改善翻译效果:薇晓朵技术论坛。