LaravelでのAPI実装時、知識不足でエラーを出してしまったため、メモです。

エラーの内容

SPAアプリを実装中、API側ソースを修正したところ、フロントエンドJS側でエラーが発生するようになった。

バージョン

  • PHP 7.0.x
  • Laravel 5.4

原因

LaravelのCollectionをJSON化してレスポンスする場合、ソートの有無でJSON構造が変わってしまう。

Laravelのソートはキーを維持する

Laravelのコレクションをソートした場合、元々のキーを維持します。

全く認識していませんでしたが、ドキュメントを読み返すと明記されていました。。

The sorted collection keeps the original array keys

Laravel > Documentation > 5.4 > Collections > sort()
https://laravel.com/docs/5.4/collections#method-sort

この仕様はsortBy()でもsortByDesc()でも同じです。


実例を見てみましょう。

サンプルA

ORMからデータを取り出し、そのままdd()します。

$books = $this->book->findByTag($tagId);
dd($books);

この場合は次のように出力されます。

Collection {#362 ▼
  #items: array:5 [▼
    0 => Book {#363 ▶}
    1 => Book {#364 ▶}
    2 => Book {#365 ▶}
    3 => Book {#366 ▶}
    4 => Book {#367 ▶}
  ]
}

サンプルB

次にソートしたものをdd()してみます。

$sortedBooks = $books->sortBy('price');
dd($sortedBooks);

ソートすると次のように出力されます。

Collection {#362 ▼
  #items: array:5 [▼
    1 => Book {#364 ▶}
    3 => Book {#366 ▶}
    0 => Book {#363 ▶}
    2 => Book {#365 ▶}
    4 => Book {#367 ▶}
  ]
}

ソートされ出力順は変化していますが、サンプルA,Bとで「配列の添え字と値の組み合わせが一致」することが確認できます。

JSONのリストとオブジェクト

Laravelでコントローラーからコレクションをreturnすると、データをJSON化してくれます。

Bookontroller
{
    public function findByTag(Request $request, $tagId)
    {
        return $this->bookService->findByTag($tagId);
    }
}

JSON化は、内部的にPHP標準関数のjson_encodeを呼び出していていますが、このjson_encodeはスカラー配列をリストに、連想配列をオブジェクトに変換します。

先のサンプルA、BのコレクションをJSONに変換した場合どのように出力されるでしょうか。

サンンプルAのをJSONにした場合

[
  {id: 1, title: "リーダブルコード", ...},
  {id: 2, title: "Team Geek", ...}
  :
  :
]

サンプルBをJSONにした場合

{
  1: {id: 1, title: "リーダブルコード", ...},
  3: {id: 2, title: "新装改訂 リファクタリング", ...}
  :
  :
}

JSONの外側構造が、Aはリスト、Bはオブジェクトになっています。

わかりやすいように書き直して見ると、こういうことです。

[ {...}, {...} ]
{ 1: {...}, 3: {...} }

PHPはスカラー配列であっても内部的には全て連想配列です。
これに対し、他の多くの言語ではリストとハッシュが別実装であるため、このような手違いが起こるのですね。

解決方法

コレクションのvalues()を実行しキーを振り直すことで解消できます。

Laravel > Documentation > 5.4 > Collections > values()
https://laravel.com/docs/5.4/collections#method-values"

valuesメソッドは、キーをリセットし連続した整数にした新しいコレクションを返してくれます。

ソート呼び出し後にチェーンすればよく、手間ではありますがシンプルな実装は保たれますね。

$sortedBooks = $books->sortBy('price')->values();

まとめ

PHP側でちょっと複雑なソートをする場合はこんなことがありますよという内容でした。
通常「SQLレベルでOrderを済ましてしまう」実装がベターです。

この記事はtomita@atuwebがお届けしました。