RareJob Tech Blog

レアジョブテクノロジーズのエンジニア・デザイナーによる技術ブログです

PHP バージョンアップに伴うコード修正を Bedrock に手伝ってもらう

はじめに

こんにちは、DevOps グループの中島です。
弊社が開発しているサービスでは多くのアプリケーションが PHP で動作しています。そのうち、PHP 7.4 で動作しているアプリケーションを PHP 8.3 に置き換える際に、多くのコード修正が必要になりました。今回は、それらの中でも手で行うと手間のかかる部分について,生成 AI を活用して効率的に行った事例をご紹介します。

前提

アップデート前の主な依存ライブラリのバージョンは以下の通りです。
PHP 7.4, Laravel 6, PHPUnit 8
ここから、執筆時点で最新のバージョンである、
PHP 8.3, Laravel 11, PHPUnit 11
に置き換えます。

Amazon Bedrock とは

Amazon Bedrock は AWS が提供している生成AIサービスです。高精度だと評判の Anthoropic Claude を含めた、様々な言語モデルが API を通じて利用可能です。特徴として、与えた情報が学習に使用されない と明記されていることから、Bedrock を利用することにしました。

今回はコードの修正をお願いしたいので、修正内容を記述したプロンプトとコードを API に渡す必要があります。ファイルからプロンプトとコードを読み込み、変換後のファイルを書き出す Python のコードは以下のように書くことができます。

import json
import boto3

def convert(query, source):
    body = json.dumps({
        "max_tokens": 100000,
        "messages": [{"role": "user", "content": [{"type": "text", "text": source}]}],
        "temperature": 0,
        "system": query,
        "anthropic_version": "bedrock-2023-05-31"
    })
    bedrock = boto3.client(service_name="bedrock-runtime", region_name="us-east-1")
    response = bedrock.invoke_model(body=body, modelId="anthropic.claude-3-5-sonnet-20240620-v1:0")
    response_body = json.loads(response.get("body").read())
    converted = []
    for message in response_body.get("content"):
        converted.append(message.get("text"))
    return ''.join(converted)

# プロンプトとソースコードの読み込み
with open('query.txt') as f:
    query = f.read()
with open('source.php') as f:
    source = f.read()

# コードの修正を依頼
converted = converd(query, source)

# 修正後のコード書き出し
with open('source_out.php', 'w') as f:
    f.write(converted)

以下では必要になった 2 つの修正について紹介します。

修正1: Eloquent の日付キャスト

Eloquent は Laravel が提供している ORM です。Laravel 10 からは dates 変数を用いた日付キャストが廃止になり、casts を利用する方法に統一されました (参考)。 Eloquent の定義ファイルは、読み書きするテーブルの数だけあるのでかなりの量がありました。これを手で書き換えるのは大変なので Bedrock に任せることにしました。

修正前

<?php
protected $dates = [
    'deployed_at',
];

修正後

<?php
protected $casts = [
    'deployed_at' => 'datetime',
];

利用したプロンプト

あなたの仕事は、与えられた PHP のコードを指定の内容で書き換えることです。
以下の変更を加えてください。

<instructions>
1. 文字列の配列である dates 変数が存在する場合、連想配列である casts 変数に変更してください。casts 連想配列の値は 'datetime' とします。
<example1_before>
/** @var string[]  */
protected $dates = [
    'start_date',
    'expire_date'
];
</example1_before>

<example1_after>
/** @var array<string, string> */
protected $casts = [
    'start_date' => 'datetime',
    'expire_date' => 'datetime'
];
</example1_after>
</instructions>

ただし、以下の点に留意してください。
<instructions>
2. 変更の必要がない部分は元のコードを可能な限り維持してください。コメントや PHPDoc も消してはいけません。
3. コードは省略せずすべて出力してください。
4. 解説は不要です。コードだけ出力してください。
</instructions>

↑の 2,3,4 を書かないと修正部分だけが説明付きで出力されてしまいます。 ただ、指定すればちゃんとその通りに出力してくれるのでえらいです。

いくつかのファイルでは必要ないのに勝手に casts を入れられたりしましたが、おおむね想定どおり動きました。

修正2: PHPUnit データプロバイダの static 必須化

PHPUnit 10 からはテストコードで使用するデータプロバイダメソッドを static にすることが推奨されるようになりました (参考) 。また、データプロバイダを指定する方法としてアノテーションではなく PHP 8 で追加されたアトリビュートを利用する必要があります (下記例の #[DataProvider('dataprovider')] の部分がアトリビュート)。さらに、PHP 8, PHPUnit 10 で名前付きパラメータに対応されたことにより、データプロバイダが返すキー名と、テストメソッドの入力パラメータ名が一致させる必要があります。(下記例の requestBodyexpected)

修正後

<?php

/**
 * 正常系
 * @param array $requestBody
 * @param array $expected
 */
#[DataProvider('dataprovider')]
public function test(
    array $requestBody,
    array $expected
) {
    ...
}

/**
 * テストデータ
 * @return array
 */
public static function dataprovider(): array
{
    ...
    $testCases['test'] = [
        'requestBody' => [
            ...],
        'expected' => ...
    ];
    return $testCases;
}

利用したプロンプト

あなたの仕事は、与えられたテストコードを最新の PHPUnit で動作するように書き換えることです。
テストコードは、複数のテストメソッドとそのメソッドの入力となるデータプロバイダメソッドから構成されます。
このテストコードに対して以下の変更を加えてください。

<instructions>
1. データプロバイダメソッドを static にする。
2. もしデータプロバイダメソッドで $this->createApplication が呼ばれていたら削除する。
3. テストメソッドのデータプロバイダアノテーションを削除し、データプロバイダアトリビュートにする。
<example3_before>
/**
 * テストメソッド
 * @param array $request
 * @param array $expectedArgs
 * @dataProvider dataProviderName
 */
 public function testMethod() {}
</example3_before>

<example3_after>
/**
 * テストメソッド
 * @param array $request
 * @param array $expectedArgs
 */
 #[DataProvider('dataProviderName')]
 public function testMethod() {}
</example3_after>

4. テストメソッドの入力パラメータ名とデータプロバイダメソッドの戻り値のキー名が異なる場合、テストメソッドの入力パラメータ名をデータプロバイダメソッドの戻り値のキー名に合わせる。

</instructions>

ただし、以下の点に留意してください。
<instructions>
5. データプロバイダアトリビュートは PHPDoc の後、メソッドの直前に挿入してください。
6. 変更の必要がない部分は元のコードを可能な限り維持してください。コメントや PHPDoc も消してはいけません。メソッド名も変更しないでください。
7. コードは省略せずすべて出力してください。
8. 解説は不要です。コードだけ出力してください。
9. データプロバイダアトリビュートは `use PHPUnit\Framework\Attributes\DataProvider;`を宣言して利用してください。
10. use はアルファベット順に並べてください。
</instructions>

実際には static にできないコードもあるので全てに対応することはできないのですが、これもおおむね満足行く形で動いてくれました。データプロバイダから呼ばれているメソッドも $this-> から $self-> に勝手になって驚きですし、異なる場合のあるパラメータ名 (これまではチェックされずに動いていた) を直すのは修正箇所が多くかなり大変なので助かりました。

注意点

想像以上に役に立ってくれる Bedrock ですが、いくつか問題はありました。

最後は人が確認する必要がある

コメント消すなと散々書いているのに消されたりするので、悲しい気分になりました。小学校の先生のような職業の方は日々大変なんだろうなと感じます。 どのみちプルリクという形になるので最後は人が確認するのですが、その前提で利用する必要があります。

処理が遅い

1 ファイルにつき、処理に 30 秒から 数分はかかります。大量にファイルがあるため自動化されるにしてもあまりにも長い時間待つことになりますが、これは並列で API を呼び出すことで解決することができます。

処理が止まる

コードが 1000 行以上あるような場合、レスポンスが返ってきません。今回は 500 行以下のファイルのみを処理の対象とすることで回避しましたが、うまくやればコードを分割して渡して、後で結合する手法も取れるかもしれません。

おわりに

機械的にコードを生成する手段としては AST を使うのが初めに思い浮かびますが、この取り組みを始めた時点では PHP Parser が PHP 8 に対応していなかったため、せっかくだからと生成 AI に入門してみました。 想像以上にちゃんと動いてくれたので、近い将来 PHP X で動くようにしてほしいとお願いするだけで修正してくれる未来がありそうです。

We're hiring!
弊社では、一緒に働いてくださるエンジニアを募集しています。
rarejob-tech.co.jp