4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

はじめに

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

目次

  1. 前提の仕様
  2. 調べて分かったこと
  3. 解決方法
  4. 参考文献

前提の仕様

・フロントからのリクエストパラメータを「キャメルケース」と「スネークケース」両方許容できるように
・バックエンド側では「スネークケース」に統一する
・formRequestによるバリデーションの前に「スネークケース」に変換する必要がある
・アプリ内のすべてのPOSTリクエストに対応(リクエストオブジェクトの構造やパラメータは各処理によって異なる)
・多次元配列にも対応(6階層までネストされてるリクエストがある等)

例)request body
$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が作成される

app/Http/Middleware/ConversionToSnakeCase.php
class ConversionToSnakeCase
{
    public function handle($request, Closure $next)
    {
       // ここに関数を作成
       // 関数実行処理記述
       return $next($request);
    }
}

・app/Http/Kernel.php に追記

app/Http/Kernel.php
protected $routeMiddleware = [
    中略...,
    'snake' => \App\Http\Middleware\ConversionToSnakeCase::class,
];

・routes/api.phpに追記
適用したいルーティングの末尾に追記

routes/api.php
Route::post('hoge/hoge', [\App\Http\Controllers\v1\hogeController::class, 'hoge'])->middleware('snake');

【再起処理による関数作成】

・app/Http/Middleware/ConversionToSnakeCase.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階層分の多次元配列の代入式を記述

$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でテスト

例)request body
{
    "PerfectHuman" : "",
    "OriginalFood" : {
        "HogeFish" : "1",
        "Color":[
            {
                "Black": {},
                "Red" : {
                    "Fluits" : {
                        "Apple":"4"
                    }
                },
                "Blue": {
                    "Slime": "1"
                }
            },
            {}
        ]
    },
    "OriginalClothes" : {},
    "OriginalDrink":[
        {
            "Cola" : "10"
        },
        {
            "Cola" : "5",
            "Tea":"5"
        }
    ]
}

ログ出力

ConversionToSnakeCase.php
\Log::debug($request->all());

実行結果

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階層)

$new_array['original_food']['color'][0]['red']['fluits']['apple']

この時の$keysの中身

laravel.log
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で確認できる

laravel.log
2023-02-03 17:55:11.535 local.[DEBUG] 以下にログが出力される

終わりに

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

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

参考文献

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?