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と記述上の区別なしに扱うことができます。