🦔

Laravel6から8にアップデートしたので変更内容をレポートします

2021/11/19に公開
2

先日、マナリンクで利用しているLaravelのバージョンを6系から8系に上げたので、変更内容や注意事項をレポートします。

前提条件

マナリンクはオンライン家庭教師サービスで、LaravelはフロントのNuxt.jsに対するAPIサーバーとして主に運用しています。先生の検索機能や、先生へのお支払い機能などがあります。
マナリンク自体は開発を開始して2年弱のサービスなので、世間の大規模なLaravelサービスに比べるとかなり小規模な方だと思われます。

どうして6系から8系に上げたのか

モデルファクトリの変更を反映しておきたかった

Laravel8系の目玉アップデートといえば、モデルファクトリの大幅な仕様変更です。

https://readouble.com/laravel/8.x/ja/database-testing.html

https://qiita.com/ucan-lab/items/788d1a603f2b53ebb999

日々開発してテストコードを書いていくたびに、「ああ...今書いているfactory()ヘルパは使われなくなるんだよなぁ」と思っていたので一刻も早く導入したいなと思っていました。

もちろんlegacy-factoriesパッケージを入れても良いのですが、まだ小規模なアプリケーションなので妥協したくなかったのと、legacy-factoriesは一度入れるとなおさらモデルファクトリに移行するタイミングを見失いそうだったのが懸念でした。

9系に上げる前に8系を一度挟みたかった

https://readouble.com/laravel/8.x/ja/releases.html によると、Laravel8系はLTSではないのでバグフィックスが2022年1月26日で切れてしまいます。なのでいっそ9系のリリースまで待っても良かったのですが、すでに現時点で6→7と7→8への差分があるのに関わらずさらにそこに8→9への差分も加わると、モデルファクトリ以外のアップデートの懸念点が増えてきても不思議ではないです。

現時点では自分1人で開発していますが、採用して数人に増えると途端にプロジェクトが並行で動いたり特大プロジェクトが始まったりするでしょうから、少しでも最新版に近づけておくのは無駄ではないはずだと考えました。

PHP8を使いたかった

記事公開時点でマナリンクのPHPは7.4系ですが、PHP8.0がGAになってからもうすぐ1年、あっという間にPHP8.1のリリースが間近になっています。

PHP8系の新機能のうち、Nullsafe operatorなどぜひとも開発効率化に使いたい文法が特に魅力的で、Laravelのアップデートと同様にこちらも上げたいと思っています。

https://qiita.com/rana_kualu/items/fe7998fbe773544d5d25

https://qiita.com/rana_kualu/items/a6601b49e0591eb42200

ところが例えばlaravel-ide-helperなど、Laravel8系への対応をメインにメンテナンスされている関連パッケージがいくつか見当たりました。

https://github.com/barryvdh/laravel-ide-helper/blob/master/CHANGELOG.md#2020-12-30-290

そこでそれぞれの実装やCHANGELOGを読んでいくと、どうもPHP8への対応も、Laravel8対応のバージョンにしか取り込んでいないものがあったのです。

となると先にLaravel8にアップデートしないことには、これらのライブラリへの対応ができないためPHP8にも上げられないということになります(もちろん無理やり解決する方法はあると思いますが、そこまで切羽詰まっているわけでもない)。

以上より、Laravel6を先に8にアップデートしたいと判断しました。

Laravel8に上げることができたので、PHP8.1が公開されたら、作業ブランチを切ってアップデートに取り組んでみようと思っています。

アップデート作業内容

モデルファクトリ

モデルファクトリ機能の概要を学ぶ

最も大きい差分はモデルファクトリでした。これに伴い、既存のファクトリファイルをすべて再実装し、関連するテストコードをすべて編集することになりました。

簡単な例ですと、以下のように実装し直すことになります。

▼旧 チャット部屋Modelの例

<?php

use App\Models\ChatRoom;
use Faker\Generator as Faker;
use Illuminate\Database\Eloquent\Factory;

$factory->define(ChatRoom::class, function (Faker $faker) {
    return [
        'id' => $faker->uuid,
    ];
});

▼新

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class ChatRoomFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'id' => $this->faker->uuid,
        ];
    }
}

基本的に定義内容をdefinition()メソッドに移植するだけなので、慣れてくればコピペで大丈夫です。

また、afterCreatingなど追加の処理を実装している場合はconfigure()メソッドに実装します。

以下はSuper AdministratorのRoleを保有する社内向け管理画面ユーザーの生成処理です。

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;

class AdminFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        return [
            'name' => $this->faker->name,
            'email' => $this->faker->email(),
            'password' => makePassword() // パスワード生成処理がここに入るイメージ
        ];
    }

    public function configure()
    {
        return $this->afterCreating(function ($admin) {
            $admin->assignRole(\App\Domain\Admin\AdminRole::SuperAdministrator);
        });
    }
}

あるModelから生成されるデータは1パターンではなく、様々な状態を持つことがあるでしょう。たとえばキャンセルされてしまった契約情報を生成する場合は以下のように追加の関数を実装します。

    public function userCanceled()
    {
        return $this->state([
            'canceled_at' => now()->subDays(3),
        ]);
    }

以下のように生成時はstate関数をチェインしながらcreateします。

Contract::factory()->userCanceled()->create()

既存のモデルにtraitを追加

Factoryは上記の通り、Eloquent Modelに対してfactory()メソッドが生える形で提供されます。なので以下のようにファクトリの対象になるモデルにはトレイトを追加します。

+ use Illuminate\Database\Eloquent\Factories\HasFactory;

+     use HasFactory;

ひたすらモデルファクトリを実装

モデルファクトリを機械的に既存のものから置換して生成するのはなかなか難しいと思ったので、手作業でひたすら実装しました。

マナリンクの場合は、たかだか35ファイルで済んだので、意外と数時間ほど集中すれば終わりました。既存のファクトリ実装が関数ベースだったのをいいことに、1つのPHPファイルにいくつものモデルのファクトリが書かれていることがあり、そこが少々辛かったです。

細かいですが、definitionメソッド等にコピーしたときは、既存のファクトリの$faker$this->factoryにしないといけないので、そこは地味に面倒ではありました。

既存のテストコードを置換

モデルファクトリの実装が終わったら、既存のテストコードに一斉置換を掛けます。

// 旧
$user = factory(User::class)->create();
$user = factory(User::class, 'HogeTypeUser')->create();

// 新
$user = User::factory()->create();
$user = User::factory()->hogeTypeUser()->create();

見るからに置換できそうですよね。

最終的には以下のような正規表現を書くことで、容易に一斉置換をかけることができました

▼シンプルな場合

factory\(([a-zA-Z\\]+)::class\)$1::factory()

▼stateを伴う場合

factory\(([a-zA-Z\\]+)::class(, ')([a-zA-Z]+)(')\)$1::factory()->\l$3()

ほとんど同じ正規表現なのでもしかしたら一発で書けたかもしれませんし、もっと綺麗な表現で書けた気もしますが、それを探求するよりおとなしく作業するほうがコスパ良さそうだったので秒で作った正規表現をそのまま使うことにしました。

stateを伴う場合の正規表現が肝で、\l(先頭の文字を小文字に変換する)を使って既存ではパスカルケースで書かれていたstateの名前をキャメルケースに置換して関数名にしました。

\lは多分標準正規表現ではないのですが、PHPStormでは使えたのでありがたく使いました。

ここはもともと既存のファクトリでstateを使うときにパスカルケースに統一していたので楽ができましたが、ルールがバラバラだと厳しいかもしれません。不幸中の幸い?でした。

デグレしたテストケースを調査

ここまで終わったらテストケースを一通り再実行してデグレしていないかチェックします。

機械的に置換できなかったところを中心に手動で修正しましたが、これも幸いなことにさほど手間取りませんでした。

ちなみに記事公開時点でテストケース数は657 tests, 3725 assertionsです。


以上でモデルファクトリの説明を終わります。

Mailable::sendメソッドのシグネチャ変更

Mailableクラスを直接使わずに、内部で拡張したものを挟んでそれを普段さらに拡張してメール送信処理を書いています。

そこでsendメソッドをオーバーライドしているのですが、public function send(MailerContract $mailer)と書いていたものをpublic function send($mailer)と書かないとエラーになるようになってしまっていました。

詳しい理由まで追えていないのですが、これだけ見るとmixedにデグレしちゃっているので少し複雑です。

PHPUnitのmigrationコマンドを実行

以下のコマンドを実行し、PHPUnitの設定ファイルのアップデートをしました。

ちゃんと内容を見れていないので調べないといけないな・・・

vendor/bin/phpunit --migrate-configuration

メールテンプレートのstyleが外れていた

マナリンクはSPAですが、メールを送信するときだけbladeを使っています。

メールアドレス本人確認のメールと、パスワード再設定のメールはLaravelのプリセットされているメールテンプレートを使っているのが現状なのですが、それぞれボタンの色がPrimaryの青色になっていたのが黒になっていました。これはおそらくlaravel/uiパッケージのアップデートによるものでしょう。

php artisan vendor:publish --tag=laravel-mail

このコマンドを実行するとメールのテンプレートが自分で自由に変更できるようになるので、CSSを書き換えて色をマナリンクのブルーにしておきました。

https://readouble.com/laravel/8.x/ja/mail.html

その他

概ねこの程度でした。それ以外には、既存実装でバグっていたけど無視されていたものが、8に上げたことできちんとErrorとして扱われるようになっていた差分があり、これは明確にステージング環境でデグレしてしまっていたので修正しました。このパターンが一番怖いですね。

こういうときのためにテストコードがあるので、もっとテストコードを書いていかなければと改めて思いました。

まとめ

モデルファクトリの差分が最も面倒そうに思っていたのですが、腰を据えたら置換が上手く行ったこともあって2日程度で作業が終わりました。期間とか大変さはサービスの規模によるとは思いますが、どなたかの参考になれば幸いです。

開発組織に関しては、2021年11月現在、開発はCTO1人で行っているため、同時並行でプロジェクトが進む中でどのタイミングでアップデートすべきか、といった悩みは基本的にありませんでした(大至急採用中です!)。

リリース後数日経過しましたが今のところ本番環境でデグレがなく、既存テストコードが良い仕事したな〜と思ってます(フラグかもしれない)。

また、今回のアプデを行うにあたってカジュアル面談サービスのMeety経由で出会った方に軽く相談をさせていただきました。この場を借りてお礼申し上げます。

https://meety.net/matches/xtznANVQOPKb

マナリンク Tech Blog

Discussion

ゆうやみゆうやみ

Laravel 9のリリースを延期した影響で、サポート期間が変更されてます。
結果、6より8の方がサポート期間が長くなっています。
6→8の段階で変更量も多いので、6→9組は相当えらいことになりそう……お疲れ様でした。

マナリンク Tech Blog運営マナリンク Tech Blog運営

本当ですね!日本語版のドキュメントには反映されていないんですね。8のほうがサポート期間が長いと知って、結果論ではありますが安心しました。

本当そうなんですよね。調べてみると6と8は同時にメンテナンスされているものの、8系にしか入っていない軽微なバグフィックスもちらほら見えるので、そのあたりも考慮すると6→9は凄い差分になりそうです。

教えていただいてありがとうございます!