問題描述
我建立了一個外掛,當然是我,我想用一個漂亮的 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 成員 toscho 在 this 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 functions 和 namespacesü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”,因此以這種方式可以分離責任。
另外,AdminStuff 和 FrontStuff 類使用 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 輔助翻譯的英文資料結果。如果您對結果不滿意,可以加入我們改善翻譯效果:薇曉朵技術論壇。