見出し画像

Laravel (Lumen) 6.x → 9.xに更新しました

fondi DEVチームのissoです。

TL;DR

Laravel/LumenのLTSサポート終了に伴って、バージョンを6 → 9に上げました。そのときの詰まりポイントを書き残しておきます。
Laravel/LumenのUpgradeGuideに載っている内容は一部省きます。

背景

fondiのバックエンド環境

fondiのバックエンド開発では、Laravel由来の軽量PHPフレームワークであるLumenを利用しています。 インフラはGoogle App Engine Standard環境です。元々サービス立ち上げ時にLumenを選択してから大きな変化は無く、また定期的なアップデートが追いついていなかったため、LTSバージョンであるLumen 6.xを使っていました。

Laravel/Lumen 6のサポート期限

Lumenのバージョンライフサイクルは基本的に親フレームワークであるLaravelのライフサイクルに依存しています。 Laravel 6のSecurityFix期限は2022.09.06となっており、今年中のアップデートが必須でした。なお、Laravel 6以降LTSバージョンはリリースされておらず、各メジャーバージョンはリリース後およそ2年間サポートされます。

アップデートターゲット

Laravel 9 + PHP 8.1まで上げます。 このターゲットはPHPバージョンと今後のメンテナンス容易性で判断しています。

まず、Google App Engine(GAE)インスタンスのPHPバージョンは、7.4の次に8.1が出ています。(まだPreview版ではあり、当分は切り戻しを担保する必要があります。)今回のアップデートに着手する時点でGAEのPHPバージョンは7.4まで上げていたため、Laravel 8.x(PHP 7.3 ~ 8.1)まではPHPのメジャーバージョンを更新することなく上げられます。

しかし、Laravel 8.x系のサポート期限はBugFixesが2022.07.26、SecurityFixesが2023.01となっており、アップデート後2ヶ月程度でまたアップデート判断を迫られることになります。

事業上の余裕度も加味し、今回Laravel 9へのアップデートは必須としました。これにより、PHP 8.0 over が必須となるため、PHP 8.1へのアップデートも同時に進めます。

なお、fondiのバックエンドローカル開発環境は基本的にDockerを利用しており、Staging/本番環境のコンテナ化も検討すべき課題ではあるのですが、問題を切り分けて喫緊の課題であるサポート期限のキャッチアップという課題にFocusし、GAEの利用は継続しつつバージョンアップを優先対応することにしました。

手順

概要

Lumenのアップデートは実質、Laravelのアップデートに近いため、LaravelのUpgrade手順に従って1つずつバージョンを上げていきます。最後にStaging、本番環境でテストしてユーザーさんに届く、という流れです。
本家LaravelのUpgradeGuideを見ながら上げていきます。

1. Lumen 6 → Lumen 7

依存先の Symfony 5.xの更新により、 `App\Exceptions\Handler` は `Throwable` を引数に取るようになりました。

- public function report(Exception $e)
+ public function report(Throwable $e)

また、Consoleコマンドの handle() メソッドがintegerのみを返すようになりました。

use Illuminate\Console\Command;

class ExampleCommand extends Command
{
	...
	public function handle()
	{
		...
		return 0;
	}
}

認証ライブラリのPassportのアップデートにより、LumenのApplicationクラスがIncompatibleに。

On 30 Jul 2019 Laravel Passport 7.3.2
 had a breaking change - new method introduced on Application class that exists in Laravel but not in Lumen. You could either lock in to an older version or swap the Application class like follows:

https://github.com/dusterio/lumen-passport#laravel-passport-732-and-newer
// app.php
- $app = new Laravel\Lumen\Application(
+ $app = new \Dusterio\LumenPassport\Lumen7Application(
...

2. Lumen 7 → Lumen 8

Factory関連の変更
database/ 下の seeder, factory の各クラスのNamespace規則が変更されました。伴って composer.json を変更します。

...
"autoload": {
-       "classmap": [
-           "database/seeds",
-           "database/factories"
-       ],
        "psr-4": {
-           "App\\": "app/"
+           "App\\": "app/",
+           "Database\\Factories\\": "database/factories/",
+           "Database\\Seeders\\": "database/seeders/"
...

各Seederクラスにも以下を追加します。

namespace Database\Seeders;

...
class ExampleSeeder extends Seeder
{
	...
}

Globalなfactoryメソッドを廃止し、各Modelクラスに `HasFactory` traitを適用します。

- factory(App\Models\User::class)->create();
+ App\Models\User::factory()->create();

Laravel 7以前のLegacyな方法も一応存在しますが、今後の更新は新規側にかかるはずなので、このタイミングで完全に移行してしまいます。モデルクラスの書き換えはかなり面倒ですが必要犠牲と割り切る。

However, to ease the upgrade process, a new `laravel/legacy-factories`
 package has been created to continue using your existing factories with Laravel 8.x

https://laravel.com/docs/8.x/upgrade#model-factories

`Database\Factories\UserFactory` クラスでも、 `state` 定義をpublicメソッドで書くようになりました。`$this->faker` で `Faker\Generator` クラスを参照できるのも個人的には嬉しいポイントです。

class UserFactory extends Factory
{
	public function definition(): array
	{
		return ['type' => $this->faker->randomLetter];
	}
	
	public function foo(): static
	{
		return $this->state(['type' => 'foo']);
	}
}

余談ですが、 `Factory` クラスを追ってみると、 Genericsが使われており、IDE補完も良い感じに効かせていきたいですね。

/**
 *@templateTModel of \Illuminate\Database\Eloquent\Model
 */
abstract class Factory
{
	...
	/**
	 * Create a collection of models and persist them to the database.
	 *
	 * @param  (callable(array<string, mixed>): array<string, mixed>)|array<string, mixed>  $attributes
	 * @param  \Illuminate\Database\Eloquent\Model|null  $parent
	 * @return \Illuminate\Database\Eloquent\Collection<int, \Illuminate\Database\Eloquent\Model|TModel>|\Illuminate\Database\Eloquent\Model|TModel
	 */
	public function create($attributes = [], ?Model $parent = null)
	{...

PHPUnitの変更

`docker-compose.yml` 等でグローバル環境変数を定義する際、 `phpunit.xml` の<env>タグによる変数定義で上書きできなくなりました。
https://github.com/sebastianbergmann/phpunit/issues/4540
このため、<server>タグで上書きします。

- <env name="APP_ENV" value="testing" force="true"/>
+ <server name="APP_ENV" value="testing" />

3. PHP 7.4 → PHP 8.1

Laravel 9の必要条件である、PHP 8.1へのアップデートを先に進めていきます。
`ext-json` はPHP 8.0から、Always Availableになりました。
PHP: rfc:always_enable_json

このため、`composer.json` から取り除いておきます。

...
"require-dev": {
	...
- "ext-json": "*",

Dockerfileからも。

FROM php:8.1-fpm

...

- RUN docker-php-ext-install json

...

PHP 8.0から増えたWarning項目がいくつかあります。

今回は、一部コードで `str_xx` 系の関数にnullを渡していた箇所があり、以下に引っかかってWarningの出ていた箇所がありました。

A number of notices have been converted into warnings:
...
Attempting to use null, a boolean, or a float as a string offset.

https://www.php.net/manual/en/migration80.incompatible.php

nullableな値が渡されるところで、空文字への変換をちゃんと書く必要があります。

// Old, with warning
$foo = str_contains($bar->canBeNull, 'foo');

// Without warning
$foo = str_contains($bar->canBeNull ?? '', 'foo');

4. Lumen 8 → Lumen 9

いよいよ最新リリースのLumen 9へのアップデートです。

Flysystem 1.x → 3.xへのUpgrade
https://laravel.com/docs/9.x/upgrade#flysystem-3

これに伴い、一部Google Cloud Storage(GCS)に静的ファイルを内部アップロードするために利用していた superbalist/laravel-google-cloud-storage ライブラリの依存解決ができなくなったので、LaravelのCustom Filesystemsを使って再実装します。

class GcsFileSystemServiceProvider extends ServiceProvider
{
	public function boot()
	{
		$storageClient = new StorageClient($clientOptions);
    $bucket = $storageClient->bucket('bucket-name');

    $adapter = new GoogleCloudStorageAdapter($bucket, 'path-prefix-');

    return new FilesystemAdapter(
        new Filesystem($adapter),
        $adapter,
        $config
    );
	}
}

あとはLaravel公式のUpgradeGuideに従って各依存パッケージをバージョンアップして完了です。

https://laravel.com/docs/9.x/upgrade

まとめ

PHPバージョンとLaravelバージョン、Google App Engineインスタンスの互換性が若干入り組んでいるのが困難ポイントでした。
それから、Factoryの変更も変更量が割と膨大でした(テストの書き方に依ったかも)。

今回コストを払った分、サポート期限の延長を得たのとアプリケーションコードの書きやすさがちゃんと向上したのは良かったと感じています。


宣伝

fondiでは、一緒にプロダクト開発を加速させてくれるエンジニア仲間を募集しています!


この記事が気に入ったらサポートをしてみませんか?