はじめに
業務にて、Laravelを用いたAPI開発の中で、フロントエンドから飛んでくるリクエストパラメータを「キャメルケース」と「スネークケース」両方とも許容できるようにするタスクがあったので、そのメモを未来の自分のために残そうと思う。
解決方法としては、リクエストの全要素のキーを見て、キャメルケースだった場合にスネークケースに変換する関数をミドルウェアで作成し、ルート時にそれを通すことにした。
目次
前提の仕様
・フロントからのリクエストパラメータを「キャメルケース」と「スネークケース」両方許容できるように
・バックエンド側では「スネークケース」に統一する
・formRequestによるバリデーションの前に「スネークケース」に変換する必要がある
・アプリ内のすべてのPOSTリクエストに対応(リクエストオブジェクトの構造やパラメータは各処理によって異なる)
・多次元配列にも対応(6階層までネストされてるリクエストがある等)
$array = {
"A" : "TEST",
"B" : {
"B1" : "TEST",
"B2": {
"B3" : {
"B4" : {
"B5": {
"B6": "TEST"
}
}
}
}
},
"C" : {
"C1" : "TEST"
}
}
この場合以下が最も深いネスト(6階層)
$array['B']['B2']['B3']['B4']['B5']['B6'] = "TEST"
調べて分かったこと
・formRequestによるバリデーションの前に「スネークケース」に変換するには、ミドルウェア(とその中に関数)を作成し、ルーティング時にそこを通す方法がよさそう
・PHP標準搭載の便利関数では対応出来なさそう
→array_replace_recursive()やarray_map()などは再起的な処理で、各要素の値をいじったりできるのだが、キー(名)は変更不可のため
・構造が不確定の配列に対する処理は、「再起処理」によって全要素見ていくことができる
→必要な回数分だけforeach文を回す
・$array[ ]の[ ]の数を文字列にして変数に格納したりできないため、以下のように
$array['key']='value'; // 1階層の配列
$array['key']['key2']='value'; // 2階層の配列
$array['key']['key2']['key3']='value'; //3階層の配列
といった感じで必要階層分は直接書くしかなさそう
そのため、定義されている分より深い階層構造のリクエストには対応出来ない(が、リクエストの構造を把握できていれば問題ない)
以下、まとめ
・ミドルウェアを作成する
・PHP標準搭載の関数で対処はできない
・再起処理による関数を作成する
・階層の分[ ]は直書きする必要がある
解決方法
【ミドルウェア作成、セット】
・app/Http/Middlewareに追加
$php artisan make:middleware ConversionToSnakeCase
↓
app/Http/Middleware/ConversionToSnakeCase.phpが作成される
class ConversionToSnakeCase
{
public function handle($request, Closure $next)
{
// ここに関数を作成
// 関数実行処理記述
return $next($request);
}
}
・app/Http/Kernel.php に追記
protected $routeMiddleware = [
中略...,
'snake' => \App\Http\Middleware\ConversionToSnakeCase::class,
];
・routes/api.phpに追記
適用したいルーティングの末尾に追記
Route::post('hoge/hoge', [\App\Http\Controllers\v1\hogeController::class, 'hoge'])->middleware('snake');
【再起処理による関数作成】
・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階層分の多次元配列の代入式を記述
$new_array[$keys[0]][$keys[1]][$keys[2]][$keys[3]][$keys[4]][$key] = $value;
【foreach内の流れ】
・キー名をスネークケースに変換
if(preg_match('/[A-Z]/', $key)){
$key = Str::snake($key);
}
【$value
の値がarray型かつ要素数が1以上だった場合(if)】
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)】
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でテスト
{
"PerfectHuman" : "",
"OriginalFood" : {
"HogeFish" : "1",
"Color":[
{
"Black": {},
"Red" : {
"Fluits" : {
"Apple":"4"
}
},
"Blue": {
"Slime": "1"
}
},
{}
]
},
"OriginalClothes" : {},
"OriginalDrink":[
{
"Cola" : "10"
},
{
"Cola" : "5",
"Tea":"5"
}
]
}
ログ出力
\Log::debug($request->all());
実行結果
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階層)
$new_array['original_food']['color'][0]['red']['fluits']['apple']
この時の$keys
の中身
array (
0 => 'original_food',
1 => 'color',
2 => 0,
3 => 'red',
4 => 'fluits',
)
※'apple'は$key
の値
・最も深い階層が6階層以下なら、上記の例以外の構造の配列データでもいける(いくつかテスト済み)
・また、caseに追記すれば7以上に増やせる
・最後に、新しく作成した配列($new_array
)を$request
に入れ直す
$request->replace($new_array);
【その他】
Middlewareと無名関数
・middleware内ではhandle()以外の関数が使えなかったため、無名関数を使って実装した
・再起処理をするには自身の関数名をuseで定義する必要がある
Logの出力方法
・phpファイルの任意の場所で以下記述
\Log::debug();
・laravel.logで確認できる
2023-02-03 17:55:11.535 local.[DEBUG] 以下にログが出力される
終わりに
再起処理によるキー名を変更するロジックの情報は、多次元配列ではない、または、決まった一つの構造前提のものしかなかったため、オリジナルで作ってみた。
もっといいアイデアや、バグなどがあったらご教授願いたい。
また、本記事執筆中に他に良案を思いついたので、形にできたら後日投稿したいと思う。