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的物件來說,立即可以延伸運用到 getLatitudegetLongitude 的方法。執行的結果如下: https://gyazo.com/9bb2b067e9374b3c228a9fb394e35b25

使用Trait的注意事項

如果Trait出現相同的 functions 名稱

這時候我們必須用 insteadof 或是 as 指定使用哪一個trait的 functions,不然會得到fatal errors。

https://gyazo.com/6d117377a54bf0e5e302b24e96c195ce

如下面的例子:

<?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";

結果輸出就成為: https://gyazo.com/9d45d1292033146c3fa9bd0e3ea8d76b

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

輸出如下: https://gyazo.com/54a5ba1cd8ed41e54d5145cf0ba7ab45

注意:在Trait內小心使用self::,很容易造成infine loop而memory leak。

if (is_callable('self::sayHello')) {
    self::sayHello();
}

https://gyazo.com/53d88cbb924bc5d17931188e5d7a9b15

results matching ""

    No results matching ""