在可湿性粉剂插件中启动一门课程的最佳方式?

时间:2012-10-22 作者:kalpaitch

我已经创建了一个插件,当然,作为我自己,我想采用一种很好的OO方法。现在我所做的是创建这个类,然后在下面创建这个类的一个实例:

class ClassName {

    public function __construct(){

    }
}

$class_instance = new ClassName();  
我假设有更多的WP方式来启动这个类,然后我遇到一些人说他们更喜欢init() 功能比a__construct() 一同样,我发现一些人在使用以下钩子:

class ClassName {

    public function init(){

    }
}
add_action( \'load-plugins.php\', array( \'ClassName\', \'init\' ) );
通常认为,在加载时创建WP类实例并将其作为全局可访问变量的最佳方法是什么?

NOTE: 有趣的是,我注意到register_activation_hook() 可以从中调用__construct, 无法从内部调用init() 使用第二个示例。也许有人能在这一点上启发我。

Edit: 感谢所有的答案,对于如何在类本身中处理初始化,显然存在着相当多的争论,但我认为通常有一个相当好的共识add_action( \'plugins_loaded\', ...); 是启动它的最佳方式。。。

Edit: 为了混淆问题,我也看到了这种方法的使用(虽然我自己不会使用这种方法,因为将一个很好的OO类转换为函数似乎无法解决问题):

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

5 个回复
最合适的回答,由SO网友:Adam 整理而成

好问题,有很多方法,这取决于你想要实现什么。

我经常这样做;

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...
}
最近在聊天室中就这一主题进行了一些讨论,得出了一个更深入的例子,如this gist WPSE成员toscho.

The empty constructor approach.

这里摘录了上述要点的优点/缺点,充分说明了空构造函数方法。

优势:

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

无需全局变量。

任何想使用插件实例的人都可以调用T5\\u plugin\\u Class\\u Demo::get\\u instance()。

易于停用。

仍然是真正的OOP:没有任何工作方法是静态的。

缺点:

可能更难读

注:我需要从toscho 这通过3到4次比较来说明如何在插件中实例化一个类,并查看每个类的优缺点,上面的链接是最常用的方法,但其他示例与此主题形成了很好的对比。有希望地toscho 这件事还在档案中

Note: 这个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!
    }
}

SO网友:gmazzap

Arriving here exactly 2 years after the original question was asked, there are a few things I want to point out. (Don\'t ask me to point out a lot of things, ever).

Proper hook

To instantiate a plugin class, the proper hook should be used. There isn\'t a general rule for which it is, because it depends on what the class does.

Using a very early hook like "plugins_loaded" often make no sense because an hook like that is fired for admin, frontend and AJAX requests, but very often a later hook is far better because it allows to instantiate plugin classes only when needed.

E.g. a class that does stuff for templates can be instantiated on "template_redirect".

Generally speaking it is very rare that a class needs to be instantiated before "wp_loaded" has been fired.

No God Class

Most of all the classes used as examples in older answers use a class named like "Prefix_Example_Plugin" or "My_Plugin"... This indicates that there probably is a main class for the plugin.

Well, unless a plugin is made by one single class (in which case naming it after plugin name is absolutely reasonable), to create a class that manages the entire plugin (e.g. adding all the hooks a plugin needs or instantiating all the other plugin classes) can be considered a bad practice, as an example of a god object.

In object oriented programming code should tend to be S.O.L.I.D. where the "S" stand for "Single responsibility principle".

It means that every class should do a single thing. In WordPress plugin development it means that developers should avoid to use a single hook to instantiate a main plugin class, but different hooks should be used to instantiate different classes, according to the class responsibility.

Avoid hooks in constructor

This argument has been introduced in other answers here, however I want to remark this concept and link this other answer where it has been pretty widely explained in the purview of unit testing.

Almost 2015: PHP 5.2 is for zombies

Since 14 August 2014, PHP 5.3 reached its end of life. It\'s definitely dead. PHP 5.4 is going to be supported for all 2015, it means for another year at the moment I\'m writing.

However, WordPress still supports PHP 5.2, but no one should write a single line of code that support that version, especially if code is OOP.

There are different reasons:

  • PHP 5.2 dead a long time ago, no security fixes are released for it, that means it isn\'t secure
  • PHP 5.3 added a lot of features to PHP, anonymous functions and namespaces über alles
  • newer versions of PHP are a lot faster. PHP is free. Updating it is free. Why use a slower, insecure version if you can use a faster, more secure one for free?

If you don\'t want to use PHP 5.4+ code, use at least 5.3+

Example

At this point it is time to review older answers based on what I said until here.

Once we don\'t have to care about 5.2 anymore, we can and should, use namespaces.

For sake of better explain the single responsibility principle, my example will use 3 classes, one that does something on the frontend, one on the backend and a third used in both cases.

Admin class:

namespace GM\\WPSE\\Example;

class AdminStuff {

   private $tools;

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

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

}

Frontend class:

namespace GM\\WPSE\\Example;

class FrontStuff {

   private $tools;

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

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

}

Tools interface:

namespace GM\\WPSE\\Example;

interface ToolsInterface {

   function doSomething();

}

And a Tools class, used by the other two:

namespace GM\\WPSE\\Example;

class Tools implements ToolsInterface {

   function doSomething() {
      return \'done\';
   }

}

Having this classes, I can instantiate them using proper hooks. Something like:

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

Dependency Inversion & Dependency Injection

In the example above I used namespaces and anonymous functions to instantiate different classes at different hooks, putting in practice what I said above.

Note how namespaces allow to create classes named without any prefix.

I applied another concept that was indirectly mentioned above: Dependency Injection, it is one method to apply Dependency Inversion Principle, the "D" in SOLID acronym.

The Tools class is "injected" in the other two classes when they are instantiated, so in this way it is possible to separate responsibility.

In addition, AdminStuff and FrontStuff classes use type hinting to declare they need a class that implements ToolsInterface.

In this way ourselves or users that use our code may use different implementations of the same interface, making our code not coupled to a concrete class but to an abstraction: that\'s exactly what Dependency Inversion Principle is about.

However, the example above can be further improved. Let\'s see how.

Autoloader

A good way to write better readable OOP code is not to mix types (Interfaces, Classes) definition with other code, and to put every type in its own file.

This rule is also one of the PSR-1 coding standards1.

However, doing so, before being able to use a class one needs to require the file that contains it.

This can be overwhelming, but PHP provides utility functions to auto load a class when it is required, using a callback that loads a file based on its name.

Using namespaces it becomes very easy, because now it is possible to match the folder structure with the namespace structure.

That\'s not only possible, but it is also another PSR standard (or better 2: PSR-0 now deprecated, and PSR-4).

Following that standards it is possible to make use of different tools that handle autoload, without having to code a custom autoloader.

I have to say that WordPress coding standards have different rules for naming files.

So when writing code for WordPress core, developers have to follow WP rules, but when writing custom code it\'s a developer choice, but using PSR standard is easier to use already written tools2.

Global Access, Registry and Service Locator Patterns.

One of the biggest issues when instantiating plugin classes in WordPress, is how to access them from various parts of the code.

WordPress itself uses the global approach: variables are saved in global scope, making them accessible everywhere. Every WP developer types the word global thousands of times in their career.

This is also the approach I used for the example above, but it is evil.

This answer is already much too long to allow me to further explain why, but reading the first results in the SERP for "global variables evil" is a good starting point.

But how is it possible to avoid global variables?

There are different ways.

Some of the older answers here use the static instance approach.

public static function instance() {

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

  return self::$instance;
}

It\'s easy and pretty fine, but it forces to implement the pattern for every class we want to access.

Moreover, a lot of times this approach puts on the way to fall in the god class issue, because developers make accessible a main class using this method, and then use it to access all other classes.

I already explained how bad a god class is, so the static instance approach is a good way to go when a plugin only needs to make accessible one or two classes.

This doesn\'t mean that it can be used only for plugins having just a couple of classes, in fact, when the dependency injection principle is used properly, it is possible to create pretty complex applications without the need to make globally accessible a large number of objects.

However, sometimes plugins need to make accessible some classes, and in that case the static instance approach is overwhelming.

Another possible approach is to use the registry pattern.

This is a very simple implementation of it:

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

}

Using this class it is possible to store objects in the registry object by an id, so having access to a registry it\'s possible to have access to all the objects. Of course when an object is created for the first time it needs to be added to the registry.

Example:

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\' ) );

The example above makes clear that to be useful the registry needs to be globally accessible. A global variable for the sole registry is not very bad, however for non-global purists it is possible to implement the static instance approach for a registry, or maybe a function with a static variable:

function gm_wpse_example_registry() {
  static $registry = NULL;
  if ( is_null( $registry ) ) {
    $registry = new GM\\WPSE\\Example\\Registry;
  }
  return $registry;
}

The first time the function is called it will instantiate the registry, on subsequent calls it will just return it.

Another WordPress-specific method to make a class globally accessible is returning an object instance from a filter. Something like this:

$registry = new GM\\WPSE\\Example\\Registry;

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

After that everywhere the registry is needed:

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

Another pattern that can be used is the service locator pattern. It\'s similar to the registry pattern, but service locators are passed to various classes using dependency injection.

Main problem with this pattern is that it hides classes dependencies making code harder to maintain and read.

DI Containers

No matter the method used to make registry or service locator globally accessible, objects have to be stored there, and before to be stored they need to be instantiated.

In complex applications, where there are quite a lot classes and many of them have several dependencies, instantiating classes requires a lot of code, so the possibility of bugs increases: code that doesn\'t exist can\'t have bugs.

In last years there appeared some PHP libraries that help PHP developers to easily instantiate and store instances of objects, automatically resolving their dependencies.

This libraries are known as Dependency Injection Containers because they are capable of instantiating classes resolving dependencies and also to store objects and return them when needed, acting similarly to a registry object.

Usually, when using DI containers, developers have to setup the dependencies for every class of the application, and then the first time a class is needed in the code it is instantiated with proper dependencies and the same instance is returned again and again on subsequent requests.

Some DI containers are also capable to automatically discover dependencies without configuration, but using PHP reflection.

Some well known DI Containers are:

and many others.

I want to point out that for simple plugins, that involve only few classes and classes have not many dependencies, probably it doesn\'t worth to use DI containers: the static instance method or a global accessible registry are good solutions, but for complex plugins the benefit of a DI container becomes evident.

Of course, even DI container objects have to be accessible to be used in the application and for that purpose it is possible to use one of the methods seen above, global variable, static instance variable, returning object via filter and so on.

Composer

To use DI container often means using 3rd party code. Nowadays, in PHP, when we need to use an external lib (so not only DI containers, but any code that isn\'t part of the application), simply downloading it and putting it in our application folder is not considered a good practice. Even if we are the authors of that other piece of code.

Decoupling an application code from external dependencies is sign of better organization, better reliability and better sanity of the code.

Composer, is the de-facto standard in PHP community to manage PHP dependencies. Far away to be mainstream in WP community as well, it\'s a tool that every PHP and WordPress developer should at least know, if not use.

This answer is already book-sized to allow further discussion, and also discussing Composer here is probably off topic, it was only mentioned for sake of completeness.

For more information visit the Composer site and it\'s also worth giving a read to this minisite curated by @Rarst.


1 PSR are PHP standards rules released by the PHP Framework Interop Group

2 Composer (a library that will be mentioned in this answer) among other things also contains an autoloader utility.

SO网友:Rarst

我使用以下结构:

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(大多数情况下)
  • Disclaimer 我还没有使用单元测试(myplate上有这么多东西),我听说静态测试对它们来说可能不太合适。如果需要进行单元测试,请对此进行研究。

    SO网友:Tim S.

    这完全取决于功能。

    我曾经制作了一个插件,在调用构造函数时注册脚本,所以我必须在wp_enqueue_scripts

    如果你想在functions.php 文件已加载,您也可以自己创建一个实例$class_instance = new ClassName(); 正如你所说。

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

    SO网友:Basti

    我知道这有几年了,但与此同时php 5.3 supports anonymous methods, 所以我想到了这个:

    add_action( \'plugins_loaded\', function() { new My_Plugin(); } );
    
    不知何故,我最喜欢它。我可以使用常规构造函数,并且不需要定义任何会打乱OOP结构的“init”或“on\\u load”方法。

    结束