問題描述

我創建了一個插件,當然是我,我想用一個漂亮的 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 輔助翻譯的英文資料結果。如果您對結果不滿意,可以加入我們改善翻譯效果:薇曉朵技術論壇。