thettler/laravel-factory-classes

此包已被弃用,不再维护。未建议替代包。

一个通过流畅的API和自动补全轻松创建测试用例Factory类的包

v0.0.2 2020-04-01 09:52 UTC

README

Latest Version on Packagist Packagist Total Downloads Twitter: TobiSlice

Laravel Factory Classes是一个帮助您通过Factory类使用流畅的API和自动补全创建带有数据的Model的包,用于测试、seeder或任何您可能需要的地方。

💡 为什么想要使用这个包?

为所有Model创建Factory有时会变得相当混乱。它缺乏自动补全,您通常需要了解Model的业务逻辑才能将其置于特定状态。Factory类解决了这些问题。每个Model都有自己的Factory,它包含创建和填充数据所需的一切。您可以使用Factory的API简单地实例化、保存或修改Model。
要了解更多关于这个主题的信息,我建议阅读来自sticher.io这篇博客文章

示例

考虑一个场景,我们有一个具有地点(belongsTo)、支持艺人(belongsToMany)、压轴艺人(belongsToMany)以及其他我们在此示例中不关心的属性的音乐会Model。使用常规Factory,您会这样做来创建一个音乐会:

$concert = factory(Concert::class)->create([
    'date' => now()
]);
$venue = factory(Venue::class)->create();
$headliner = factory(Artist::class)->create();
$support = factory(Artist::class)->create();

$concert->venue()->associate($venue);
$concert->lineup()->attach($headliner->id, ['headliner'=> true]);
$concert->lineup()->attach($support->id, ['headliner'=> false]);

$this->assert($concert, /*Do something with your Concert*/);

使用此包,它看起来会是这样:

$concert = ConcertFatory::new()
            ->date(now())
            ->withVenue()
            ->withHeadliner()
            ->withSupport()
            ->create();

$this->assert($concert, /*Do something with your Concert*/);

Factory类的样子如下

<?php

namespace App\Factories;

use App\Concert;
use Faker\Generator;
use Illuminate\Support\Carbon;
use Thettler\LaravelFactoryClasses\FactoryClass;

class ConcertFactory extends FactoryClass
{
    protected string $model = Concert::class;

    protected function fakeData(Generator $faker): array
    {
        return [
            'date' => $faker->dateTimeBetween('-5 years', '+5 years'),
            /* More attributes*/
        ];
    }

    public function create(array $extra = []): Concert
    {
        return $this->createModel($extra);
    }

    public function make(array $extra = []): Concert
    {
       return $this->makeModel($extra);
    }

    public function date(Carbon $data): self
    {
        return $this->addData('date', $data);
    }

    public function withVenue($venueFactory = null): self
    {
        return $this->belongsTo('venue', $venueFactory ?? VenueFactory::new());
    }

    public function withHeadliner(...$artistFactories): self
    {
        return $this->belongsToMany(
            'lineups',
            empty($artistFactories) ? [ArtistFactory::new()] : $artistFactories,
            fn(BelongsToManyFactoryRelation $relation) => $relation->pivot(['headliner' => true])
        );
    }

    public function withSupport(...$artistFactories): self
    {
        return $this->belongsToMany(
            'lineups',
            empty($artistFactories) ? [ArtistFactory::new()] : $artistFactories,
            fn(BelongsToManyFactoryRelation $relation) => $relation->pivot(['headliner' => true])
        );
    }

}

📜 功能

  • 创建模型并将其保存到数据库
  • 创建模型但不保存到数据库
  • 创建具有关系的模型
  • 通过一个命令(--recursive)创建所有所需的Factory
  • 自动为您的模型生成模拟数据
  • 无需生成模拟数据也可在测试之外使用
  • 将模型创建逻辑提取到自己的类中
  • 在任何时候都可以完全控制关系和属性

💻 安装

您可以通过composer安装此包

composer require thettler/laravel-factory-classes

要发布配置文件,请运行

php artisan vendor:publish --provider="Thettler\LaravelFactoryClasses\FactoryClassServiceProvider"

它将提供包的配置文件,您可以在其中定义模型的路径、生成的Factory的路径以及生成的Factory的命名空间

<?php

/*
 * You can place your custom package configuration in here.
 */
return [
    'models_path' => base_path('app'),

    'factories_path' => base_path('app/Factories'),

    'factories_namespace' => 'App\Factories',
];

🚀 入门

快速入门

要创建新的Factory,可以使用以下命令:

php artisan make:factory-class

这将提示您一个列表,其中包含模型,它知道您的模型位于您的配置中。您可以选择要创建的模型。这将为您在App\Factories命名空间下创建一个新的Factory,并自动添加所有关系,如果您在Model内部使用它们的返回值进行类型提示。有关关系的更多信息,请参阅手动指南

附加参数和选项

如果您不想从列表中选择模型,您可以将模型路径中模型的类名作为参数传递,您的工厂将立即为您创建。

php artisan make:factory-class User

默认情况下,如果尝试创建的工厂已存在,此命令将停止并报错。您可以使用强制选项覆盖现有工厂。

php artisan make:factory-class User --force

使用--recursive标志可以快速构建大量新的工厂。然后它将遍历所选类的所有关系,如果还没有,还会为它们创建工厂。

php artisan make:factory-class User --recursive

如果您不希望自动创建关系方法,可以使用此标志。

php artisan make:factory-class User --without-relations

您还可以使用以下命令覆盖配置:--models_path=app/models

--factories_path=你的工厂路径

--factories_namespace=你的命名空间\Your\Factories

手动方式

要创建第一个FactoryClass,您只需创建一个普通的PHP类,并扩展抽象类Thettler\LaravelFactoryClasses\FactoryClass。此类要求您定义3个方法和一个属性。

  1. 首先,您必须向工厂添加一个protected string $model属性。它包含要创建的模型的引用。例如,Laravel提供的标准User类。
<?php
namespace App\Factories;

use Thettler\LaravelFactoryClasses\FactoryClass;
use App\User;

class UserFactory extends FactoryClass {
      protected string $model = User::class;
}
  1. 告诉工厂它应该创建哪个模型后,您必须定义一个public create()函数。它期望一个参数$extra,它是一个默认值为空数组的数组。关于$extra的更多内容稍后讨论。在这里,您应该将模型类类型化为返回值。这将使大多数编辑器提供自动完成功能。最后,调用并返回$this->createModel($extra)方法。此方法将负责为您创建模型。您仍然可以在模型创建后对其进行修改,或完全自行创建它。所以我们的User类看起来像这样
<?php
namespace App\Factories;

use Thettler\LaravelFactoryClasses\FactoryClass;

class UserFactory extends FactoryClass {
      /*...*/

     public function create(array $extra = []): User
      {
          return $this->createModel($extra);
      }
}
  1. 现在您必须添加一个public make()函数。它与create()几乎相同,但在这里您调用的是$this->makeModel($extra)方法而不是$this->createModel($extra)。我们稍后将讨论create()make()之间的区别。所以我们的User类看起来像这样
<?php
namespace App\Factories;

use Thettler\LaravelFactoryClasses\FactoryClass;

class UserFactory extends FactoryClass {
      /*...*/

     public function make(array $extra = []): User
      {
          return $this->makeModel($extra);
      }
}
  1. 您必须添加的最后一种方法是protected fakeData()。此方法接收一个Faker实例作为参数,并期望您返回一个数组。fakeData()方法用于为您的模型生成默认数据。它返回一个关联数组,其中属性名称作为键,您要设置的值作为值。
    所以我们的User类看起来像这样
<?php
namespace App\Factories;

use Thettler\LaravelFactoryClasses\FactoryClass;

class UserFactory extends FactoryClass {
      /*...*/

    protected function fakeData(\Faker\Generator $faker): array
    {
        return [
            'name' => $faker->name,
            'email' => $faker->unique()->safeEmail,
            'email_verified_at' => now(),
            'password' => Hash::make('secret'),
            'remember_token' => Str::random(10),
        ];
    }
}

现在您可以开始了,并已创建了第一个FactoryClass。完整的类现在看起来像这样

<?php
namespace App\Factories;

use Thettler\LaravelFactoryClasses\FactoryClass;
use App\User;

class UserFactory extends FactoryClass {

        protected string $model = User::class;

        public function create(array $extra = []): User
        {
            return $this->createModel($extra);
        }

        public function make(array $extra = []): User
        {
            return $this->makeModel($extra);
        }

        protected function fakeData(\Faker\Generator $faker): array
        {
            return [
                'name' => $faker->name,
                'email' => $faker->unique()->safeEmail,
                'email_verified_at' => now(),
                'password' => Hash::make('secret'),
                'remember_token' => Str::random(10),
            ];
        }
}

使用工厂

创建/制作模型

现在您已创建您的FactoryClass,您将想使用它。要实例化您的FactoryClass,请使用static new()方法。

$userFactory = UserFactory::new();

现在要创建并保存模型到数据库,请调用create()方法。这将创建一个模型,其数据定义在fakeData()中。

$userFactory = UserFactory::new();
$userModel = $userFactory->create();

如果您不想将模型存储在数据库中,请使用make()方法。

$userFactory = UserFactory::new();
$userModel = $userFactory->make();

如果您需要多个模型,可以使用createMany()makeMany()方法。两个方法都接受一个整数参数,表示应创建多少个模型,并返回包含模型的Collection

$threeSavedUserCollection = UserFactory::new()->createMany(3);
$threeNotSavedUserCollection = UserFactory::new()->makeMany(3);

更改数据

在大多数情况下,您可能希望根据情况更改某些属性,而不希望使用所有的 fakeData。本包为您提供了3种更改用于创建模型所使用的数据的选项。

  1. 使用 addData() 方法。该方法以属性名称作为第一个参数,以应设置的值作为第二个参数。
$user = UserFactory::new()
           ->addData('name', 'myName')
           ->create();

echo $user->name; // myName
  1. 第二种选项是使用 data() 方法。该方法接受一个键值对数组,表示属性及其值。此方法还将覆盖之前设置的所有数据
 $user = UserFactory::new()
            ->data(['name' => 'myName'])
            ->create();
 echo $user->name; // myName

⚠️ data()addData() 将自动为您提供一个新实例的工厂,以防止副作用

  1. 最后一个选项是在 create()createMany()make()makeMany() 方法的 $extra 参数上使用。这将使用相同的键覆盖以前的数据。
$user = UserFactory::new()->create(['name' => 'extraName']);
echo $user->name; // extraName

$user = UserFactory::new()->make(['name' => 'extraName']);
echo $user->name; // extraName

$userCollection = UserFactory::new()->createMany(2, ['name' => 'extraName']);
$userCollection->pluck('name')->all(); // ['extraName', 'extraName']

$user = UserFactory::new()->makeMany(2, ['name' => 'extraName']);
$userCollection->pluck('name')->all(); // ['extraName', 'extraName']

createMany()makeMany()$extracreate()make() 略有不同。在这里,您也可以传递一个嵌套数组,为集合中的模型提供不同的数据。

$userCollection = UserFactory::new()->createMany(2, [['name' => 'firstName'], ['name' => 'secondName']]);
$userCollection->pluck('name')->all(); // ['firstName', 'secondName']
$userCollection = UserFactory::new()->createMany(3, [['name' => 'firstName'], ['name' => 'secondName']]);
$userCollection->pluck('name')->all(); // ['firstName', 'secondName', '<value-from-fakeData>']

✨ 小贴士

如果您想获得更易于阅读的API,可以在工厂类中添加一些小辅助设置器,如下所示

use Thettler\LaravelFactoryClasses\FactoryClass;

class UserFactory extends FactoryClass {
      /*...*/

    public function name(string $name): self
    {
        return $this->addData('name', $name);
    }
}
 $user = UserFactory::new()
            ->name('myName')
            ->create();
 echo $user->name; // myName

关系

创建具有关系的模型通常有些繁琐。本包提供了简单而强大的关系系统,该系统使用模型上已定义的关系。

所有关系基本上工作方式相同,它们接受关系名称作为第一个参数,接受 FactoryClassModel 作为第二个参数。它们还有一个第三个参数,允许您挂钩到关系并修改其行为,但我们将保留这部分内容以供高级部分

如果您提供 FactoryClass 作为第二个参数,则关系将在每次调用 create()createMany()make()makeMany() 时从 FactoryClass 创建一个新的模型,并将其附加到模型关系。

⚠️ 与 data()addData() 一样,所有关系方法都会为您提供工厂的新实例以防止副作用。

对于所有示例,我们假设模型有一个名为 company 的公司关系。

HasOne

class User extends Model {
    public function company(): HasOne {
        return $this->hasOne(Company::class);
    }
}
$user = UserFactory::new()->hasOne('company', CompanyFactory::new())->create();
$user->company; // Some new created Company
// Or
$company = Company::find('xy');
$user = UserFactory::new()->hasOne('company', $company)->create();
$user->company; // Company XY

HasMany

hasMany() 方法接受一个包含 FactoryClassesModels 的数组作为第二个参数。

class User extends Model {
    public function companies(): HasMany {
        return $this->hasMany(Company::class);
    }
}
$user = UserFactory::new()->belongsTo('companies',[CompanyFactory::new()])->create();
$user->companies[0]; // Some new created Company
// Or
$company = Company::find('xy');
$user = UserFactory::new()->belongsTo('companies', [$company])->create();
$user->companies[0]; // Company XY

BelongsTo

class User extends Model {
    public function company(): BelongsTo {
        return $this->belongsTo(Company::class);
    }
}
$user = UserFactory::new()->belongsTo('company', CompanyFactory::new())->create();
$user->company; // Some new created Company
// Or
$company = Company::find('xy');
$user = UserFactory::new()->belongsTo('company', $company)->create();
$user->company; // Company XY

BelongsToMany

hasMany() 一样,belongsToMany() 也接受一个包含 FactoryClassesModels 的数组

class User extends Model {
    public function companies(): BelongsToMany {
        return $this->belongsToMany(Company::class);
    }
}
$user = UserFactory::new()->belongsToMany('companies', CompanyFactory::new())->create();
$user->companies[0]; // Some new created Company
// Or
$company = Company::find('xy');
$user = UserFactory::new()->belongsToMany('companies', $company)->create();
$user->companies[0]; // Company XY

如果您想为 belongsToMany() 关系添加枢轴数据,可以使用第三个参数。它是一个回调,为您提供 BelongsToManyFactoryRelation 实例。在这个类中,您可以调用 pivot() 方法并返回它以添加您的枢轴数据。有关第三个参数的更多信息,请参阅高级部分

class User extends Model {
    public function companies(): BelongsToMany {
        return $this->belongsToMany(Company::class)->withPivot('role');
    }
}
$user = UserFactory::new()
         ->belongsToMany(
            'companies', 
            CompanyFactory::new(),
            fn(BelongsToManyFactoryRelation $relation) => $relation->pivot(['role' => 'manager'])
         )
         ->create();
$user->companies[0]->role; // manager

MorphTo

class User extends Model {
    public function company(): MorphTo {
        return $this->morphTo(Company::class);
    }
}
$user = UserFactory::new()->morphTo('company', CompanyFactory::new())->create();
$user->company; // Some new created Company
// Or
$company = Company::find('xy');
$user = UserFactory::new()->morphTo('company', $company)->create();
$user->company; // Company XY

MorphOne

class User extends Model {
    public function company(): MorphOne {
        return $this->morphOne(Company::class);
    }
}
$user = UserFactory::new()->morphOne('company', CompanyFactory::new())->create();
$user->company; // Some new created Company
// Or
$company = Company::find('xy');
$user = UserFactory::new()->morphOne('company', $company)->create();
$user->company; // Company XY

✨ 小贴士

在您的FactoryClass中创建一些带有默认值的辅助函数,以便您可以无需明确提供FactoryClass或模型即可调用它们。

use Thettler\LaravelFactoryClasses\FactoryClass;

class UserFactory extends FactoryClass {
      /*...*/

    public function withCompany($company = null): self
    {
        return $this->belongsTo('company', $company ?? CompanyFactory::new());
    }
}
 $user = UserFactory::new()
            ->withCompany()
            ->create();
 echo $user->company; // Company XY

或者对于多个关系

use Thettler\LaravelFactoryClasses\FactoryClass;

class UserFactory extends FactoryClass {
      /*...*/

    public function withCompanies(...$companies): self
    {
        return $this->belongsToMany('companies', empty($companies) ? [CompanyFactory::new()] : $companies);
    }
}
 $user = UserFactory::new()
            ->withCompanies()
            ->create();
 echo $user->companies[0]; // Company

如果您在模型的关联上定义了返回类型,命令将自动为您生成这些方法。

class User extends Model {
    // Would generate a withCompany() method on your factory
    public function company(): BelongsTo {
        return $this->belongsto(Company::class);
    }
    // Would not generate a withCompany() method on your factory
    public function company() {
        return $this->belongsto(Company::class);
    }
}

⚠️ 自动方法创建不适用于MorphTo关系

禁用假数据生成

如果您不希望为模型生成假数据,可以在工厂上使用withoutFakeData()方法。

 $user = UserFactory::new()
            ->withoutFakeData()
            ->make();

🔨 高级

自定义关系

每个关系都有其专门的类,该类负责创建关系。您可以通过使用关系函数的第三个参数来挂钩到该类并修改它。这是一个可调用对象,它接收一个FactoryRelation实例并返回一个FactoryRelation实例。

use Thettler\LaravelFactoryClasses\FactoryClass;
use App\User;

class UserFactory extends FactoryClass {

         /* ... */

        public function withCompany ($company) {
            return $this->belongsTo(
                'company', 
                $company, 
                fn(\Thettler\LaravelFactoryClasses\Relations\BelongsToFactoryRelation $relation) => $relation
                ->type('before') // after|before this indicates if the relation creation should take place before or after the main Model has ben created and saved to the DB
                ->relation('diffrentCompany') // change the name of the relation that gets used so here it changes from 'company' to 'differentCompany'
                ->factories(CompanyFactory::new()->withSpecialState()) // Lets you add an one or more Factories to the Relation !! only the many relations using mor then one factory
                ->models(Company::find('xy')) // Same as factories() only for Models 
            );
        }
}

使用自定义关系

如果您需要更复杂的关系,您可以编写自己的FactoryRelations并在工厂中使用它们。为此,在您的FactoryClass上使用with()方法,并通过第一个参数传递您的关联。

use Thettler\LaravelFactoryClasses\FactoryClass;
use App\User;

class UserFactory extends FactoryClass {

         /* ... */

        public function withCompany ($company) {
            return $this->with(MyFactoryRelation::new('company', $company));
        }
}

要创建您的FactoryRelation,创建一个新的PHP类并扩展FactoryRelation类。定义抽象方法,然后您就可以使用了。有关更多示例,请查看src/Relations

namespace Thettler\LaravelFactoryClasses\Relations;

use Illuminate\Database\Eloquent\Model;
use Thettler\LaravelFactoryClasses\FactoryRelation;

class BelongsToFactoryRelation extends FactoryRelation
{
    protected string $type = self::BEFORE_TYPE; // after|before this indicates if the relation creation should take place before or after the main Model has ben created and saved to the DB

    /**
    * This Method will be used if the make() or makeMany() method gets used it gets the current Model (in our case User) and returns this Model again
    * 
    * @param Model $model
    * @return Model
    * @throws \Thettler\LaravelFactoryClasses\Exceptions\FactoryException
    */
    public function make(Model $model): Model
    {
        $relative = $this->convertRelative($this->relatives[0], 'make');
        return $model->setRelation($this->relation, $relative);
    }

    /**
    * This Method will be used if the create() or createMany() method gets used it gets the current Model (in our case User) and returns this Model again
    * 
    * @param Model $model
    * @return Model
    * @throws \Thettler\LaravelFactoryClasses\Exceptions\FactoryException
    */
    public function create(Model $model): Model
    {
        $relation = $this->relation;
        $relative = $this->convertRelative($this->relatives[0]);
        $model->$relation()->associate($relative)->save();
        return $model;
    }
}

运行测试

./vendor/bin/phpunit

作者

👤 托比亚斯·赫特勒

🤝 贡献

欢迎贡献、问题和功能请求!
请随意查看问题页面

展示您的支持

如果这个项目对您有帮助,请点一个⭐️!

📝 许可证

版权所有 © 2020 托比亚斯·赫特勒
本项目遵循MIT许可证。

此README是用❤️由readme-md-generator生成的