はじめに

Laravel 11から12へのアップグレードに伴い、spatie/laravel-data パッケージもv3からv4へメジャーアップデートした際、APIエラーに遭遇しました。
以前は正常に動作していた箇所での突然のエラーだったため、備忘録として、実際に発生した事象とその原因、および解決策についてまとめます。
内容について、お気づきの点がございましたら、お教えいただけますと幸いです。

前提

  • PHP: 8.2
  • Laravel: 11 → 12
  • spatie/laravel-data: v3 → v4

発生した問題

以前は正常に動作していたAPIを実行すると、突然500エラー(TypeError)が発生するようになりました。
エラーログを確認したところ、以下のような内容が出力されていました。

TypeError: Spatie\LaravelData\Resolvers\TransformedDataCollectableResolver::Spatie\LaravelData\Resolvers\{closure}(): Argument #1 ($data) must be of type Spatie\LaravelData\Contracts\BaseData, array given...

問題のコード箇所は、以下のようにコレクションをDataオブジェクトの配列に変換し、レスポンスに渡す部分でした。「引数1には BaseDataが来るはずなのに、ただの array(連想配列) が渡されている」とPHPが怒っています。

// エラーが起きたコード
$taskValues = $tasks
    ->map(fn($task) => TestTaskValue::fromModel($task))
    ->toArray();

原因究明

このエラーは、以下の2つの仕様が衝突したことで発生していました。

要因1: Laravel Collection::toArray() の仕様

Laravelが提供する toArray() メソッドは、コレクション内の要素が Arrayable を実装している場合、再帰的に中身まで連想配列(array)に変換してしまう仕様があります。
Spatieの Data クラスも Arrayable を実装しているため、オブジェクトの配列ではなく「連想配列の配列」になってしまっていました。
toArray()メソッドの処理 (Laravel公式リポジトリ)

要因2: Spatie laravel-data v4 の厳格な型チェック

JSONシリアライズ時、Spatieの内部処理(TransformedDataCollectableResolver)は、配列の各要素が BaseData オブジェクトであることを厳格に要求します。
しかし、要因1により実際に渡ってきたのはただの array だったため、PHPの型エラー(TypeError)がスローされていました。

解決策

toArray()の使用をやめ、values()->all() を使用するように修正しました。

// 修正後のコード
$taskValues = $tasks
    ->map(fn($task) => TestTaskValue::fromModel($task))
    ->values()->all(); // ここを変更

all() は toArray() とは異なり、要素の再帰的な変換を行わず、生の配列(オブジェクトを保持したままの配列)を返します。
また、間に values() を挟むことで、インデックスを0始まりの連番にリセットし、JSONの配列として正しく振る舞うようにしています。

深掘り:なぜv3では動いていたのか?

「Laravelの toArray() の仕様が変わったわけではないのに、なぜ今になって?」という疑問について考えてみます。
以前の v3の挙動 ではSpatie側が寛容に作られており、配列が渡ってきてもオブジェクトとして解釈・再変換する処理が入っていました。
しかし、今回の v4へのアップデート に伴い、パフォーマンス向上のためこの重い「よしなに変換する処理」が廃止され、正しい型(オブジェクト)が渡されることを前提とした、より厳格なアーキテクチャに変更されています。

この背景について、パッケージ作者の Ruben Van Assche 氏による v4 リリースの公式アナウンスで、変換処理(Transformation)の根本的な書き直しについて以下のように語られています。

“The partials system is constantly being used when transforming data objects or collections into arrays and responses, … it needs to be fast to have a performant application. That’s why we’ve rewritten the whole partials system again! This time, it’s structured much more straightforward, and we’ve ensured it is performant!”
“We used a complex tree-based structure over there, which was complicated to work on, not performant at all… while the more pragmatic solution was far better.”

つまり、v3の時は複雑なツリー構造を用いてデータを「よしなに」解釈していましたが、それがパフォーマンスの致命的なボトルネックになっていました。そのためv4ではその複雑な推論を排除し、よりストレートで高速な(=型に厳格な)システムへと完全に書き直されています。
実際に、公式の GitHub Discussion (#826) においても、パッケージ作者本人が「(toArrayによってすべてが配列化されるのは)期待される挙動であり、all() を使うべき」と明言しています。

まとめ

Laravelフレームワーク本体だけでなく、依存パッケージ(特にSpatieのようなコアな処理を担うもの)のメジャーアップデート時の型チェックの厳格化には注意が必要だと思いました。
また、コレクションを扱う際、中身のオブジェクトを維持したい場合は安易に toArray() を使わず、目的に応じて all() と使い分けることの重要性について、確認して理解しながら実装することが必要だと勉強になりました。