【Laravel】大規模開発プロジェクトでも流行りに乗りたい!Laravel 6からLaravel 9への移行と対応。

【Laravel】大規模開発プロジェクトでも流行りに乗りたい!Laravel 6からLaravel 9への移行と対応。

はじめまして。サーバーサイドエンジニアの水谷です。
現在は新規プロジェクトの開発に携わっています。

私が現在携わっているプロジェクトではPHP / Laravelを採用しており、
2月上旬にLaravel 9がリリースされたので、バージョンアップ対応を行いました。
今回は対応内容の一部を紹介したいと思います。

バージョンアップの背景

近年のスマートフォンゲームはリリース時点でクオリティの高いものが多く、
準備・開発に年単位の時間がかかっているものも多く存在します。

リリースまでに時間がかかる分、日々進化していく言語やフレームワークを追従するために、
バージョンアップ対応が必要になってきます。
私が現在のプロジェクトに参加した時点ではPHP 8.0 / Laravel 6でしたが、

フレームワークの新機能を使いたい。
・アップデートを行うことで、バグ修正やセキュリティ対応の恩恵を受けることができる
Laravel 6のセキュリティ対応は2022年9月までのため、早めにアップデートを行いたい。
・まだアプリがリリースされていないため、リリース前の今のほうがアップデート関連のリスクが少ない

等の理由で、バージョンアップ対応を行うことになりました。

環境情報

・アップデート対応前 : PHP 8.0 / Laravel 6
・アップデート対応後 : PHP 8.1 / Laravel 9
・ローカル環境は、開発メンバー間で差が出ないようにdockerを用いて環境を構築しています。
※ 記事内容に、dockerを通して実行するコマンドで記述している部分があります。

環境情報についての補足

PHPのバージョンが8.0 → 8.1に上がっていますが、一部のパッケージをPHP 8.1対応のものにアップデートしただけなので、
今回の記事では、PHPのバージョンアップ関連については省略いたします。

大まかな作業方針

developブランチには新機能の追加や修正対応が頻繁に行われており、高頻度で更新されていきます。
自分のブランチでバージョンアップの対応を行っても、別のPRがマージされてコンフリクトが起きたり、
新しく追加された機能に対して追加で修正が発生する可能性があります。

そこで、大まかな方針として、下記内容で進めることになりました。

  • 極力コンフリクトが起きないような対応を行う(暫定対応を行ったものは、後ほど修正する)。
  • 一括で置換が必要な部分は、チーム全体にタイミングを共有して、developへのマージが行われないタイミングで作業を行う。
  • developにマージされていない開発中の機能でも修正対応が必要になるため、補助的なコマンドやスクリプトを用意する。

また、バージョンアップ対応を行う判断をした時点ではLaravel 9がリリースされていなかったため、
2回に分けてアップデート対応を行うことにしました。

  1. 当時の最新はLaravel 8のため、一旦Laravel 6 → Laravel 8に上げる。
  2. Laravel 9がリリースされるタイミングで、Laravel 8 → Laravel 9に上げる。

Laravel 6からLaravel 8へ

具体的な作業

  1. Laravelのバージョンアップと共にパッケージを刷新し、環境を作り直す。
  2. Laravel 8を適用した状態で、テストがどの程度通っているか確認する。
  3. laravel/legacy-factoriesを導入する。
  4. テストやphpstanで出ているエラーを修正していく。
  5. Factory以外にも置換が必要なものは、最後に一括で作業を行う。
  6. developへマージして動作確認を行う。
  7. 開発チーム全体に対して、ローカル環境で必要な対応の説明を行う。
1. Laravelのバージョンアップと共にパッケージを刷新し、環境を作り直す。

まずは各パッケージのバージョンを上げていきます。
アップグレードガイドにある通りにバージョンを上げました。

readouble.com

コマンドでパッケージのアップデートを行います。

$ docker-compose exec container env COMPOSER_MEMORY_LIMIT=2G composer update
2. Laravel 8を適用した状態で、テストがどの程度通っているか確認する。

一旦現状を把握したいので、テストだけ実行してみます。

$ docker-compose exec container phpunit

結果

Tests: 1764, Assertions: 1147, Errors: 1298, Failures: 251.

ただパッケージを更新しただけなのでエラーの数は多いですが、同じ内容のエラーもあるはずです。
順番に修正していきます。

3. laravel/legacy-factoriesを導入する。

Laravel 8の変更点の一つとして、モデルファクトリの構造が大きく変わりました

readouble.com

クラスをサポートする形で大きく書き換えが入っていますが、laravel/legacy-factoriesを利用することで、
以前のFactoryをそのまま使用することができます。

後ほどlaravel/legacy-factoriesは取り除きますが、一次対応としてFactoryが動く状態まで持っていきます。

4. テストやphpstanで出ているエラーを修正していく。

Factoryが動く状態になったので、テストのエラーを修正していきます。
今回は影響の大きかった修正と対応策について、一部抜粋して紹介していきます。

4 - 1. Illuminate/Database/Eloquent/Concerns/HasAttributes.phpについて

Laravel 7で入った変更ですが、getOriginalメソッドの挙動が変わりました。

readouble.com

getRawOriginal が同じ挙動として残っているので、こちらを使用する形に修正したいと思います。

ソースコード内にgetOriginal は多数使用されていました。
このタイミングで一括置換しても、別の誰かが新機能を開発した際に、追加で修正が必要になってしまう可能性があります。

そこで、暫定対応として、HasAttributesをuseしている基底クラスでオーバーライドすることにしました

<?php

public function getOriginal($key = null, $default = null)
{
    return $this->getRawOriginal($key, $default);
}

これにより、新しくdevelopブランチを取り込んだ際にgetOriginalが使用されていても、
コンフリクトを回避しつつgetRawOriginalの動きを表現できます。

ただ、この様な形でgetOriginalをオーバーライドしてしまうと、
フレームワーク側で使用されているgetOriginalの挙動が変わってしまうため、
別の部分でエラーが出てしまう可能性があります。
あくまでも暫定対応で、最終的にはコード全体で置換対応を行い、このオーバーライドの記述は削除します

4 - 2. rectorでFactoryの置き換えを行う

とりあえず動く状態にするためにlaravel/legacy-factoriesを導入しましたが、
本格的にFactoryをクラスの形式に書き換えていきます。

ただ、Factoryの数も多いため、手動で一つ一つ書き換えていくのは現実的ではありません。
何かいい方法は無いかと調査していたところ、rectorが書き換えをサポートしているのを発見しました。

github.com

rector.phpに下記2つの設定を入れて、コマンドで書き換えができる状態を作ります。
FactoryFuncCallToStaticCallRector
FactoryDefinitionRector

<?php

use Rector\Laravel\Rector\FuncCall\FactoryFuncCallToStaticCallRector;
use Rector\Laravel\Rector\Namespace_\FactoryDefinitionRector;

return static function (ContainerConfigurator $container_configurator): void {
    $services = $container_configurator->services();

    $services->set(FactoryDefinitionRector::class);
    $services->set(FactoryFuncCallToStaticCallRector::class);
};

コマンドで既存のFactoryを書き換えることができる状態になりました。

$ docker-compose exec container vendor/bin/rector process database
4 - 3. Factoryの置き換えに必要なスクリプトを用意する

rectorだけでは全てをうまく置き換えることができませんでした。
既存のFactoryをある程度うまく書き換えてくれたのですが、名前空間が抜けていました

そこで、database配下のファイルに名前空間を追加するスクリプトを用意することにしました。
database/factories配下にadd_namespace.phpを用意し、各ファイルにnamespaceを追加するスクリプトを作成します。
ファイルの先頭にdeclare(strict_types=1)の記述があるため、これをうまく利用するスクリプトです。

開発メンバーにもスクリプトは共有しますが、各メンバーは自分のブランチで作成したFactoryのみ手動で編集すればよいので、
このスクリプトはgitにコミットしません。
一括で置換対応を行う際に1回だけ使用するスクリプトなので、ベタ書きで実装しました。

<?php

function getFileList($dir)
{
    $files = scandir($dir);
    $files = array_filter($files, function ($file) {
        return !in_array($file, ['.', '..']);
    });
    $list = [];
    foreach ($files as $file) {
        $fullpath = rtrim($dir, '/') . '/' . $file;
        if (is_file($fullpath)) {
            $list[] = $fullpath;
        }
        if (is_dir($fullpath)) {
            $list = array_merge($list, getFileList($fullpath));
        }
    }
    return $list;
}

$file_name_list = getFileList('.');

foreach ($file_name_list as $file_name) {
    if (strpos($file_name, 'add_namespace.php') !== false) {
        continue;
    }
    $namespace = str_replace('./', '', dirname($file_name));
    $namespace = str_replace('/', '\\', $namespace);
    $contents = file_get_contents($file_name);
    if (strpos($contents, 'namespace') !== false) {
        continue;
    }
    $contents = str_replace('declare(strict_types=1);', "declare(strict_types=1);\n\nnamespace Database\Factories\\" . $namespace . ';', $contents);
    file_put_contents($file_name, $contents);
}
5. Factory以外にも置換が必要なものは、最後に一括で作業を行う。

他にも細かい修正はたくさんありましたが、テストが全部通る状態まで持っていった後は、
developにマージするための準備に入ります。

予め開発メンバー内で相談した時間帯はdevelopへのマージが止まっているので、最新のdevelopを取り込み、
Factoryの置換対応やソースコード内で暫定対応していた部分を修正していきます。

このタイミングでソースコード内のgetOriginalを全てgetRawOriginalに置換し、
オーバーライドの記述を削除しました。

6. developへマージして動作確認を行う。

開発メンバーからレビューをもらい、必要な確認を順番に行っていきます。

7. 開発チーム全体に対して、ローカル環境で必要な対応の説明を行う。

ローカル環境を使用している開発メンバー全員に対し、アップデートに伴う変更点と更新に必要な手順をまとめて共有します。
パッケージの更新作業やrectorのコマンド、必要であればFactory書き換えのスクリプト実行など、
各メンバーの調査コストを減らすために、順番にコマンドを実行すればアップデートが終わるように資料を用意しました。

Laravel 8からLaravel 9へ

2022年2月上旬に、Laravel 9がリリースされました。
PHP 8.0がシステム要件となり、Symfonyが6系にアップデートされました

具体的な作業

対応内容は変わりますが、大まかな作業手順としてはLaravel 6からLaravel 8へアップデートしたときとほぼ同じです。

  1. Laravelのバージョンアップと共にパッケージを刷新し、環境を作り直す。
  2. Laravel 9を適用した状態で、テストがどの程度通っているか確認する。
  3. テストやphpstanで出ているエラーを修正していく。
  4. developへマージして動作確認を行う。
  5. 開発チーム全体に対して、ローカル環境で必要な対応の説明を行う。
1. Laravelのバージョンアップと共にパッケージを刷新し、環境を作り直す。

Laravel 9が正式にリリースされる前に、2022年1月末の時点で9.x-devが使えそうな状態だったので、
こちらを取り込んで進めることにしました。
Laravel 9(9.x-dev)を導入しインストールしようとしましたが、早速問題が発生しました。

1 - 1. illuminate 9系に対応していないパッケージがある

使用しているパッケージの中に、illuminateに依存しており、illuminate 9系に対応していないものがありました
2022年1月の段階ではLaravel 9はリリースされていないため、当然といえば当然です。

Problem 1
- aws/aws-sdk-php-laravel 3.6.0 requires illuminate/support ^5.1 || ^6.0 || ^7.0 || ^8.0 -> found illuminate/support[v5.1.1, ..., 5.8.x-dev, v6.0.0, ..., 6.x-dev, v7.0.0, ..., 7.x-dev, v8.0.0, ..., 8.x-dev] but it conflicts with your root composer.json require (9.x-dev).

※ エラー内容を一部抜粋したものです。

これを回避するために、aws/aws-sdk-php-laravelをforkしてカスタマイズすることにしました。
forkしたリポジトリで新しくブランチを作成し、composer.jsonを書き換えます。

"illuminate/support": "^5.1 || ^6.0 || ^7.0 || ^8.0"

"illuminate/support": "^5.1 || ^6.0 || ^7.0 || ^8.0 || ^9.0"

forkしたリポジトリを使用するように、取り込む側のcomposer.jsonに項目を追加します。

"repositories": {
    "aws/aws-sdk-php-laravel": {
        "type": "vcs",
        "url": "https://github.com/fork-user/aws-sdk-php-laravel"
    }
}

composer installする際に必要となる認証ファイル(auth.json)を用意して、無事にインストールすることができました。
余談ですが、aws/aws-sdk-php-laravelがLaravel 9に対応するためのPRは作成されています。

github.com

1 - 2. Google API Clientがインストールできない問題

使用しているパッケージの一つに、Google API Clientがありました。
Laravel 9.x-devを導入し、2月頭の時点でインストールしようとしたところ、エラーが発生しました。

[RuntimeException]
  Failed to extract google/apiclient-services: (1) '/usr/bin/unzip' -qq '/var/www/vendor/composer/tmp-e96837111bda82e18ba6c462ef1ec372' -d '/var/www/vendor/composer/634d6eb6'

  replace /var/www/vendor/composer/634d6eb6/googleapis-google-api-php-client-services-d6bf710/src/ArtifactRegistry/Resource/ProjectsLocationsRepositoriesAptartifacts.php? [y]es, [n]o, [A]ll, [N]one, [r]ename:  NULL
  (EOF or read error, treating as "[N]one" ...)

調査したところ、issueとして上がっていました。

github.com

どうやらファイルシステムの関係でエラーが発生しているようでした。
conflict項目を指定することで回避できるようだったので、こちらの対応を行いました。

github.com

なお、2022年2月半ばの段階でこのエラーは修正されていました。
google/apiclientの依存に入っているgoogle/apiclient-services0.233.1で修正されたようです。

これらの問題を解決し、無事にパッケージをアップデートすることができました。

2. Laravel 9を適用した状態で、テストがどの程度通っているか確認する。

とりあえずテストを実行してみます。

Tests: 2119, Assertions: 5598, Errors: 44, Failures: 13.

Factoryの変更など、大きな改修はLaravel 6 → 8ときに終わっているので、
エラーの数としては減りました。

3. テストやphpstanで出ているエラーを修正していく。
3 - 1. HEADER_X_FORWARDED_ALLの対応

Symfonyが6系にアップデートされ、vendor/symfony/http-foundation/Request.phpからHEADER_X_FORWARDED_ALLが削除されました。

github.com

用途に合わせ、適切なものに置き換えを行いました。

3 - 2. vendor/symfony/http-foundation/ResponseHeaderBag.php

Symfony 5系と6系で、setメソッドの引数に違いがありました

<?php

// Symfony 5系
public function set(string $key, $values, bool $replace = true)

// Symfony 6系
public function set(string $key, string|array|null $values, bool $replace = true)

数値で渡している$valuesをstringでキャストする対応を行いました。

3 - 3. phpstanのエラー対応

配列(Collectionオブジェクト)にキーを指定して直接データを入れている部分でエラーが出ていたので、
コード全体で下記修正対応を行いました。

<?php

// 修正前
$collection['user_id'] = $user_id;

// 修正後
$collection->put('user_id', $user_id);
4. developへマージして動作確認を行う。

こちらもLaravel 6 → 8の時と同じく、開発メンバーからレビューをもらい確認作業を行いました。

5. 開発チーム全体に対して、ローカル環境で必要な対応の説明を行う。

Laravel 6 → 8のときはFactoryが大きく変わっていたためスクリプトなどを用意しましたが、
今回はパッケージの更新のみで問題なかったため、各メンバーにその旨を伝えて完了しました。

まとめ

開発メンバーの多いプロジェクトですが、バージョンアップ対応が完了したため記事を書いてみました。
developブランチは日々更新され、新しい機能や改修がたくさんマージされていく中で作業を進めていきました。
今回のブログの内容が誰かの役に立てば幸いです。

また、Laravel 9はLTSとして発表されていましたが、2022年2月末現在はLTSの表記が消えています。

laravel.com

これに対して、開発者であるTaylor Otwellさんがツイッターで説明をしていました。

Laravel 9はPHP 8.0をシステム要件としていましたが、Symfony 6.1がPHP 8.1をシステム要件とする可能性があるため、
現在協議しているようです。
今後の展開も引き続き追っていこうと思います。

最後まで読んでいただき、ありがとうございました。