本篇為PHP系列。
先貼個官方說明壓壓驚,The ArrayAccess interface
Interface to provide accessing objects as arrays.
簡單來說,這個介面讓你可以用讀取陣列Array
的方式,去讀取物件Object
。
這個介面總共提供四種方法,讓你的物件可以使用:
ArrayAccess {
/* Methods */
abstract public offsetSet(mixed $offset, mixed $value): void
abstract public offsetGet(mixed $offset): mixed
abstract public offsetExists(mixed $offset): bool
abstract public offsetUnset(mixed $offset): void
}
我會一個一個來看,想跳過細節說明的人,文末有Laravel Container的範例可以參考。
範例與說明
ArrayAccess::offsetSet
官方說法:Assign a value to the specified offset
其實就是將一個value放入offset中,
範例如下,我們定義一個類別叫做Container
,並且實作ArrayAccess
介面。
class Container implements ArrayAccess
{
private $data = [];
public function offsetSet($offset, $value)
{
$this->data[$offset] = $value;
}
public function offsetGet($offset) {}
public function offsetExists($offset) {}
public function offsetUnset($offset) {}
}
我們試著去執行這樣的程式,把'Demo'
塞進去'name'
:
$container = new Container;
$container['name'] = 'Demo';
var_dump($container);
輸出結果如下,可以看到private的$data
,已經儲存了剛剛丟進去的資料了:
object(Container)#1 (1) {
["data":"Container":private]=>
array(1) {
["name"]=>
string(4) "Demo"
}
}
說明一下,一般來說,物件是沒辦法用square brackets
,也就是沒辦法以[]
的方式來操作資料的,如果對沒有實作ArrayAccess的類別使用Array操作,你會獲得這個錯誤訊息:
Uncaught Error: Cannot use object of type Container as array
那因為我們已經實作了ArrayAccess類別,所以可以。
回頭來看$container['name'] = 'Demo';
做了什麼。
基本上它就是呼叫了offsetSet
方法,然後將資料塞入內部的$data
中,所以可以從前面的輸出結果看到,$offset
其實就是[]
裡面的值,也就是範例的'name'
,俗稱的key或index,$value
則是'Demo'
。
特別注意的是,這邊是為了範例簡單,所以內部的$data
資料容器也是一個array。其實可以看到,offsetSet
就是一個普通的function,所以在裡面想要對$offset
跟$value
怎麼做都可以,可以丟到db,也可以丟到另一個物件,並沒有強制只能丟到array。
ArrayAccess::offsetGet
官方說法:Offset to retrieve
透過offset取得特定value。
延續前面的範例,補上一個offsetGet的方法,
class Container implements ArrayAccess
{
private $data = [];
public function offsetSet($offset, $value)
{
$this->data[$offset] = $value;
}
public function offsetGet($offset)
{
return $this->data[$offset];
}
public function offsetExists($offset) {}
public function offsetUnset($offset) {}
}
測試一下,
$container = new Container;
$container['name'] = 'Demo';
var_dump($container['name']);
var_dump($container['version']);
輸出結果如下,
string(4) "Demo"
NULL
跟Array很熟悉的人都知道,直接對Array使用[]
就可以取值,所以$container['name']
就是透過offset'name'
去取出value。
這邊就是呼叫offsetGet
去取值,一樣,內部的實作是可以自由發揮的,因為我們$data
是array,所以直接把$offset丟給$data
就好了。
所以當我尋找'name'
的時候,他會回傳我前面設定的'Demo'
,而如果是其他offset,則會回傳NULL。
我們也可以稍微改變一下offsetGet
的行為:
public function offsetGet($offset)
{
if (array_key_exists($offset, $this->data)) {
return $this->data[$offset];
}
return "Offset:{$offset} Not Found!";
}
這樣前面的輸出就會變成:
string(4) "Demo"
string(25) "Offset:version Not Found!"
ArrayAccess::offsetExists
官方說法:Whether an offset exists
判斷offset是否存在。
繼續擴充我們的程式,補上offsetExists
:
class Container implements ArrayAccess
{
private $data = [];
public function offsetSet($offset, $value)
{
$this->data[$offset] = $value;
}
public function offsetGet($offset)
{
return $this->data[$offset];
}
public function offsetExists($offset)
{
return array_key_exists($offset, $this->data);
}
public function offsetUnset($offset) {}
}
然後執行這段程式:
$container = new Container;
$container['name'] = 'Demo';
$container['updatedAt'] = null;
var_dump(isset($container['name']));
var_dump(isset($container['updatedAt']));
var_dump(isset($container['version']));
結果如下:
bool(true) // name
bool(true) // updatedAt
bool(false) // version
沒錯,當你對某個offset執行isset()
的時候,就會去觸發offsetExists
方法,然後觸發array_key_exists
。
可以看到'name'
跟'updatedAt'
key存在,所以true
,而'version'
不存在,所以false
。
一樣,開發者可以在offsetExists
裡面根據實際需求做變化。
empty
另外還有一個比較特別的地方,就是empty()
,empty()
也會去觸發offsetExists
,然後回傳相反的結果。
我們來測試一下,改成這樣執行:
$container = new Container;
$container['name'] = 'Demo';
$container['updatedAt'] = null;
var_dump(empty($container['name']));
var_dump(empty($container['updatedAt']));
var_dump(empty($container['version']));
結果會是:
bool(false) // name
bool(true) // updatedAt
bool(true) // version
offset 'name'
不是empty,所以為false
,而'updatedAt'
及'version'
都是空的,所以會是true
。
看起來很合理對吧?但仔細一看這邊有個有趣的小地方。
因為我們的offsetExists
是用array_key_exists
去實作判斷的,如果按照前面說的邏輯來看,'updatedAt'
應該是存在的offset,offsetExists
會回傳true
然後變成相反的false
,然而並沒有。
其實是因為這邊,PHP有特別設計,當使用empty()
觸發offsetExists
後,如果回傳是true
,offset存在,那它會進一步去呼叫offsetGet
,來確保是不是真的empty。
用這個機制來看,因為'updatedAt'
這個offset是存在的,PHP會在去呼叫offsetGet
,然後得到的value null
,null
對empty來說,就真的是空的了!
這邊稍微總結一下遇到empty的判斷方式是:
!offsetExists($offset) || empty(offsetGet($offset))
ArrayAccess::offsetUnset
官方說法:Unset an offset
移除offset。
終於來到最後一個方法,offsetUnset
,我們來把範例補完:
class Container implements ArrayAccess
{
private $data = [];
public function offsetSet($offset, $value)
{
$this->data[$offset] = $value;
}
public function offsetGet($offset)
{
return $this->data[$offset];
}
public function offsetExists($offset)
{
return array_key_exists($offset, $this->data);
}
public function offsetUnset($offset)
{
unset($this->data[$offset]);
}
}
執行以下程式:
$container = new Container;
$container['name'] = 'Demo';
var_dump($container['name']);
unset($container['name']);
var_dump($container['name']);
輸出結果:
string(4) "Demo"
NULL
相信大家看到這邊應該都知道發生什麼事了,當呼叫unset
的時候,會觸發offsetUnset
方法。
所以我們可以在offsetUnset
裡面去攥寫程式,把對應的offset的移除掉。
最後還是再提醒一次,因為這邊內部儲存機制是用Array做範例,實際上可以是任意方式喔。
現實中的應用
大家有注意到我範例的類別名稱,取名是Container
嗎?其實這就是為了帶出這個範例。
在著名的框架Laravel
中,它的Container其實就實作了ArrayAccess
介面。
Laravel的實作當然複雜得多,所以我把部分程式碼找出來給大家看一下。
Laravel Container類別的全名是Illuminate\Container\Container
,
路徑應該會在這個位置,大家也可以自己去找來看看。
/vendor/laravel/framework/src/Illuminate/Container/Container.php
或是參考Laravel官方github上的檔案。
可以看到Container
實作了ArrayAccess
Interface,
class Container implements ArrayAccess, ContainerContract
挑選其中幾個跟ArrayAccess有關的方法:
public function offsetExists($key)
{
return $this->bound($key);
}
public function offsetGet($key)
{
return $this->make($key);
}
public function offsetSet($key, $value)
{
$this->bind($key, $value instanceof Closure ? $value : function () use ($value) {
return $value;
});
}
public function offsetUnset($key)
{
unset($this->bindings[$key], $this->instances[$key], $this->resolved[$key]);
}
Container使用ArrayAccess結合其自身的其他方法,因此可以提供不同的方式讓開發者來操作的Container
類別。
從這邊也可以了解,為什麼在Laravel的主要核心實作Application.php
(參考)中,會看到$this['events']
這樣子使用方式。
public function bootstrapWith(array $bootstrappers)
{
$this->hasBeenBootstrapped = true;
foreach ($bootstrappers as $bootstrapper) {
$this['events']->dispatch('bootstrapping: '.$bootstrapper, [$this]);
$this->make($bootstrapper)->bootstrap($this);
$this['events']->dispatch('bootstrapped: '.$bootstrapper, [$this]);
}
}
因為Application
繼承了Container
,所以也繼承Container
中,實作的ArrayAccess
的相關用法,因此使用$this['events']
時,其實觸發了Container
的offsetGet
。
結語
ArrayAccess
是PHP提供的一個介面,作為內建功能跟自訂功能連結的橋樑,讓開發者可以更有彈性的創作。
我認為有幾個時機可以使用這個介面:
- 需要對資料有權限控管,每次讀取之前,都要做判斷
- 需要封裝資料,有些資料背後的儲存機制比較複雜,需要做處理
- 給開發者自己好用的介面,呈上,透過這個介面把複雜的資料結構包裝起來
最後,可能要稍微注意的是,這個方法畢竟只是讓類別用起來像是Array而已,並不是真的Array,有些for array的方法,可能會有問題,大家要注意一下,避免踩雷。
更多PHP相關文章,請參考:PHP系列