PHP高級編程-迴歸原生態-數組類和魔術方法



4.3 活用魔術方法

Java有註解和反射,Ruby有代碼生成代碼的元編程,Scala有Monad函子,而PHP有魔術方法。這些都是非常強大的武器,有人喜歡它的強大,但也有人討厭它的複雜以及伴隨而來的難以理解、萬丈深淵。例如Ruby中的猴子補丁,非線性順序的執行經常會讓人摸不着頭腦。


另一方面,如果能夠深入理解PHP的魔法方法,並加以靈活、恰當地使用,你將能節省很多重複性的代碼編寫,具備在陌生環境更頑強的代碼生存能力,還能對某些看似神奇的現象做出合理的解釋。


下面,我們將來一起踏上這片魔法之地。


4.3.1 繼續探討DI容器背後的技巧

前面有說到Phalcon和PhalApi這兩個PHP開源框架的DI容器,也見識了它的數組訪問形式。但它的使用方式不止這一種,還有兩種是和本次要討論的魔法方法有關。我們先來看最終客戶端的使用效果,再反過來追尋它背後的實現和原理。


以PhalApi框架爲例,對於服務資源的註冊和獲取,還可以通過類屬性以及類成員函數來操作。例如:

// 通過類屬性方式操作$di->request = new \PhalApi\Request();var_dump($di->request);// 通過類成員函數方式操作$di->setRequest(new \PhalApi\Request());var_dump($di->getRequest());

這樣是不是很酷?!開發工程師完全可以根據自己的喜愛來選擇操作方式,不用再擔心會忘記如何使用DI容器。那麼這些炫酷的特效是如何實現的呢?


如果查看PhalApi框架中DependenceInjection類的源代碼,是找不到上面這些類屬性和類成員函數的。事實上,它也不可能窮舉全部開發人員會用到哪些資源服務。爲此,只能使用動態的方式來維護。如果細心品讀DependenceInjection類的源代碼,我們可以找到魔法方法的影子,順着這些蛛絲馬跡,我們就能領略魔法方法的美妙之處。


在給不可訪問屬性賦值時,__set() 會被調用。讀取不可訪問屬性的值時,__get() 會被調用。所以,當對$di->request進行賦值時,會觸發DependenceInjection內的__set()方法,對應代碼是:

    public function __set($name, $value) {        $this->set($name, $value);    }

而當通過$di->request獲取不存在的屬性時,會觸發DependenceInjection內的__get() 方法,對應代碼是:

    public function __get($name) {        return $this->get($name, NULL);    }

通常情況,__set()__get() 是配套使用的。

再來看下另外一個魔法方法——__call(),當在對象中調用一個不可訪問的方法時,就會觸發這個魔法方法。例如,執行$di->setRequest()操作時,就會觸發DependenceInjection內的__call()方法,對應代碼是:

    public function __call($name, $arguments) {        if (substr($name, 0, 3) == 'set') {            $key = lcfirst(substr($name, 3));            return $this->set($key, isset($arguments[0]) ? $arguments[0] : NULL);        } else if (substr($name, 0, 3) == 'get') {            $key = lcfirst(substr($name, 3));            return $this->get($key, isset($arguments[0]) ? $arguments[0] : NULL);        }        throw new InternalServerErrorException(            T('Call to undefined method DependenceInjection::{name}() .', array('name' => $name))        );    }

稍微解釋一下,__call()方法的第一個參數是要調用的方法名稱,第二個參數是數組類型,即傳遞過來的參數列表。在這裏,先判斷調用的方法是以set還是以get開頭,然後如果有傳遞參數再將參數列表傳遞下去。最後如果既不是set也不get操作,則拋出異常,告知開發人員存在非法調用。


4.3.2 魔法方法與代碼生成

順便說一下,魔法方法都是以雙下劃線開頭的。此外,引申兩點。先說簡短的,再說稍長的。第一點, 當調用對象中一個不存在的方法時,會觸發__call()魔法方法,那如果嘗試調用的是類的靜態方法,又會觸發哪個魔術方法呢?答案是:__callStatic()。它的參數以及功能,和__call()類似,唯一不同點是名稱以及需要使用static關鍵字,它的函數簽名是:

public static mixed __callStatic ( string $name , array $arguments )


有興趣的同學可以自行實現一個具體的示例,並嘗試對它進行使用。


第二點是,有人擔心過多調用魔法方法會影響性能,因此會禁用魔法方法。但我覺得,既然選擇了PHP這門語言,就不會過多關注相差幾毫秒的性能。事實上,大型系統的性能瓶頸都不在於語言的執行層面,而主要集中於I/O方面,例如文件I/O,網絡I/O,數據庫I/O。但這也給了我們另一個啓發,如果確實需要關注性能,我們也可以對於常見的setter/getter提前生成相應的PHP代碼。例如針對數據傳輸對象DTO,就可以使用這一招。


先來看下,使用魔術方法的實現方式。很簡單,起一個合適的類名,然後重載__call()這個方法即可,非常簡單。

<?phpclass DTO {    public function __call($method, $params) {        if (substr($method, 0, 3) == 'set') {            $key = lcfirst(substr($method, 3));            $this->$key = $params[0];        } else if (substr($method, 0, 3) == 'get') {            $key = lcfirst(substr($method, 3));            return isset($this->$key) ? $this->$key : NULL;        }    }}

出於簡單性,這裏暫時不對異常的情況作過多的預防和處理。同樣,客戶端使用setter/getter也是非常簡單的。例如這樣:

$dto = new DTO();$dto->setName('dogstar');var_dump($dto->getName());

這些都是沒什麼難度的,一旦你熟悉魔法方法後。如果在大型企業系統中,想獲得更多細緻的控制權,也可以爲此提前自動生成setter/getter的代碼。編寫一個代碼生成器,對於初學者來說會有點難度,甚至對於從沒接觸過這塊的同學來說也會有點陌生。但一旦在實際項目中應用過後,你就會發現其實代碼自動生成也是很簡單的,而且應用場景很多。這裏以自動生成setter/getter代碼爲例,先簡單說一下實現的思路,再來介紹代碼生成在各大開源框架中的應用場景。


每個DTO的類代碼,類名是不一樣的,另外各自的類屬性也是不盡相同的。如果我們能手動編寫其中一個DTO的類代碼,就能知道其它DTO的類代碼要如何生成了。快速來寫一個代碼生成器腳本 ,命名爲:generate_dto_class.php,並在內放置以下實現代碼:

<?php// DTO簡易代碼生成器
$class = $argv[1];$properties = array_slice($argv, 2);
$code = "<?phpclass $class {";foreach ($properties as $it) { $itUpper = ucfirst($it);
$code .= " public function set{$itUpper}(\${$it}) { \$this->{$it} = \${$it}; }
public function get{$itUpper}() { return \$this->{$it}; }";}
$code .= "}";
file_put_contents(dirname(__FILE__) . '/' . $class . '.php', $code);
echo "OK!\n";

開發完成後,執行以下命令:

$ php ./generate_dto_class.php Student name age

就可以生成一個Student的DTO類,裏面有兩個類成員屬性,分別是name和age。並且,可以看到在生成的Student.php文件裏有以下自動生成的PHP代碼:

<?phpclass Student {
public function setName($name) { $this->name = $name; }
public function getName() { return $this->name; }
public function setAge($age) { $this->age = $age; }
public function getAge() { return $this->age; }}

是不是很有趣?


在代碼生成這一領域,不同的開源框架有不同的做法。Yii框架提供了Gii,一個強大的基於Web 的代碼生成器,可以生成Model類的代碼,以及CRUD代碼。Symfony框架則可以使用Doctrine組件提供的命令來創建Entity實體類的代碼。例如輸入以下命令並按提示操作:

$ php bin/console make:entity
Class name of the entity to create or update:> Product

最後可以生成類似這樣的代碼:

// src/Entity/Product.phpnamespace App\Entity;use Doctrine\ORM\Mapping as ORM;/** * @ORM\Entity(repositoryClass="App\Repository\ProductRepository") */class Product{    /**     * @ORM\Id     * @ORM\GeneratedValue     * @ORM\Column(type="integer")     */    private $id;
public function getId(){ return $this->id; }
// ... getter and setter methods}

代碼自動生成更多是應用在與數據庫操作相關的層級上,例如DTO、實體Entity、模型Model。在我曾經任職的第一家公司裏,也提供了一個強大的命令,可以根據xml的配置,自動生成相應的整套數據庫相關操作的代碼庫。另一方面,在其他場景也可以發現代碼生成的身影。例如,在PhalApi框架中,提供了phalapi-buildtest命令,可自動生成測試代碼。


如果想提升自己的開發效率,提升整個項目的交付速度,魔術方法或者代碼生成,都是值得推薦的策略。前者可以節省編寫重複的代碼,後者則可以直接幫你生成重複的代碼。何樂而不爲?


本文分享自微信公衆號 - 小白開放平臺(yesapi)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章