【PHP】ドット記法での多次元配列アクセス

PHP

Laravelのconfig()等でよく使われているドット記法(dot notation)での配列アクセスをピュアPHPで実装していきます。

Laravel - The PHP Framework For Web Artisans
Laravel is a PHP web application framework with expressive, elegant syntax. We’ve already laid the foundation — freeing ...

「Illuminate\Support\Arr::dot()」でサポートされていたりしますが

Laravel - The PHP Framework For Web Artisans
Laravel is a PHP web application framework with expressive, elegant syntax. We’ve already laid the foundation — freeing ...

敢えてピュアPHPで実装していきます。

前提

  • PHP8.1以降(執筆時点でPHP8.0はサポート終了しているため)

ドット記法とは

ここでいうドット記法とは、

多次元配列のキーをドット「.」区切りで連結する記法のことです。

例えば、PHPの多次元配列で

$columns = $data['database']['tables']['users']['columns'];

のようにアクセスするところを、

$columns = Arr::get('database.tables.users.columns');

のように「database.tables.users.columns」でアクセスしようというものです。

階層を掘り下げる毎にシングルクオートや角括弧で括る必要なく

少し短くなりスッキリします。

使用する多次元配列の例

▼data.php

<?php

return [
    'database' => [
        'connection' => 'mysql',
        'host' => '127.0.0.1',
        'user' => 'dbadmin',
        'password' => 'Pass_w0rd',
        'dbname' => 'webapp',
        'tables' => [
            'users' => [
                'columns' => [
                    'id' => 'user id, primary key',
                    'name' => "user's name, required",
                    'passowrd' => 'login password, required, encrypted',
                    'email' => "user's email",
                ],
            ],
            'posts' => [
                'columns' => [
                    'id' => 'post id, primary key',
                    'user_id' => "user's id, required",
                    'title' => 'title of the post',
                    'content' => 'content of the post',
                ],
            ],
        ],
    ],
];

オペレーターファイル

▼accessArray.php

<?php

// クラス読込
require __DIR__ . '/Arr.php';

// 配列データ読込
$data = require __DIR__ . '/data.php';

// 配列アクセッサーのインスタンス生成
$array = new MyLib\Arr($data);

// 指定要素の出力
var_dump($array->get('database.tables.users.columns'));

※Arr.phpは次で作成していきます。

プランA

ドット記法で指定されたキーをドットで文字列分割し、

各要素毎に多次元配列を掘り下げていく手法です。

▼処理部のコア

        // キーをドットで文字列分割してループ
        foreach (explode('.', $key) as $k) {

            // 配列キーが存在しなければnullを返す
            if (!array_key_exists($k, $data)) {
                return null;
            }

            // データ掘り下げ
            $data = $data[$k];
        }

▼「Arr.php」プランAの完成形

<?php

namespace MyLib;

class Arr
{
    /**
     * constructor
     *
     * @param mixed[]   $data
     */
    public function __construct(
        private array $data
    ) {
        $this->set($data);
    }

    /**
     * setter
     *
     * @param   mixed[]
     */
    public function set(array $data)
    {
        $this->data = $data;
    }

    /**
     * getter
     *
     * @param   string  $key = ''
     */
    public function get(string $key = '')
    {
        $data = $this->data;

        // キーが空ならすべて返す
        if (strlen($key) === 0) {
            return $data;
        }

        // キーをドットで文字列分割してループ
        foreach (explode('.', $key) as $k) {

            // 配列キーが存在しなければnullを返す
            if (!array_key_exists($k, $data)) {
                return null;
            }

            // データ掘り下げ
            $data = $data[$k];
        }

        // めしあがれ
        return $data;
    }
}

▼ accessArray.php 実行結果

array(4) {
  'id' =>
  string(20) "user id, primary key"
  'name' =>
  string(21) "user's name, required"
  'passowrd' =>
  string(35) "login password, required, encrypted"
  'email' =>
  string(12) "user's email"
}

プランB

LaravelのArr::dot()の実装に習います。

Arr::dot メソッドは、多次元配列を、深さを示すために「ドット」表記を使用する単一レベルの配列に平坦化します。

例えば

use Illuminate\Support\Arr;
 
$array = ['products' => ['desk' => ['price' => 100]]];
 
$flattened = Arr::dot($array);

のようなコードを実行すると

['products.desk.price' => 100]

のようになります。

実装コードを見てみると次のようになっています。

▼Illuminate\Collections\Arr::dot()

    /**
     * Flatten a multi-dimensional associative array with dots.
     *
     * @param  iterable  $array
     * @param  string  $prepend
     * @return array
     */
    public static function dot($array, $prepend = '')
    {
        $results = [];

        foreach ($array as $key => $value) {
            if (is_array($value) && ! empty($value)) {
                $results = array_merge($results, static::dot($value, $prepend.$key.'.'));
            } else {
                $results[$prepend.$key] = $value;
            }
        }

        return $results;
    }

ただし、

['products.desk' => ['price' => 100]]

のような途中の要素も欲しいので少し改造します。

▼Illuminate\Collections\Arr::dot() のパクリをリファクタ

    /**
     * Flatten a multi-dimensional associative array with dots.
     * Illuminate\Collections\Arr::dot()
     * からのパクリをちょっとリファクタ
     *
     * @param  iterable  $array
     * @param  string  $prepend
     * @return array
     */
    public function dot($array, $prepend = '')
    {
        $results = [];

        foreach ($array as $key => $value) {

            // 現在の要素をセット
            $results[$prepend.$key] = $value;

            // 要素が配列で子要素を持っているなら掘り下げ
            if (is_array($value) && ! empty($value)) {
                $results = array_merge($results, $this->dot($value, $prepend.$key.'.'));
            }
        }

        // よろ!
        return $results;
    }

このメソッドを絞り込み候補を提供する専用処理として追加し、ゲッター側で活用します。

▼ゲッター

    /**
     * getter
     *
     * @param   string  $key = ''
     */
    public function get(string $key = '')
    {
        // キーが空ならすべて返す
        if (strlen($key) === 0) {
            return $this->data;
        }

        // めしあがれ
        return $this->dot($this->data)[$key] ?? null;
    }

▼「Arr.php」プランBの完成形

<?php

namespace MyLib;

class Arr
{
    /**
     * constructor
     *
     * @param mixed[]   $data
     */
    public function __construct(
        private array $data
    ) {
        $this->set($data);
    }

    /**
     * setter
     *
     * @param   mixed[]
     */
    public function set(array $data)
    {
        $this->data = $data;
    }

    /**
     * getter
     *
     * @param   string  $key = ''
     */
    public function get(string $key = '')
    {
        // キーが空ならすべて返す
        if (strlen($key) === 0) {
            return $this->data;
        }

        // めしあがれ
        return $this->dot($this->data)[$key] ?? null;
    }

    /**
     * Flatten a multi-dimensional associative array with dots.
     * Illuminate\Collections\Arr::dot()
     * からのパクリをちょっとリファクタ
     *
     * @param  iterable  $array
     * @param  string  $prepend
     * @return array
     */
    public function dot($array, $prepend = '')
    {
        $results = [];

        foreach ($array as $key => $value) {

            // 現在の要素をセット
            $results[$prepend.$key] = $value;

            // 要素が配列で子要素を持っているなら掘り下げ
            if (is_array($value) && ! empty($value)) {
                $results = array_merge($results, $this->dot($value, $prepend.$key.'.'));
            }
        }

        // よろ!
        return $results;
    }
}

▼ accessArray.php の実行結果

array(4) {
  'id' =>
  string(20) "user id, primary key"
  'name' =>
  string(21) "user's name, required"
  'passowrd' =>
  string(35) "login password, required, encrypted"
  'email' =>
  string(12) "user's email"
}

ワイルドカード対応

ドット記法でのキー指定でよく見られる

「users.*.name」などのワイルドカード指定に対応していきます。

プランAでは対応しにくく冗長になりそうなので、

プランBで対応していきます。

dot() で返った配列のキーについて正規表現チェックをすれば対応できそうです。

正規表現は処理コストが高いですが、ワイルドカードが複数指定されたケースも想定すると、正規表現でのチェックが妥当でしょう。

なお、ワイルドカードは言語や機構により異なり、「アスタリスク(*)」や「ドット(.)」や「?」などがありますが、今回許容するワイルドカードは「アスタリスク(*)」のみとします。

一応定義をしておくと、

「アスタリスク(*)」→空を含む、PHPの配列キーとして指定できる int|string の内、任意の値に相当

としておきます。

▼正規表現チェック用パターン構築メソッド

    /**
     * 配列キーを基に正規表現チェック用のパターン文字列を返す
     *
     * @param   string $key
     */
    private function getPattern(string $key): string
    {
        return sprintf(
            "/^%s$/",
            implode(
                // アスタリスク(*)を
                // 「空含む任意の文字列」
                // の検出パターンに置換してキーを結合
                '.*',
                array_map(
                    // アスタリスク(*)以外をpreg_quote()
                    fn (string $e) => preg_quote($e),
                    // アスタリスク(*)で分割
                    explode('*', $key)
                )
            )
        );
    }

▼ゲッター

    /**
     * getter
     *
     * @param   string  $key = ''
     */
    public function get(string $key = '')
    {
        // キーが空ならすべて返す
        if (strlen($key) === 0) {
            return $this->data;
        }

        // ワイルドカードが無い場合
        if (!str_contains($key, '*')) {
            // めしあがれ
            return $this->dot($this->data)[$key] ?? null;
        }

        // ワイルドカード対応
        // -----------------
        // 返す配列初期化
        $data = [];
        // 正規表現パターン
        $pattern = $this->getPattern($key);
        // 候補配列をフィルター
        $data = array_filter(
            $this->dot($this->data),
            function (int|string $k) use ($pattern) {
                return preg_match($pattern, $k);
            },
            ARRAY_FILTER_USE_KEY
        );
        // 空ならnullを返す
        return $data === [] ? null : $data;
    }

▼「Arr.php」ワイルドカード対応版の完成形

<?php

namespace MyLib;

class Arr
{
    /**
     * constructor
     *
     * @param mixed[]   $data
     */
    public function __construct(
        private array $data
    ) {
        $this->set($data);
    }

    /**
     * setter
     *
     * @param   mixed[]
     */
    public function set(array $data)
    {
        $this->data = $data;
    }

    /**
     * getter
     *
     * @param   string  $key = ''
     */
    public function get(string $key = '')
    {
        // キーが空ならすべて返す
        if (strlen($key) === 0) {
            return $this->data;
        }

        // ワイルドカードが無い場合
        if (!str_contains($key, '*')) {
            // めしあがれ
            return $this->dot($this->data)[$key] ?? null;
        }

        // ワイルドカード対応
        // -----------------
        // 返す配列初期化
        $data = [];
        // 正規表現パターン
        $pattern = $this->getPattern($key);
        // 候補配列をフィルター
        $data = array_filter(
            $this->dot($this->data),
            function (int|string $k) use ($pattern) {
                return preg_match($pattern, $k);
            },
            ARRAY_FILTER_USE_KEY
        );
        // 空ならnullを返す
        return $data === [] ? null : $data;
    }

    /**
     * 配列キーを基に正規表現チェック用のパターン文字列を返す
     *
     * @param   string $key
     */
    private function getPattern(string $key): string
    {
        return sprintf(
            "/^%s$/",
            implode(
                // アスタリスク(*)を
                // 「空を含む任意の文字列」
                // の検出パターンに置換してキーを結合
                '.*',
                array_map(
                    // アスタリスク(*)以外をpreg_quote()
                    fn (string $e) => preg_quote($e),
                    // アスタリスク(*)で分割
                    explode('*', $key)
                )
            )
        );
    }

    /**
     * Flatten a multi-dimensional associative array with dots.
     * Illuminate\Collections\Arr::dot()
     * からのパクリをちょっとリファクタ
     *
     * @param  iterable  $array
     * @param  string  $prepend
     * @return array
     */
    public function dot($array, $prepend = '')
    {
        $results = [];

        foreach ($array as $key => $value) {

            // 現在の要素をセット
            $results[$prepend.$key] = $value;

            // 要素が配列で子要素を持っているなら掘り下げ
            if (is_array($value) && ! empty($value)) {
                $results = array_merge($results, $this->dot($value, $prepend.$key.'.'));
            }
        }

        // よろ!
        return $results;
    }
}

▼accessArray.php の実行結果

 ※キー指定「*.columns」

array(2) {
  'database.tables.users.columns' =>
  array(4) {
    'id' =>
    string(20) "user id, primary key"
    'name' =>
    string(21) "user's name, required"
    'passowrd' =>
    string(35) "login password, required, encrypted"
    'email' =>
    string(12) "user's email"
  }
  'database.tables.posts.columns' =>
  array(4) {
    'id' =>
    string(20) "post id, primary key"
    'user_id' =>
    string(19) "user's id, required"
    'title' =>
    string(17) "title of the post"
    'content' =>
    string(19) "content of the post"
  }
}

 ※ キー指定「*id」

array(3) {
  'database.tables.users.columns.id' =>
  string(20) "user id, primary key"
  'database.tables.posts.columns.id' =>
  string(20) "post id, primary key"
  'database.tables.posts.columns.user_id' =>
  string(19) "user's id, required"
}

 ※ キー指定「*.columns.*」

array(8) {
  'database.tables.users.columns.id' =>
  string(20) "user id, primary key"
  'database.tables.users.columns.name' =>
  string(21) "user's name, required"
  'database.tables.users.columns.passowrd' =>
  string(35) "login password, required, encrypted"
  'database.tables.users.columns.email' =>
  string(12) "user's email"
  'database.tables.posts.columns.id' =>
  string(20) "post id, primary key"
  'database.tables.posts.columns.user_id' =>
  string(19) "user's id, required"
  'database.tables.posts.columns.title' =>
  string(17) "title of the post"
  'database.tables.posts.columns.content' =>
  string(19) "content of the post"
}

※ キー指定「*.sessions.*」(存在しない)

NULL

※ キー指定「*」とするととんでもないことになりますが、

それは「仕様」であり、

キー指定の仕方が「サポート対象外」

であるとします。

以上です。

今回はドット記法でのアクセスについて記しましたが、

この逆で、Arr::undot()、つまり、ドット記法の配列を多次元配列化する処理については別記事にしています。

コメント

タイトルとURLをコピーしました