PHP Trait
trait的設計目的是解決在單線物件繼承的限制下,讓程式碼可以重複使用,並且降低複雜度。
例如:RetailStore 和 Car 兩個非常不同的 Classes,從分類上不太會有共同的 Parent Class。但是如果 RetailStore 和 Car 都要使用到 GeoCodeable 時,這問題要如何處理?
- Solution 1 (bad - class): RetailStore 和 Car 都繼承 GeoCodeable Class.
- Solution 2 (fair - interface): RetailStore 和 Car 都實踐 GeoCodeable Interface.但是,程式碼就會重複出現在兩個地方。
- Solution 3 (good - trait): RetailStore 和 Car 都使用 GeoCodeable Traits.這樣子程式碼就不會重複,而相關的 GeoCodeable 程式邏輯也會包含在這裡面。
Trait Example
下面是 GeoCodeable Trait的程式碼:
trait GeoCodeable
{
private $address;
private $geoCoder;
public function setAddress($address)
{
$this->address = $address;
}
public function setGeoCoder($geoCoder)
{
$this->geoCoder = $geoCoder;
}
public function getLatitude()
{
return $this->geoCoder->geocode($this->address)->first()->getLatitude();
}
public function getLongitude()
{
return $this->geoCoder->geocode($this->address)->first()->getLongitude();
}
}
這裡我用到了DI的方式,讓geoCoder從外面給予。這裡推薦使用geocoder-php,因此在composer.json
必須加入:
"require": {
"willdurand/geocoder": "^3.3"
},
使用 Trait
在RetailStore Class宣告中,必須使用 use 這關鍵字,這樣就可使用到 trait物件中所定義的 properties 和 methods.
class RetailStore
{
use GeoCodeable;
}
執行相關的Geocodeable的代碼如下:
$curl = new \Ivory\HttpAdapter\CurlHttpAdapter();
$geocoder = new \Geocoder\Provider\GoogleMaps($curl);
$store = new RetailStore();
$store->setAddress('Taipei 101');
$store->setGeocoder($geocoder);
$latitude = $store->getLatitude();
$longitude = $store->getLongitude();
echo 'Taipei 101: ', $latitude, ':', $longitude, "\n";
對於使用RetailStore的物件來說,立即可以延伸運用到 getLatitude
和 getLongitude
的方法。執行的結果如下:
使用Trait的注意事項
如果Trait出現相同的 functions 名稱
這時候我們必須用 insteadof
或是 as
指定使用哪一個trait的 functions,不然會得到fatal errors。
如下面的例子:
<?php
trait A
{
public function smallTalk()
{
echo 'a';
}
public function bigTalk()
{
echo 'A';
}
}
trait B
{
public function smallTalk()
{
echo 'b';
}
public function bigTalk()
{
echo 'B';
}
}
trait C
{
public function smallTalk()
{
echo 'c';
}
public function bigTalk()
{
echo 'C';
}
}
class Talker
{
use A, B, C {
B::smallTalk insteadof A, C;
A::bigTalk insteadof B, C;
}
}
class Aliased_Talker
{
use A, B {
B::smallTalk insteadof A;
A::bigTalk insteadof B;
B::bigTalk as talk;
}
}
注意 : 如果有兩的以上的 Traits 有相同名稱時,insteadof 必須詳列出來,只宣告 A::bigTalk
而沒有 as or insteadof 是不行的。
如果Class有與Trait相同的 function,以Class為主
在使用trait經常會發生Class內不小心把 Trait 的 function 給 Overwrite,讓自己豬頭debug許久。如下面的情況:
<?php
trait HelloWorld
{
public function sayHello()
{
echo 'Hello World!';
}
}
class TheWorldIsNotEnough
{
use HelloWorld;
public function sayHello()
{
echo 'Hello Universe!';
}
}
$o = new TheWorldIsNotEnough();
$o->sayHello();
echo "\n";
Trait可以使用到繼承的物件
為了避免上面的問題發生,程式必須做些手腳,將共同的functions轉移到 Base Class,搭配使用is_callable
加上parent
的方式先做檢查,如下面的範例:
<?php
class Base
{
public function sayHello()
{
echo 'Hello ';
}
}
trait SayWorld
{
public function sayHello()
{
if (is_callable('parent::sayHello')) {
parent::sayHello();
}
echo 'World!';
}
}
class MyHelloWorld extends Base
{
use SayWorld;
}
注意:在Trait內小心使用self::
,很容易造成infine loop而memory leak。
if (is_callable('self::sayHello')) {
self::sayHello();
}