Laravelのソースコード内を
implements[^\n]+ArrayAccess
という正規表現でググるとArrayAccessを満たすClassを軒並み見つけられます。この記事ではこれらのClassで使われているArrayAccessの実装方法とその設計について述べます。
ArrayAccessというのはPHPの組み込みインタフェースの一つです。
PHP: ArrayAccess – Manual
このインタフェースを満たしたClassのインスタンスは配列として値を呼び出せるようになります。例えば次です。
<?php
class obj implements ArrayAccess {
private $container = array();
public function __construct() {
$this->container = array(
"one" => 1,
"two" => 2,
"three" => 3,
);
}
public function offsetSet($offset, $value) {
if (is_null($offset)) {
$this->container[] = $value;
} else {
$this->container[$offset] = $value;
}
}
public function offsetExists($offset) {
return isset($this->container[$offset]);
}
public function offsetUnset($offset) {
unset($this->container[$offset]);
}
public function offsetGet($offset) {
return isset($this->container[$offset]) ? $this->container[$offset] : null;
}
}
$obj = new obj;
var_dump(isset($obj["two"]));
var_dump($obj["two"]);
unset($obj["two"]);
var_dump(isset($obj["two"]));
$obj["two"] = "A value";
var_dump($obj["two"]);
$obj[] = 'Append 1';
$obj[] = 'Append 2';
$obj[] = 'Append 3';
print_r($obj);
配列として呼び出し、代入、isset()、empty()、unset()が使えるようになります。注意するのは配列となっているわけではないことです。上のコードの続きで
array_pop($obj);
などとした場合、引数がArray型でないと怒られ、PHPが致命的なエラーを起こします。
Laravelのソースコード中のいくつかのClassではこのArrayAccessを実装しています。例えばデータベース定義を行うクラスであるFluentでは次の様になっています。
/**
* Determine if the given offset exists.
*
* @param string $offset
* @return bool
*/
public function offsetExists($offset)
{
return isset($this->attributes[$offset]);
}
/**
* Get the value for a given offset.
*
* @param string $offset
* @return mixed
*/
public function offsetGet($offset)
{
return $this->get($offset);
}
/**
* Get an attribute from the fluent instance.
*
* @param string $key
* @param mixed $default
* @return mixed
*/
public function get($key, $default = null)
{
if (array_key_exists($key, $this->attributes)) {
return $this->attributes[$key];
}
return value($default);
}
/**
* Set the value at the given offset.
*
* @param string $offset
* @param mixed $value
* @return void
*/
public function offsetSet($offset, $value)
{
$this->attributes[$offset] = $value;
}
/**
* Unset the value at the given offset.
*
* @param string $offset
* @return void
*/
public function offsetUnset($offset)
{
unset($this->attributes[$offset]);
}
いずれもattributesへのアクセス方法です。attributesにはカラムの定義が羅列されます。
ブラウザに返すためのwebページを定義するクラスViewでは次の様になっています。
/**
* Determine if a piece of data is bound.
*
* @param string $key
* @return bool
*/
public function offsetExists($key)
{
return array_key_exists($key, $this->data);
}
/**
* Get a piece of bound data to the view.
*
* @param string $key
* @return mixed
*/
public function offsetGet($key)
{
return $this->data[$key];
}
/**
* Set a piece of data on the view.
*
* @param string $key
* @param mixed $value
* @return void
*/
public function offsetSet($key, $value)
{
$this->with($key, $value);
}
/**
* Add a piece of data to the view.
*
* @param string|array $key
* @param mixed $value
* @return $this
*/
public function with($key, $value = null)
{
if (is_array($key)) {
$this->data = array_merge($this->data, $key);
} else {
$this->data[$key] = $value;
}
return $this;
}
/**
* Unset a piece of data from the view.
*
* @param string $key
* @return void
*/
public function offsetUnset($key)
{
unset($this->data[$key]);
}
いずれもdataへのアクセス方法です。dataにはHTMLコードのテンプレートファイルに渡す変数が羅列されます。
ここで挙げたのは二例ですが、Collection、Modelなど他にも同様の実装はあります。この実装の方針は、Classの中に主目的のデータらを格納した配列を持ち、その配列を直接扱う方法の一つとしてArrayAccessを実装、というものです。このため次の様にClassを作るとLaravel内のコードと違和感なく扱えるままClassを拡張していくことができます。
class MyModel implements ArrayAccess
{
/** @var array 主データ */
public $data;
//
// $dataを操る色々
//
/**
* {@inheritdoc}
* @param mixed $offset
* @return bool
*/
public function offsetExists($offset)
{
// この実装は疑問。LaravelではoffsetExistsの返り値がarray_key_existsの返り値であることがしばしばある。
// しかしArrayAccessの実装によってisset($this[$offset])とした時、$this->offsetExists($offset)が評価される。
// Laravelに従った場合、$this->data[$offset]=nullの時にisset($this->data[$offset])===false、isset($this[$offset])===trueとなる。
// この実装の場合、isset($this->data[$offset])===isset($this[$offset])が担保できる。
return isset($this->data[$offset]);
}
/**
* {@inheritdoc}
* @param mixed $offset
* @return mixed
*/
public function offsetGet($offset)
{
return $this->data[$offset];
}
/**
* {@inheritdoc}
* @param mixed $offset
* @param mixed $value
* @return void
*/
public function offsetSet($offset, $value)
{
return $this->data[$offset] = $value;
}
/**
* {@inheritdoc}
* @param mixed $offset
* @return void
*/
public function offsetUnset($offset)
{
unset($this->data[$offset]);
}
}
このやり方はリレーショナルデータベース以外のデータリソースを参照するModelを作る時などで割と便利です。EloquentをそのままExtendすると不意にクエリビルダが走る事故が起きますが、この書き方なら起きません。また適切に拡張した場合、Controller等のModelを外部から参照する際に他のEloquentModelと記述上の区別なしに扱うことができます。