【Laravel】LaravelのソースコードでよくあるArrayAccessを実装するClass設計

著者:杉浦

【Laravel】LaravelのソースコードでよくあるArrayAccessを実装するClass設計

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

  • この記事いいね! (1)

著者について

杉浦 administrator