2021年1月21日

PHP 魔術方法|__get 及 __set 的說明與範例,搭配 Laravel 實例說明

Introduce to php magic method: __get & __set

本篇為PHP 系列

接續PHP魔術方法系列,

此次要講的是,可以讓操作資料更彈性的 __get__set

先來看看官網定義:

__set() is run when writing data to inaccessible (protected or private) or non-existing properties.

__get() is utilized for reading data from inaccessible (protected or private) or non-existing properties.

簡單來說,當存取到不存在(non-existing)、或沒有讀取權限(protected or private)的屬性(property)時,就會自動觸發__get__set

先看__get

先來看一個使用__get的範例·:

class Fruit
{
    public $lemon = 'goodLemon';
    private $cherry = 'goodCherry';
    protected $orange = 'goodOrange';

    public function __get($name)
    {
        return "[__get] $name";
    }
}

$fruit = new Fruit();

// 1. public
//    → string(5) "goodLemon" 
var_dump($fruit->lemon); 

// 2. inaccessible(pivate)
//    → string(14) "[__get] cherry"
var_dump($fruit->cherry);

// 3. inaccessible(protected) 
//    → string(14) "[__get] orange"
var_dump($fruit->orange);

// 4. non-existing
//    → string(13) "[__get] apple"
var_dump($fruit->apple);

如範例所見,

  1. lemno是public的屬性,所以可以正常讀取。
  2. cherry是private,外界基本上是沒辦法讀取的,因此觸發了__get魔術方法。
  3. orange是protected,外f界基本上是沒辦法讀取的,因此觸發了__get魔術方法。
  4. 第四個範例的apple,基本上根本沒有宣告過,所以是不存在的,也觸發了__get魔術方法。

補充:

__get的第一個參數$name就是使用者所呼叫的屬性(property)名稱,例如,當使用者呼叫$fruit->cherry時,__get收到的第一個參數就是'cherry'字串。

再看__set

一樣直接來看__set的範例:

class Fruit
{
    public $lemon = '';
    private $cherry = '';
    protected $orange = '';

    public function __set($name, $value)
    {
        echo "[__set] $name, $value".PHP_EOL;
    }
}

$fruit = new Fruit();

// 1. public
//    → 正常
$fruit->lemon = 'goodLemon';

// 2. pivate
//    → [__set] cherry, goodCherry
$fruit->cherry = 'goodCherry';

// 3. protected
//    → [__set] orange, goodOrange
$fruit->orange = 'goodOrange';

// 4. non-existing
//    → [__set] apple, goodApple
$fruit->apple = 'goodApple';

如範例所見,

  1. lemno是public的屬性,所以可以正常寫入。
  2. cherry是private,外界基本上是沒辦法寫入的,因此觸發了__set魔術方法。
  3. orange是protected,外界基本上是沒辦法寫入的,因此觸發了__set魔術方法。
  4. 第四個範例的apple,基本上根本沒有宣告過,所以是不存在的,也觸發了__set魔術方法。

補充:

__set的第一個參數$name一樣是屬性的名稱,第二個參數$value則是想要設定的值,例如$fruit->cherry = 'goodCherry'$value就是字串'goodCherry'

get、set放在一起看

大家都知道, 一般來說,是無法讀取private屬性的,所以像下面範例一樣,直接讀取private $apple,程式會直接噴錯。

class Fruit
{
    private $apple;
}

$fruit = new Fruit();

// PHP Fatal error:  Uncaught Error: Cannot access private property Fruit::$apple
// → 程式終止
var_dump($fruit->apple);

$fruit->apple = 'apple';

var_dump($fruit->apple);

因此,為了要完成這段程式,我們需要同時利用__get__set

class Fruit
{
    private $apple;

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

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

$fruit = new Fruit();

// 1. NULL
var_dump($fruit->apple);

// 2. 成功
$fruit->apple = 'apple';

// 3. string(5) "apple"
var_dump($fruit->apple);

  1. 第一次呼叫apple的時候,雖然它仍然是private的屬性,但因為這次有__get了,所以不會報錯,得到了NULL
  2. 我們assign一個值給apple,透過__set,我們成功把值賦與了變數$apple
  3. 再一次呼叫apple,因為前面成功assign,這次可以成功拿到字串apple

補充:

$this->$name,這樣的用法,是因為php的語法特性,可以把變數中所包含的字串取出,作為變數名稱。所以當$name為'apple'時,$this->$name等同於$this->apple

Laravel中的應用

其實__get__set在Laravel中,有相當廣泛的用運用,而其中有一個應用是,大家基本上都會用到的,那就是Model。

不知道大家有沒有好奇過,為什麼Laravel中的Eloquent Model,可以的取得db中任意欄位的值?為什麼不需要在Model中做事先的宣告?

$user = User::first();
$user->email;

像這個範例,你並不用手動去User Model的類別新增email屬性,就可以直接透過user model物件取得email。

其實這就是__get__set在其中搞怪。

如果我們把Laravel Model的程式找出來,會發現這段:

// 路徑:/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php

/**
 * Dynamically retrieve attributes on the model.
 *
 * @param  string  $key
 * @return mixed
 */
public function __get($key)
{
    return $this->getAttribute($key);
}

/**
 * Dynamically set attributes on the model.
 *
 * @param  string  $key
 * @param  mixed  $value
 * @return void
 */
public function __set($key, $value)
{
    $this->setAttribute($key, $value);
}

就會發現Laravel其實也是透過魔術方法,去幫你取得各種屬性的!

當我們要讀取email時,因為user model中並不存在這個屬性,因此會觸發__get,再藉由getAttribute方法,去找出真正的值。

什麼時候要使用get或set?

  1. 類別的是動態產生的,難以事前宣告,例如前面提到的Laravel的Eloquent Model。

  2. 需要透過一些特殊的方式來存取property,可藉由__get__set的來一致性地處理。

    function __get($eky)
    {
      return $this->doSomethingBeforeGet($this->$key);
    }
    
    function __set($eky, $value)
    {
      return $this->$key = $this->doSomethingBeforeSet($value);
    }
    

     

  3. property傳遞,一個主類別可以注入許多小類別,當想取得的property不屬於主類別時,可以藉由__get將請求傳遞到小類別,並取得小類別的的property。

    function __get($eky)
    {
      return $this->dataStoreManager->get($key);
    }
    
    function __set($eky, $value)
    {fs
      return $this->dataStoreManager->set($eky, $value);
    }
    

小結

__get__set這兩個魔術方法,可以讓開發者更彈性地的操作資料,小缺點就是debug的時候可能比較困難,因為有些事情被魔術般地完成了。

但如果可以克服這個缺點,就可以善用這些特性,設計出更有彈性跟變化的類別,來滿足實際的需求。

環境

PHP 7.4.12

延伸

認識PHP魔術方法: __call

看更多PHP系列