AWS SummitでAWS Transform customを見て、旧PHP FWからLaravel移行の失敗ポイントを思い出した
AWS Summit Japanで AWS Transform custom のセッションを見て、過去に参画した旧PHPフレームワークからLaravelへの移行プロジェクトを思い出しました。
そのプロジェクトでは、AIエディタとしてCursorを使っていました。
Cursorを使ったことで、コードを書く速度は上がりました。単純な置換、Controllerの雛形作成、Repositoryの移植、Viewの変換など、手作業だけで進めるより速くなった部分は確実にあります。
でも、プロジェクト全体として順調だったかというとそうではありませんでした。かなり苦戦しました。
今振り返ると、AIを使う前に決めておくべきことを決め切れていなかった。そのせいで、同じ種類の不具合を何度も出してしまったと思っています。
この記事は成功談ではなく、反省録になります。AWS Transform custom のセッションを見て、反省し整理してみたくなったので。
ある程度抽象化しています。
反省録にはなりますが読んで何か得られれば幸いです。
この記事で言いたいこと
言いたいことは、大きくは次の3つです。
- AIツールを入れる前に、移行後のRepositoryの返却型を決めるべきだった
- Cursorが悪かったのではなく、Cursorに渡す移行ルールが粗かった
- AWS Transform customは移行初期は検討はしても初期導入はしなかったかも。ルール化後の横展開に向いていそう
旧FWからLaravelへ移行するとき、本当に難しかったのは「コードを書くこと」ではありませんでした。
旧FWの暗黙仕様を見つけて、Laravel側の明示的なルールに置き換えること。
ここを甘く見ていたがゆえに、同種のシステムエラーが多発しました。
前提
当時の移行対象は、おおよそ次のような構成でした。
- 旧PHPフレームワークからLaravelへの移行
- 古いPHPバージョンからPHP 8系以降へのバージョンアップ
- Controller -> Repository -> View を中心としたMVC構成
- DBアクセスはLaravel側のORMやQuery Builderを利用
- 移行作業ではCursorを利用
当時は「構成を大きく変えないなら、AIでかなり効率化できるのでは」と考え、チームでのCursor導入が決定されました。
実際、効率化できた部分は多々ありました。
ただ、それ以上に痛感したのはAIに任せる前に「移行後の正解形」を決めておく必要があった、ということです。
一番つらかったのは undefined array key
移行後に一番多かったエラーは undefined array key でした。画面上でシステムエラーになるほとんどの原因がこいつでした。
原因の多くは、DBから取得した値を配列として扱うべきか、オブジェクトとして扱うべきかが揺れていたことです。
たとえば、ある箇所ではこう書かれているとします。
$name = $row['name'];
別の箇所ではこう書かれているとします。
$name = $row->name;
見た目としては、本当に小さな差分です。
でも、移行プロジェクトではこの小さな違いがかなり大きな問題になります。
LaravelのQuery Builderでは、DB::table(...)->get() はCollectionを返し、その各要素は stdClass として扱われます。first() も単一の stdClass を返します。
一方で、旧FW側では、DBアクセスの結果が配列だったり、単一行だったり、ORM由来のオブジェクトだったりしました。
典型的には、次のような取得パターンが混在していました。
// 複数行を配列で取得する
$rows = $query->execute()->as_array();
// 先頭1行を取得する
$row = $query->execute()->current();
// ORM由来のオブジェクトとして扱う
$model = Model_User::query()->where('id', $id)->get_one();
Laravel側でも、Eloquent Model、Collection、Query Builderの stdClass、toArray() 済みの配列が混ざります。
つまり、移行後のコードには次のような型が混在しやすくなります。
// 配列
$user['email'];
// stdClass
$user->email;
// Eloquent Model
$user->email;
// Collection
$users->map(...);
// EloquentのtoArray()済み
$user['email'];
この状態でCursorに「このRepositoryをLaravelに移行して」と依頼すると、Cursorは周辺コードを見ながらもっともらしい変換をしてくれます。
ただし、周辺コード自体に配列アクセスとオブジェクトアクセスが混在している場合、AIの出力も揺れます。
結果として、$row['name'] と $row->name が混ざりました。これがほんっっっとうに多発しました。
そして、undefined array key が出たり、エラーにはならないけれど期待通りに値が入っていない、という不具合が出ました。
この手の不具合はレビューでは見つけづらかったです。ぱっと見たコードはLaravelっぽく見えるからです。
画面操作の挙動をすべて確認すべきだとは思うのですが、特定の条件下でしか分岐しない処理であったり、そのためのテストデータを作るのに苦労したり、前提の処理がないためうまくデータが作れなかったりetc...まあ、言い訳になるのですがかなり大変だった記憶です。
toArray() で解決!にはならなかった
移行時に特に厄介だったのは、toArray() という名前が安心感を与えてしまうことです。
たとえば、旧FW側に次のようなコードがあったとします。
$rows = $query->execute()->as_array();
foreach ($rows as $row) {
$emails[] = $row['email'];
}
これをLaravelへ移すとき、単純な移行ルールとしては次のように書きたくなります。
$rows = DB::table('users')->get()->toArray();
foreach ($rows as $row) {
$emails[] = $row['email'];
}
しかし、ここが落とし穴でした。
LaravelのQuery Builderで get() した結果はCollectionで、その各要素は stdClass です。get()->toArray() と書いても、期待しているような「連想配列の配列」になっていないケースがあります。
そのため、次のようにアクセスしないといけない場面があります。
foreach ($rows as $row) {
$emails[] = $row->email;
}
一方で、Eloquent Modelの toArray() は属性が配列化されます。
つまり、同じ toArray() でも、Query Builder由来なのか、Eloquent由来なのかで、後続コードの書き方が変わります。
実際の移行後コードでも、stdClass を純粋な配列に変換するための補助関数が使われていました。これはかなり現実的な対応です。
// Query Builderの結果を、Viewや既存処理に渡しやすい配列へ正規化するイメージ
$rows = DB::table('users')->get();
$rows = ResultNormalizer::toArray($rows);
このような正規化関数を用意すること自体は悪くありません。
ただし、「どの層で必ず正規化するのか」「正規化後は配列アクセスだけを許可するのか」「Eloquent Modelをそのまま返す例外を許すのか」まで決めないと、結局揺れます。
当時の自分たちは、ここをもっと早い段階で移行ルールにしておく必要がありました。
Cursorが悪かったのか
当時はCursorを使っていました。
Cursorを使うと、単純な置換やLaravelらしい書き換えはかなり速く進みます。
Controllerの書き換え、Repositoryの雛形作成、Viewの変換など、手作業だけでやるより速い場面は確実にありました。
ただ、実際には $row['name'] と $row->name のズレが散見されました。
これを振り返って、チーム内では「AIに頼りすぎて、セルフレビューが弱かったのではないか」という反省が出ました。
これはかなり本質的だったと思います。
Cursorが悪かった、という話ではありません。
問題は、Cursorに渡す前の移行ルールが曖昧だったことです。
実際、Cursor rulesのような移行ルールは作っていました。旧FWのクエリビルダーをLaravelのQuery Builderへ置き換える、バリデーションをFormRequestへ寄せる、テンプレートをBladeへ変換する、といったルールです。
なので、「ルールがまったくなかった」という話ではありません。
ただ、いま振り返るとそのルールはAPI対応表に寄りすぎていました。
execute()->as_array() を get()->toArray() に変換する、というAPI対応表だけでは不十分です。
本当に必要だったのは、次のようなルールでした。
- Repositoryは何を返すのか
- 配列で返すのか、Modelで返すのか、DTOにするのか
-
toArray()や独自の配列変換はどの層で呼ぶのか - ControllerやViewに渡す時点で型をそろえるのか
- 配列アクセスとオブジェクトアクセスのどちらを許可するのか
- nullや空配列はどの層で吸収するのか
このルールが曖昧なままAIに変換させると、AIは「その場では自然に見えるコード」を出します。
AIは曖昧な方針を補完してくれます。ただし、補完結果が正しいとは限らない。
ここを軽く見ていました。
ただ今の性能だと、それすらも調整してくれる可能性はあるかもなので、今後検証していきたいです。
Repositoryの返却型を最初にそろえておけばよかった
一番の反省はここです。
Repositoryの返却型を、移行の最初にそろえておけばよかった。
たとえば、次のどれかに明確に寄せる必要がありました。
// 方針A: RepositoryはEloquent Modelを返す
public function findById(int $id): ?User
{
return User::query()->find($id);
}
// 方針B: Repositoryは配列を返す
public function findById(int $id): ?array
{
$user = User::query()->find($id);
return $user?->toArray();
}
// 方針C: RepositoryはDTOを返す
public function findById(int $id): ?UserDto
{
$user = User::query()->find($id);
if ($user === null) {
return null;
}
return new UserDto(
id: $user->id,
email: $user->email,
name: $user->name,
);
}
どれにするかは最初に決めておく必要があります。
「とりあえず動くように移す」「既存コードに合わせる」「AIが自然に変換したものをレビューする」という進め方だと、Repositoryの返却型がじわじわ揺れます。
そして、その揺れはControllerやViewに伝播します。
最終的には返却型を統一したはずです。
ただ、そこにたどり着くまでに手戻りが出ました。移行の序盤からここを固定できていれば、かなり違ったはずです。
RepositoryはDBアクセスだけではなかった
もう1つ、実コードを振り返って引っかかったのは、Repositoryが単なるDBアクセス層ではなかったことです。
RepositoryがDBから取得した値をそのまま返すだけなら、返却型の統一はまだやりやすいです。
しかし実際には、RepositoryがViewへ渡すためのデータを組み立てる役割も持っていました。
イメージとしては、次のような処理です。
$rows = DB::table('users')->get();
foreach ($rows as $row) {
$this->data['users'][] = [
'id' => $row->id,
'name' => $row->name,
'email' => $row->email,
];
}
foreach ($this->data['users'] as $index => $user) {
$this->data['users'][$index]['label'] = $user['name'].'さん';
}
前半では stdClass をオブジェクトアクセスで読んでいます。
後半では、View用に詰め直した配列を配列アクセスで読んでいます。
このようなコードは局所的には自然に見えます。
しかし、移行ルールとして見るとかなり危険な状態です。
「DBから取得した直後は stdClass」「Viewへ渡す配列に詰め直した後は配列」という境界が明文化されていないとAIも人間も間違えます。特に人間側の実装者、レビュー者の脳内コンテキストがあふれて、読解に時間がかかります。
RepositoryがDBアクセス層であり同時にViewModel組み立て層でもある場合は、返却型だけでなく、$this->data に積むデータのshapeまで決めておく必要がありました。
たとえば、以下のようにできたかなと。
- Query Builderの戻り値を直接Viewへ渡さない
- Viewへ渡す一覧は必ず配列の配列に正規化する
- Viewへ渡す連想配列のキーはRepository内で定義する
- Viewでは原則として配列アクセスだけを使う
- Repository内でDB行を読むときだけ `$row->field` を許可する
これくらい書いておけば、$row['name'] と $row->name のズレはかなり減らせたはずです。
PHPバージョンアップも同時だったのが効いた
もう1つ大きかったのは、Laravel移行と同時にPHPバージョンアップもしていたことです。
移行前は古いPHPのバージョンでした。移行後はPHP 8系以降です。
PHP 8系では、未定義変数、未定義array key、nullに対するアクセスなど、古いPHPでは見逃されがちだった問題が表面化しやすくなります。
そのため、移行後に出たエラーが「Laravel移行のせい」なのか、「PHPバージョンアップで潜在バグが顕在化したもの」なのか切り分けが難しくなりました。
たとえば、次のようなコードです。
$label = $row['label'];
旧環境ではたまたま気づかなかったとしても、PHP 8系以降では未定義キーとして表面化します。
本来は、次のようにデータの存在を明示的に扱う必要があります。
$label = $row['label'] ?? '';
または、そもそもRepositoryやDTOの段階で必須項目と任意項目を分けておく方が安全です。
final class UserDto
{
public function __construct(
public readonly int $id,
public readonly string $email,
public readonly ?string $nickname,
) {
}
}
Laravel移行とPHPバージョンアップを同時に行う場合、単純な「FW移行」ではなく「過去に見逃されていた曖昧なデータアクセスを洗い出す作業」にもなりました。
ここも見積もり上は軽く見ていたと思います。
before処理をそのまま移そうとして苦戦した
旧FWでは、各Controllerの前処理として before のような仕組みがありました。
認証、権限チェック、共通データの取得、Viewに渡す共通変数、初期化処理、ログ、リダイレクト制御などが、そこに集まっていました。
Laravel移行時には、それをLaravelのControllerのコンストラクタ相当へそのまま移そうとして苦戦した記憶があります。
実コードを振り返ると、Laravel側にも互換用の before() 呼び出しが残っていました。
つまり、「旧FWのbeforeをLaravelのconstructorへきれいに移した」というより、「旧FWのライフサイクルをLaravel側で再現するための互換層を作った」に近い状態です。
これは、当時の状況を考えるとかなり現実的な落とし所だったと思います。
ただ、記事として振り返るなら、ここは「移行できた」ではなく「旧FWの考え方をLaravel側へ持ち込んだ」と見た方がよいかなと思います。
イメージとしては、middlewareからControllerの before() を呼び、必要ならリダイレクトを返すような構成です。
if ($controller && method_exists($controller, 'before')) {
$beforeResponse = $controller->before($request);
if ($beforeResponse instanceof RedirectResponse) {
return $beforeResponse;
}
}
これは段階移行としては現実的な判断です。
旧FWのbeforeに多くの責務が集まっている場合、最初からすべてをLaravel標準のmiddleware、Policy、FormRequest、View composerへ分解するのはかなり大変です。
最終形として残すなら、どの処理を互換beforeに残し、どの処理をLaravel標準へ移すのかを決める必要があります。
Laravelでは、責務ごとに置き場所を分けた方が自然です。
| 旧FWのbeforeにありがちな処理 | Laravelでの置き換え候補 |
|---|---|
| 認証チェック | middleware |
| 権限チェック | middleware / Policy / Gate |
| 入力値の検証 | FormRequest |
| 共通View変数 | View composer / Service Provider |
| 初期データ取得 | Controller / Service |
| リダイレクト制御 | middleware / Controller |
| アクセスログ | middleware / event listener |
もちろん、移行プロジェクトでは時間の制約があります。
きれいに分解するよりも、まず動かすことを優先する判断もあります。
ただ、旧FWのbefore処理は、複数の責務が混ざりやすい場所です。
ここをそのままLaravelに持ち込むと、Laravel側のライフサイクルと合わず、あとから整合性で詰まります。
AIに変換させる場合も同じです。
「beforeをLaravelに移して」では不十分です。
少なくとも、次のように分解して依頼した方がよかった。
旧FWのbefore処理をLaravelへ移行する。
- 認証チェックはmiddlewareへ移す
- 権限チェックはPolicyまたはmiddlewareへ移す
- View共通変数はView composerへ移す
- Controllerごとの初期データ取得はControllerまたはServiceへ残す
- リダイレクトを返す処理はconstructorへ入れない
- 互換beforeを残す場合は、残す責務と撤去予定を明記する
これくらい明確にしないと、AIは「それっぽく動くコード」を作ります。
そして、そのコードがLaravelとして保守しやすいとは限りません。
これに関しては、工数の関係からみて完全にLaravelの書き方へ移行する、というよりはbefore処理をある程度引き継ぐ形で残したのがベターでよかったのかなと考えています。
もちろん完全にLaravelの書き方へ寄せられた方が保守性は高まると思うのですが、それよりも各機能の移行に工数を多く取られたので、、、ベストではないですが、現実的にはベターな落とし所だったのかなと考えています。
Viewの移行も構文変換だけではない
Viewも同じです。
実コードを振り返ると、画面構造は大きく変えず既存のテンプレート階層をかなり保ったままBladeへ移していました。
これは移行としては自然です。画面の見た目や動線を変えないことが優先なら、最初からBlade Component化や設計整理までやるより、既存の extends / include 構造を保った方が安全です。
ただ、テンプレート移行で怖いのは、拡張子や構文の変換ではありません。
旧テンプレートエンジンが暗黙に吸収していた仕様が、Bladeで表面化することです。
特に大きいのは、次の3つです。
- 配列とオブジェクトの扱い
- 未定義変数、未定義キーへの寛容さ
- エスケープの前提
user.name
この見た目だと、user が配列なのかオブジェクトなのかをテンプレートだけから判断しづらいです。
Bladeへ移すと、その曖昧さは次のどちらかを選ぶ問題として表面化します。
{{ $user['name'] }}
{{ $user->name }}
Repositoryの返却型が揺れていると、この判断をViewごとにAIへ任せることになります。
結果として、画面ごとに配列アクセスとオブジェクトアクセスが混在しました。
ここも地味ではありますが、もちろん不具合になります。
画面は表示される。けれど一部の値だけ出ない。条件によってだけエラーになる。そういう形で出てきます。
また、旧view側では未定義値やエスケープの挙動がBladeと違う場合がありました。
Bladeの {{ }} はエスケープされます。旧View側でHTMLをそのまま出していた箇所を単純に {{ }} へ置き換えると、安全側には倒れますが、表示崩れが起きることもあります。
逆に、何でも {!! !!} にすればよいわけでもありません。
{{-- 通常の文字列 --}}
{{ $title }}
{{-- HTMLとして表示する必要がある値。使うなら入力元とサニタイズ方針を確認する --}}
{!! $bodyHtml !!}
View移行で見るべきなのは、構文だけではありません。
- エスケープ済みか
- 未エスケープ出力をしていないか
- 共通部品のinclude方法
- URL生成
- フォーム生成
- CSRF
- バリデーションエラー表示
- old値の扱い
- null時の表示
- 配列アクセスかオブジェクトアクセスか
特に、手組みのhidden tokenや独自のエラー配列を、Laravelの @csrf、old()、@error に寄せるかどうかは、View移行の独立した論点として扱った方がよかったです。
@csrf
<input type="text" name="email" value="{{ old('email', $email ?? '') }}">
@error('email')
<p>{{ $message }}</p>
@enderror
View移行の前に「ControllerからViewへ渡すデータの形」を決めておけばよかった。
これも大きな反省です。
AI rulesに書いておけばよかったこと
Cursor、Codex、ClaudeのようなAIコーディングツールを使う場合、rulesや AGENTS.md、CLAUDE.md に移行ルールを書いておく必要があります。
当時の自分に渡すなら、たぶん次のように書きます。
# 旧PHP FWからLaravelへの移行ルール
## Repository
- Repositoryの返却型を勝手に変更しない
- 新規に移行するRepositoryメソッドは返却型を明示する
- 配列アクセスとオブジェクトアクセスを混在させない
- `toArray()` を呼ぶ場合はRepository内に閉じ込める
- ControllerやViewで場当たり的に `toArray()` しない
- Query Builderの戻り値をViewへ直接渡さない
- Viewへ渡す一覧は必ず配列の配列へ正規化する
## DB取得結果
- Query Builderの結果は `stdClass` として扱う
- Eloquentの結果はModelとして扱う
- Query Builder由来の `get()->toArray()` を「純粋な配列化」とみなさない
- Viewへ渡す前に必要なら配列またはDTOへ正規化する
- `$row['key']` と `$row->key` を周辺コードだけで判断しない
## null / 空 / 未定義キー
- 未定義キーを前提にしたアクセスをしない
- 任意項目は `??` またはDTOのnullable型で明示する
- 空文字、null、空配列の意味を勝手に変えない
## before相当の処理
- before処理をController constructorへそのまま移さない
- 認証はmiddlewareへ移す
- 入力検証はFormRequestへ移す
- 共通View変数はView composerを検討する
- 互換beforeを残す場合は、残す責務と撤去予定を書く
## View
- 旧ViewをBladeへ移すとき、配列アクセスかオブジェクトアクセスかを必ず確認する
- HTML出力は `{{ }}` と `{!! !!}` を機械的に置換しない
- `@csrf`、`old()`、`@error` へ寄せるかを画面単位で判断する
- Viewへ渡るデータshapeをRepositoryまたはControllerで固定する
このレベルまで書いておけば、AIの出力はもう少し安定したはずです。
少なくとも、レビュー時に見るべき観点が明確になります。
当時は「AIが書いたコードをレビューする」という意識はありました。
しかし、レビュー観点が十分に言語化されていませんでした。
結果として、ぱっと見でLaravelらしいコードになっていると、細かい返却型のズレを見落としやすくなりました。
これは「レビュー不足」と言えばその通りです。
ただ、レビューする側も何を見るべきかが明文化されていないと、どうしても構文や命名、動きそうかどうかに目が行きます。
本当に見るべきだったのは、そのメソッドが何を返しその値がViewまでどう流れるかだったのかなと思います。
AWS Transform customは救世主足り得たのか
AWS Summitのセッションで見た AWS Transform custom は、コード、API、フレームワークなどの変換を、AIエージェントで大規模に進めるための仕組みです。
AWSの公式ページでは、自然言語、ドキュメント、コードサンプル、開発者フィードバックから変換を学習しCLIやWeb体験を通じてローカルコードベースや複数リポジトリに変換を適用できる、と説明されています。
また、ユースケースには、バージョンアップ、ランタイム/API移行、フレームワーク移行、言語変換、アーキテクチャ分解などが挙げられています。
では、当時のプロジェクトでAWS Transform customを使っていれば救えたのか。
正直に言うと、最初から使っても難しかったのかなあ、、、と思います。
理由は、移行初期の段階では「何を正解とするか」がまだ曖昧だったからです。
たとえば、Repositoryの返却型をどうするかが決まっていない状態でどれだけ強力な変換ツールを使っても、正しい変換は定義できません。
AIにとって一番困るのは、作業量が多いことではなく正解が曖昧なことです。(まあ、人間にとってもそうかもですが。)
これは、Cursorを使った経験からもかなり実感があります。
曖昧なルールのまま変換速度だけを上げると、曖昧さも一緒に広がります。
一方で、次のような状態まで持っていけていれば、AWS Transform customはかなり相性がよかった可能性があります。
- Repositoryの返却型を統一済み
-
execute()->as_array()をどう正規化するか決まっている -
execute()->current()をLaravel側でどう扱うか決まっている - Query Builder由来の
stdClassをどこで配列化するか決まっている - 代表的なController移行パターンがある
- 代表的なRepository移行パターンがある
- Viewへ渡すデータ構造が決まっている
- before処理の分解ルールが決まっている
- 旧ViewからBladeへの変換ルールとエスケープ方針が決まっている
- AIレビュー観点が明文化されている
- 変換前後のサンプルが複数ある
つまり、AWS Transform customは「移行のための最強エージェント」ではなく、「移行ルールが固まった後の横展開」に向いているのだと思います。
Cursor、Codex、Claudeは、調査や個別移行、レビュー、方針検討に向いています。
AWS Transform customは、定義済みの変換を繰り返し適用し、組織的に展開する用途に向いています。
ざっくり分けると、こういう使い分けです。
| ツール | 向いている場面 | 注意点 |
|---|---|---|
| Cursor | 開発者が手元で調査、修正、個別移行を進める | rulesが弱いと出力が周辺コードに引っ張られる |
| Codex | リポジトリ全体を見ながら調査、実装、レビューを進める | プロジェクトの移行ルールを AGENTS.md などに明文化した方がよい |
| Claude | 設計相談、移行ルールの整理、実装補助 |
CLAUDE.md などで前提を固定しないと判断が揺れる |
| AWS Transform custom | 固まった変換パターンを大規模に横展開する | 変換定義が曖昧な段階では効果を出しにくい |
当時の自分たちに必要だったのは、最初から一気に自動変換することではありませんでした。
まず代表パターンをいくつか移行し、そこで得た知見をルール化することでした。
料金面でも、まず小さく測るべき
AWS Transform customの料金は、公式ページ上では agent minute 単位です。
2026年6月確認時点の公開情報では、Custom transformation agentは有料で、価格は 1 agent minute あたり $0.035 とされています。agent minuteは、エージェントが変換タスクに対して実際に作業している時間に対して発生し、ローカルマシン上のビルドやテスト実行待ち時間は課金対象外と説明されています。
ただし、ここで見たいのは「安いか高いか」だけではありません。
FW移行で本当に高くつくのは、変換そのものよりも変換後の手戻りです。曖昧な変換が多いと手動確認の後半で不具合がまとめて見つかります。
終盤に修正工数が膨らみました。結果として当初想定していたコスト感から外れていく。
経験としては、こちらの方がずっと重いです。
だから、AWS Transform customを検討する場合も、最初から全体適用するのではなく、代表的な数リポジトリや数画面でagent minuteと修正量を測るのがよさそうです。
見るべきなのは、単なる実行料金ではありません。
- 1変換あたりのagent minute
- AI出力後の人間レビュー時間
- 手修正が必要だった件数
-
undefined array keyのような型ズレがどれだけ残ったか - 手動確認の後半で見つかった不具合数
- 同じ変換を横展開したときに精度が上がったか
ここまで見て初めて、Cursor中心で進めるのか、CodexやClaudeを併用するのか、AWS Transform customへ寄せるのかを判断できます。
ツール比較ではなく、使う順番の問題だった
今なら、次の順番で進めたいです。
- 旧FWのController、Repository、Viewの代表パターンを棚卸しする
- Repositoryの返却型を決める
- Query Builder、Eloquent、配列変換、DTOの境界を決める
- null、空、未定義キーの扱いを決める
- before相当の処理をLaravelのどこへ移すか決める
- 旧ViewからBladeへの移行方針とエスケープ方針を決める
- 代表5から10パターンを人間とAIで移行する
- 移行ルールをMarkdown化する
- Cursor/Codex/Claudeのrulesやinstructionsに落とす
- 変換パターンが固まったものだけ自動化やAWS Transform customを検討する
- AI出力を返却型、null、before処理、View表示の観点でレビューする
特に自動テストが十分でない場合、AIによる移行は慎重に進めるべきです。
手動確認の後半だけだと、画面や業務フローで初めて不具合が見つかります。
その時点で undefined array key やデータ不整合が大量に出ると、原因調査と修正にかなり時間を使います。
AIを使うとコードを書く速度は上がります。
ただし、レビュー観点や移行ルールが弱いままだと、不具合を作る速度も上がります。
まとめ
旧PHPフレームワークからLaravelへ移行して、一番痛感したのは次のことです。
AIツールを選ぶ前に、移行後の正解形を決める必要がありました。
特に重要だったのは、Repositoryの返却型です。
$row['name'] で読むのか、$row->name で読むのか。
この小さな差分が、移行プロジェクトでは大きな手戻りになります。
Cursor、Codex、ClaudeのようなAIコーディングツールは強力です。
調査、変換、レビュー、ルール作成に使えます。
ただし、プロジェクトとしての移行ルールが曖昧なまま使うと、AIは曖昧さごとコードに反映します。
AWS Transform customのような仕組みも同じだと思っています。
移行ルールが曖昧な段階では、最初から一気にFW移行を任せるのは危険です。
必要だったのは「もっとAIに任せること」ではなく、AIに任せる前に移行の正解を人間が言語化することだったと思います。
AIで速く書く前に、何を正解にするのかを決める。その順番を間違えると速く進んでいるように見えて、あとでまとめて返ってきます。超絶実体験です。
このプロジェクトをやったのは少し前なので、今ならrules / skills / playbookとかを整備して、Cursor / Codex / Claudeで移行の明文化することでより正確に対応できたのかな、という気もしています。
今後はAIエージェントのワークフローの整備(everything-claude-codeみたいな)とかも勉強しつつ、プロジェクトに合わせたAI活用を進めていってみたいなと強く思いました。