😢

Laravel7から8への移行でファクトリーを整理する

2023/01/03に公開

これは、Laravel にちょっとイラッとした私がその怒りをエネルギーに Factory 周りを整理した記事です。
最初に Laravel への怒りを書いたあと、どうやってLaravel7 から 8 への移行で Factory を整理すると良いのかについて書いています。なので、整理について読みたい方は後半を読んでもらうと良いと思います。

Laravel8 から Models ディレクトリができてた

私は Laravel5.5 から使い始めてますが、Laravel7 まではイントロダクションで「開発者が置きたいところに置いてね」(拡大解釈かもしれないけど)と書いてあったため、Entities ディレクトリなど作って配置していました。

ところが、Laravel8 以降のドキュメントではしれっと app ディレクトリに Models ディレクトリができてる!?

5.5 からなのでそんなに古参ではないけど、少し裏切られたような気持ちになりました。
まあ、Models ディレクトリできてるぐらいなら大丈夫やろ〜。アップグレードガイドにも書いてないし〜。と思っていました。けど、そんなことはなかった…

Factory が面倒になった

"Models ディレクトリを使わない" or "Models ディレクトリに階層がある"状態で Laravel8 で新しいモデル、マイグレーションとファクトリを作ってテストを実行するといろいろな問題に遭遇する。

下記ブランチを使って問題を起こしてみる。
なお、私の PHP のバージョンは 8.1.12 です。
https://github.com/zatsuyooo/laravel7/tree/Laravel8

下準備(必要な人)
cp .env.example .env
composer require laravel/legacy-factories
touch database/database.SQLite
php artisan migrate
php artisan key:generate 

Book モデルとマイグレーションファイルとファクトリファイルを作る

php artisan make:model -fm Entities/Book

テストを書く

tests/Unit/ExampleTest.php
<?php

namespace Tests\Unit;

use Tests\TestCase;
use App\Entities\Book;
use App\Entities\User;

class ExampleTest extends TestCase
{
    /**
     * A basic test example.
     *
     * @return void
     */
    public function testBasicTest()
    {
        factory(User::class)->create();
        Book::factory()->make();
        $this->assertTrue(true);
    }
}

テストを実行する

php artisan test

もちろんエラーになる

Class 'Database\Factories\Entities\BookFactory' not found

ここで我々は以下の選択に迫らせます。

  1. 新旧を混在させて頑張ってみる
    • チャレンジャーな人。怖いもの知らず。
  2. Laravel7 以前の Factory にする
    • Model も Factory も多すぎるし、全部更新するのは無理だよ! って人。
  3. Laravel8 の Factory にする
    • 結局これにするのが良いと思う。

1. 新旧を混在させて頑張ってみる

  1. laravel/legacy-factories をインストールして旧 Factory を使える状態にする

    composer require laravel/legacy-factories
    
  2. Factory の名前空間がパスと合ってないので修正

    BookFactory.php
    -namespace Database\Factories;
    +namespace Database\Factories\Entities;
    
    use Illuminate\Database\Eloquent\Factories\Factory;
    
    class BookFactory extends Factory
    {
    
  3. これでもテストはエラーになる

    Class 'App\Book' not found
    
  4. App/Entities のモデルを見に行っていないので Factory の appNamespace を変更する親クラスを作る

    BaseFactory.php
    <?php
    
    namespace Database\Factories\Entities;
    
    use Illuminate\Database\Eloquent\Factories\Factory;
    
    abstract class BaseFactory extends Factory
    {
        /**
         * Get the application namespace for the application.
         *
         * @return string
         */
        protected static function appNamespace()
        {
            $string = parent::appNameSpace();
            return $string . 'Entities\\';
        }
    }
    
    BookFactory.php
    namespace Database\Factories\Entities;
    
    -use Illuminate\Database\Eloquent\Factories\Factory;
    
    -class BookFactory extends Factory
    +class BookFactory extends BaseFactory
    {
    

これで新旧が共存できる。
新旧は共存できないと言っている人もいるが、一応できている。
https://laracasts.com/discuss/channels/laravel/cannot-declare-class-because-name-already-in-use?page=1&replyId=643316

2. Laravel7 以前の Factory にする

  1. laravel/legacy-factories をインストールして旧 Factory を使える状態にする

    composer require laravel/legacy-factories
    
  2. BookFactory を旧 Factory に書き換える

    BookFactory.php
    <?php
    
    /** @var \Illuminate\Database\Eloquent\Factory $factory */
    
    use App\Entities\Book;
    
    $factory->define(Book::class, function () {
        return [
        ];
    });
    
  3. Book モデルから HasFactory を削除(※削除しなくても動いた)

    Book.php
    namespace App\Entities;
    
    -use Illuminate\Database\Eloquent\Factories\HasFactory;
    use Illuminate\Database\Eloquent\Model;
    
    class Book extends Model
    {
    -    use HasFactory;
    }
    
  4. テストを書き換える

    tests/Unit/ExampleTest.php
    public function testBasicTest()
    {
        factory(User::class)->create();
    -   Book::factory()->create();
    +   factory(Book::class)->create();
        $this->assertTrue(true);
    }
    

3. Laravel8 の Factory にする

すべて置き換えるのを手でやってるとかなり大変なので、Rector を使って自動リファクタリングしちゃいます。
なお、モデルのディレクトリは既存のままにしてなるべく修正範囲を少なくしています。(できる人はモデルを Models ディレクトリにした方が後々楽)

  1. Rector をインストールし、設定ファイルを生成する

    composer require rector/rector --dev
    # 2022/10 末にコミュニティパッケージに移行した
    composer require driftingly/rector-laravel --dev
    vendor/bin/rector init
    
  2. 作られた rector.php を変更し、Factory のリファクタリングができるようにする

    rector.php
     <?php
    
     declare(strict_types=1);
    
     use Rector\CodeQuality\Rector\Class_\InlineConstructorDefaultToPropertyRector;
     use Rector\Config\RectorConfig;
     use Rector\Set\ValueObject\LevelSetList;
     use RectorLaravel\Set\LaravelSetList;
    
     return static function (RectorConfig $rectorConfig): void {
         $rectorConfig->paths([
             __DIR__ . '/database',
             __DIR__ . '/tests',
         ]);
    
         // register a single rule
         $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class);
    
         $rectorConfig->sets([
             LaravelSetList::LARAVEL_LEGACY_FACTORIES_TO_CLASSES,
         ]);
     };
    
  3. ドライランで確認

    vendor/bin/rector process --dry-run --clear-cache
    
  4. rector を実行

    vendor/bin/rector process --clear-cache
    
    11/11 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
    3 files with changes
    ====================
    
    1) tests/Unit/ExampleTest.php:14
    
        ---------- begin diff ----------
    @@ @@
        */
        public function testBasicTest()
        {
    -       factory(User::class)->create();
    +       User::factory()->create();
            Book::factory()->make();
            $this->assertTrue(true);
        }
    }
        ----------- end diff -----------
    
    Applied rules:
    * FactoryFuncCallToStaticCallRector (https://laravel.com/docs/7.x/database-testing#creating-models)
    
    
    2) database/factories/UserFactory.php:5
    
        ---------- begin diff ----------
    @@ @@
    use Faker\Generator as Faker;
    use Illuminate\Support\Str;
    
    -/*
    -|--------------------------------------------------------------------------
    -| Model Factories
    -|--------------------------------------------------------------------------
    -|
    -| This directory should contain each of the model factory definitions for
    -| your application. Factories provide a convenient way to generate new
    -| model instances for testing / seeding your application's database.
    -|
    -*/
    -
    -$factory->define(User::class, function (Faker $faker) {
    -    return [
    -        'name' => $faker->name,
    -        'email' => $faker->unique()->safeEmail,
    -        'email_verified_at' => now(),
    -        'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
    -        'remember_token' => Str::random(10),
    -    ];
    -});
    +class UserFactory extends \Illuminate\Database\Eloquent\Factories\Factory
    +{
    +    protected $model = User::class;
    +    public function definition()
    +    {
    +        return [
    +            'name' => $this->faker->name,
    +            'email' => $this->faker->unique()->safeEmail,
    +            'email_verified_at' => now(),
    +            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
    +            'remember_token' => Str::random(10),
    +        ];
    +    }
    +}
        ----------- end diff -----------
    
    Applied rules:
    * FactoryDefinitionRector (https://laravel.com/docs/7.x/database-testing#writing-factories)
    
    
    3) database/factories/Entities/BookFactory.php:3
    
        ---------- begin diff ----------
    @@ @@
    
    use App\Entities\Book;
    
    -$factory->define(Book::class, function () {
    -    return [
    -    ];
    -});
    +class BookFactory extends \Illuminate\Database\Eloquent\Factories\Factory
    +{
    +    protected $model = Book::class;
    +    public function definition()
    +    {
    +        return [
    +        ];
    +    }
    +}
        ----------- end diff -----------
    
    Applied rules:
    * FactoryDefinitionRector (https://laravel.com/docs/7.x/database-testing#writing-factories)
    
    
    
    [OK] 3 files have been changed by Rector
    
  5. 下記のことは Rectory でできないので手動で書き換えや修正をする

    • モデルに HasFactory は追加する

      + use Illuminate\Database\Eloquent\Factories\HasFactory;
      
      class User extends Authenticatable
      {
      -    use Notifiable;
      +    use Notifiable, HasFactory;
      
    • Factory に NameSpace を追加・修正する

      - namespace Database\Factories;
      + namespace Database\Factories\Entities;
      
    • 対応するモデルを Factory のプロパティに追加する

      + use App\Entities\Book;
      use Illuminate\Database\Eloquent\Factories\Factory;
      
      class BookFactory extends Factory
      {
      +    protected $model = Book::class;
      
      
    • モデルファイルのフォルダ構成に database/factories を合わせる

      mv database/factories/UserFactory.php database/factories/Entities
      
    • Factory Callbacks を書き換える

      -    $factory->afterCreatingState(User::class, 'hoge', function ($user) {
      -       // …
      -    });
      +    public function hoge()
      +    {
      +        return $this->afterCreating(function (User $user) {
      +           // …
      +        });
      +    }
      

まとめ

記事にしてみた結果、余計にイライラが増しましたとさ。

Discussion