Laravel7から8への移行でファクトリーを整理する
これは、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 です。
下準備(必要な人)
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
テストを書く
<?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
ここで我々は以下の選択に迫らせます。
- 新旧を混在させて頑張ってみる
- チャレンジャーな人。怖いもの知らず。
- Laravel7 以前の Factory にする
- Model も Factory も多すぎるし、全部更新するのは無理だよ! って人。
- Laravel8 の Factory にする
- 結局これにするのが良いと思う。
1. 新旧を混在させて頑張ってみる
-
laravel/legacy-factories をインストールして旧 Factory を使える状態にする
composer require laravel/legacy-factories
-
Factory の名前空間がパスと合ってないので修正
BookFactory.php-namespace Database\Factories; +namespace Database\Factories\Entities; use Illuminate\Database\Eloquent\Factories\Factory; class BookFactory extends Factory {
-
これでもテストはエラーになる
Class 'App\Book' not found
-
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.phpnamespace Database\Factories\Entities; -use Illuminate\Database\Eloquent\Factories\Factory; -class BookFactory extends Factory +class BookFactory extends BaseFactory {
これで新旧が共存できる。
新旧は共存できないと言っている人もいるが、一応できている。
2. Laravel7 以前の Factory にする
-
laravel/legacy-factories をインストールして旧 Factory を使える状態にする
composer require laravel/legacy-factories
-
BookFactory を旧 Factory に書き換える
BookFactory.php<?php /** @var \Illuminate\Database\Eloquent\Factory $factory */ use App\Entities\Book; $factory->define(Book::class, function () { return [ ]; });
-
Book モデルから
HasFactory
を削除(※削除しなくても動いた)Book.phpnamespace App\Entities; -use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Book extends Model { - use HasFactory; }
-
テストを書き換える
tests/Unit/ExampleTest.phppublic function testBasicTest() { factory(User::class)->create(); - Book::factory()->create(); + factory(Book::class)->create(); $this->assertTrue(true); }
3. Laravel8 の Factory にする
すべて置き換えるのを手でやってるとかなり大変なので、Rector を使って自動リファクタリングしちゃいます。
なお、モデルのディレクトリは既存のままにしてなるべく修正範囲を少なくしています。(できる人はモデルを Models ディレクトリにした方が後々楽)
-
Rector をインストールし、設定ファイルを生成する
composer require rector/rector --dev # 2022/10 末にコミュニティパッケージに移行した composer require driftingly/rector-laravel --dev vendor/bin/rector init
-
作られた
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, ]); };
-
ドライランで確認
vendor/bin/rector process --dry-run --clear-cache
-
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
-
下記のことは 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