Laravel API 多次元連想配列のリクエストパラメータをスネークケースに変換する関数を作成した時のメモ

# はじめに
業務にて、Laravelを用いたAPI開発の中で、フロントエンドから飛んでくるリクエストパラメータを「キャメルケース」と「スネークケース」両方とも許容できるようにするタスクがあったので、そのメモを未来の自分のために残そうと思う。
解決方法としては、リクエストの全要素のキーを見て、キャメルケースだった場合にスネークケースに変換する関数をミドルウェアで作成し、ルート時にそれを通すことにした。

# 目次
1. [前提の仕様](#前提の仕様)
2. [調べて分かったこと](#調べて分かったこと)
3. [解決方法](#解決方法)
4. [参考文献](#参考文献)

# 前提の仕様
・フロントからのリクエストパラメータを「キャメルケース」と「スネークケース」両方許容できるように
・バックエンド側では「スネークケース」に統一する
・formRequestによるバリデーションの前に「スネークケース」に変換する必要がある
・アプリ内のすべてのPOSTリクエストに対応（リクエストオブジェクトの構造やパラメータは各処理によって異なる）
・多次元配列にも対応（6階層までネストされてるリクエストがある等）
```php:例）request body
$array = {
    "A" : "TEST",
    "B" : {
        "B1" : "TEST",
        "B2": {
            "B3" : {
                "B4" : {
                    "B5": {
                        "B6": "TEST"
                    }
                }
            }
        }
    },
    "C" : {
        "C1" : "TEST"
    }
}
```
この場合以下が最も深いネスト（6階層）
```php
$array['B']['B2']['B3']['B4']['B5']['B6'] = "TEST"
```

# 調べて分かったこと
・formRequestによるバリデーションの前に「スネークケース」に変換するには、ミドルウェア（とその中に関数）を作成し、ルーティング時にそこを通す方法がよさそう
・PHP標準搭載の便利関数では対応出来なさそう
　→array_replace_recursive()やarray_map()などは再起的な処理で、各要素の値をいじったりできるのだが、キー（名）は変更不可のため
・構造が不確定の配列に対する処理は、「再起処理」によって全要素見ていくことができる
　→必要な回数分だけforeach文を回す
・$array[ ]の[ ]の数を文字列にして変数に格納したりできないため、以下のように
```php
$array['key']='value'; // １階層の配列
$array['key']['key2']='value'; // ２階層の配列
$array['key']['key2']['key3']='value'; //３階層の配列
```
といった感じで必要階層分は直接書くしかなさそう
そのため、定義されている分より深い階層構造のリクエストには対応出来ない（が、リクエストの構造を把握できていれば問題ない）

以下、まとめ
・ミドルウェアを作成する
・PHP標準搭載の関数で対処はできない
・再起処理による関数を作成する
・階層の分[ ]は直書きする必要がある

# 解決方法
### 【ミドルウェア作成、セット】
・app/Http/Middlewareに追加
```php:ターミナル
$php artisan make:middleware ConversionToSnakeCase
```
↓
app/Http/Middleware/ConversionToSnakeCase.phpが作成される
```php:app/Http/Middleware/ConversionToSnakeCase.php
class ConversionToSnakeCase
{
    public function handle($request, Closure $next)
    {
       // ここに関数を作成
       // 関数実行処理記述
       return $next($request);
    }
}
```

・app/Http/Kernel.php に追記

```php:app/Http/Kernel.php
protected $routeMiddleware = [
    中略...,
    'snake' => \App\Http\Middleware\ConversionToSnakeCase::class,
];
```
・routes/api.phpに追記
適用したいルーティングの末尾に追記
```php:routes/api.php
Route::post('hoge/hoge', [\App\Http\Controllers\v1\hogeController::class, 'hoge'])->middleware('snake');
```

### 【再起処理による関数作成】
・app/Http/Middleware/ConversionToSnakeCase.php内に作成
```php:app/Http/Middleware/ConversionToSnakeCase.php
<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Str;

class ConversionToSnakeCase
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle(Request $request, Closure $next)
    {
        // 無名関数
        $array_snake_recursive = function($array, $new_array=[], $keys=[]) use(&$array_snake_recursive) {
            foreach ($array as $key => $value) {
                if(preg_match('/[A-Z]/', $key)){
                    $key = Str::snake($key);
                }

                if (is_array($value) && count($value)) {
                    $keys[] = $key; // 各階層のキー名orインデックス番号を格納
                    $new_array = $array_snake_recursive($value, $new_array, $keys); // 再起処理
                    array_splice($keys, -1);
                } else {
                    $depth = count($keys) + 1; // 階層の深さ
                    switch ($depth) {
                        // 階層増える場合caseを追記していく
                        case 6:
                            $new_array[$keys[0]][$keys[1]][$keys[2]][$keys[3]][$keys[4]][$key] = $value;
                            break;
                        case 5:
                            $new_array[$keys[0]][$keys[1]][$keys[2]][$keys[3]][$key] = $value;
                            break;
                        case 4:
                            $new_array[$keys[0]][$keys[1]][$keys[2]][$key] = $value;
                            break;
                        case 3:
                            $new_array[$keys[0]][$keys[1]][$key] = $value;
                            break;
                        case 2:
                            $new_array[$keys[0]][$key] = $value;
                            break;
                        default:
                            $new_array[$key] = $value;
                            break;
                    }
                }
            }
            return $new_array;
        };

        $array = $request->all();
        $new_array = $array_snake_recursive($array);
        $request->replace($new_array);
        // \Log::debug($request->all()); // 確認用
        
        return $next($request);
    }
}

```
#### 【ロジック】
・再起処理により、必要分foreachを回していく
・空の配列```$new_array[]```を用意し、そこにデータを入れていく
・空の配列```$keys[]```を用意し、そこにキー名を順番に入れていく
・```Str::snake```を使い、キー名をスネークケースに変換
・条件式によって、```$keys```にキー名を入れる or ```$new_array```に値を代入
　foreach内```$value```の値がarray型かつ要素数が1以上だった場合（if）
　・再起処理の前に```$keys```に```$key```を入れる
　・再起処理を走らせる
　・再起処理の後に```$keys```の末尾の値を削除
　foreach内```$value```の値がstring型 or NULL or array型かつ要素数が0の場合（else）
　・```$keys```の値を使い、変数で```$new_array```のキー(名)を指定し、```$value```の値を代入
　・switch文で、1〜6階層分の多次元配列の代入式を記述
```php
$new_array[$keys[0]][$keys[1]][$keys[2]][$keys[3]][$keys[4]][$key] = $value;
```


#### 【foreach内の流れ】

・キー名をスネークケースに変換
```php
if(preg_match('/[A-Z]/', $key)){
    $key = Str::snake($key);
}
```

【```$value```の値がarray型かつ要素数が1以上だった場合（if）】
```php
if (is_array($value) && count($value))
    $keys[] = $key; // 階層が浅い順で処理が走る
    $new_array = $array_snake_recursive($value, $new_array, $keys); // 再起的にforeach開始
    array_splice($keys, -1); // 上の再起処理が始まるのが遅かった（階層が深い）順に処理される
}
```
・```$keys```に各階層のキー名を入れていく
・再起処理を実行し、```$new_array```に代入
　※```$new_array```に代入しないと1つのforeachが終了したとき```$new_array```がリセットされて引き継げない
・```$keys```から実行し終わった（配列の）キー名を削除
※上記のような再起処理のforeachは階層が浅い順で始まり、深い順で終了する

【```$value```の値がstring型 or NULL or array型かつ要素数が0の場合（else）】
```php
else {
$depth = count($keys) + 1; // 階層の深さ
switch ($depth) {
    // 階層増える場合caseを追記していく
    case 6:
        $new_array[$keys[0]][$keys[1]][$keys[2]][$keys[3]][$keys[4]][$key] = $value;
        break;
    ...中略
    case 2:
        $new_array[$keys[0]][$key] = $value;
        break;
    default:
        $new_array[$key] = $value;
        break;
}
```
・各階層のキー名が順番に入っているkeysの値を使って指定した箇所（```$new_array```）に```$value```の値を代入する

PostManでテスト
```php:例）request body
{
    "PerfectHuman" : "",
    "OriginalFood" : {
        "HogeFish" : "1",
        "Color":[
            {
                "Black": {},
                "Red" : {
                    "Fluits" : {
                        "Apple":"4"
                    }
                },
                "Blue": {
                    "Slime": "1"
                }
            },
            {}
        ]
    },
    "OriginalClothes" : {},
    "OriginalDrink":[
        {
            "Cola" : "10"
        },
        {
            "Cola" : "5",
            "Tea":"5"
        }
    ]
}
```
ログ出力
```php:ConversionToSnakeCase.php
\Log::debug($request->all());
```
実行結果
```php:laravel.log
array (
  'perfect_human' => NULL,
  'original_food' => 
  array (
    'hoge_fish' => '1',
    'color' => 
    array (
      0 => 
      array (
        'black' => 
        array (
        ),
        'red' => 
        array (
          'fluits' => 
          array (
            'apple' => '4',
          ),
        ),
        'blue' => 
        array (
          'slime' => '1',
        ),
      ),
      1 => 
      array (
      ),
    ),
  ),
  'original_clothes' => 
  array (
  ),
  'original_drink' => 
  array (
    0 => 
    array (
      'cola' => '10',
    ),
    1 => 
    array (
      'cola' => '5',
      'tea' => '5',
    ),
  ),
)
```
→想定通り動いてる

ちなみに以下、最も深い階層(6階層)
```php
$new_array['original_food']['color'][0]['red']['fluits']['apple']
```

この時の```$keys```の中身
```php:laravel.log
array (
  0 => 'original_food',
  1 => 'color',
  2 => 0,
  3 => 'red',
  4 => 'fluits',
)
```
※'apple'は```$key```の値

・最も深い階層が6階層以下なら、上記の例以外の構造の配列データでもいける（いくつかテスト済み）
・また、caseに追記すれば7以上に増やせる

・最後に、新しく作成した配列(```$new_array```)を```$request```に入れ直す
```php
$request->replace($new_array);
```

#### 【その他】
##### Middlewareと無名関数
・middleware内ではhandle()以外の関数が使えなかったため、無名関数を使って実装した
・再起処理をするには自身の関数名をuseで定義する必要がある

##### Logの出力方法
・phpファイルの任意の場所で以下記述
```php
\Log::debug();
```
・laravel.logで確認できる
```php:laravel.log
2023-02-03 17:55:11.535 local.[DEBUG] 以下にログが出力される
```

### 終わりに
再起処理によるキー名を変更するロジックの情報は、多次元配列ではない、または、決まった一つの構造前提のものしかなかったため、オリジナルで作ってみた。
もっといいアイデアや、バグなどがあったらご教授願いたい。

また、本記事執筆中に他に良案を思いついたので、形にできたら後日投稿したいと思う。

# 参考文献
- [Laravel に Middleware を追加する](https://qiita.com/atto/items/9228bd16d3dfd1552b67)
- [Laravelでのデバッグ方法4パターンまとめ](https://qiita.com/rarrrrsQ/items/34f3867bbcb17604e136)
- [PHPで並列階層の配列から多次元配列の生成と、多次元の連想配列から階層を維持しての出力方法](https://www.vektor-inc.co.jp/post/php-array-nest/)
- [[PHP]多次元配列の次元の深さ（階層）を調べる](https://php-archive.net/php/array-depth/)
- [PHPの無名関数を使って再帰処理を行う](https://blog.asial.co.jp/677)
- [PHP: 無名関数 -Manual](https://www.php.vn.ua/manual/ja/functions.anonymous.php)
- [PHP で入れ子の配列の中身を再帰的に変更する方法](https://gotohayato.com/content/484/)
- [Laravel 8.x HTTP Requests](https://readouble.com/laravel/8.x/ja/requests.html)
- [[Laravel] Requestクラスのメソッドまとめ](https://gentuki-blog.site/articles/qV3KuS42UwcP9Y9Zcwv7n)
- [【Laravel実践】リクエストパラメータ名をスネークケースに変換する方法 - Qiita
](https://qiita.com/Yuttti/items/5a03559a82ad43fccc70)