#php #laravel #laravel-factory
#php #laravel #laravel-фабрика
Вопрос:
В Laravel 8 можно быстро заполнить отношения с фабриками. Однако я не могу понять, как сгенерировать более одной связи. Как я могу создать случайное или новое отношение для каждой ссылки, используя новый синтаксис Laravel 8?
Этот фабричный синтаксис доступен только в Laravel 8.https://laravel.com/docs/8.x/database-testing#factory-relationships
Проблема
Рассмотрим следующую взаимосвязь:
- Каждая ссылка принадлежит веб-сайту и публикации.
- Оба веб-сайта и сообщения могут содержать много ссылок.
<?php
class Post extends Model
{
use HasFactory;
function links()
{
return $this->hasMany(Link::class);
}
}
class Website extends Model
{
use HasFactory;
function links()
{
return $this->hasMany(Link::class);
}
}
class Link extends Model
{
use HasFactory;
function post()
{
return $this->belongsTo(Post::class);
}
function website()
{
return $this->belongsTo(Website::class);
}
}
Что я пробовал / хочу
То, что я попробовал ниже, сгенерирует только одну модель для всех ссылок. Как я могу создать случайное или новое отношение для каждой ссылки, используя новый синтаксис Laravel 8?
Link::factory()->count(3)->forPost()->forWebsite()->make()
=> IlluminateDatabaseEloquentCollection {#4354
all: [
AppModelsLink {#4366
post_id: 1,
website_id: 1,
},
AppModelsLink {#4395
post_id: 1, // return a different ID
website_id: 1,
},
AppModelsLink {#4370
post_id: 1, // return a different ID
website_id: 1, // return a different ID
},
],
}
Ответ №1:
Просто добавьте это в свой LinkFactory
:
public function definition()
{
return [
'post_id' => function () {
return Post::factory()->create()->id;
},
.....
];
}
И теперь вы можете создавать новую запись для каждой новой ссылки:
Link::factory()->count(3)->create();//Create 3 links with 3 new posts
или прикрепите новые ссылки к существующему сообщению:
Link::factory()->count(3)->create(['post_id' => Post::first()->id]); //create 3 links and 0 new posts
Ответ №2:
В Laravel 9 вы можете использовать этот макрос:
// database/factoryMacros.php
<?php
namespace DatabaseSupport;
use IlluminateDatabaseEloquentFactoriesBelongsToRelationship;
use IlluminateDatabaseEloquentFactoriesFactory;
use IlluminateDatabaseEloquentModel;
use IlluminateSupportStr;
/** @param Factory|Model $factory */
Factory::macro('hasParent', function (mixed $factory, string $relationship = null): self {
return $this
->state(function () use ($factory, $relationship): array {
$belongsTo = new BelongsToRelationship(
factory: $factory,
relationship: $relationship ?? guessBelongsToMethodName($factory),
);
return $belongsTo
->recycle(recycle: $this->recycle)
->attributesFor(model: $this->newModel());
});
});
Factory::macro('hasChildren', fn (...$arguments): self => $this->has(...$arguments));
Factory::macro('hasChild', fn (...$arguments): self => $this->has(...$arguments));
/** @param Factory|Model $factory */
function guessBelongsToMethodName(mixed $factory): string
{
$modelName = is_subclass_of($factory, Factory::class)
? $factory->modelName()
: $factory::class;
return Str::camel(class_basename($modelName));
}
Использование
Используйте метод hasParent($factory)
вместо for($factory)
:
// Creates 3 Link, 3 Post, 3 Website
Link::factory()
->count(3)
->hasParent(Post::factory())
->hasParent(Website::factory())
->make();
Вы также можете использовать hasChildren($factory)
или hasChild($factory)
вместо has
для обеспечения согласованности имен:
// Creates 3 Post, 3 Link
Post::factory()
->count(3)
->hasChild(Link::factory())
->make();
Синтаксис макросов такой же, как for
и has
.
Вы можете явно определить имя связи, передать сложные цепочки фабрики, передать конкретную модель и использовать ее с recycle
, например.
Установка
- Добавьте файл в свой
composer.json
:
{
...
"autoload": {
"files": [
"database/factoryMacros.php"
]
}
}
- Запустите
composer dump-autoload
, чтобы перезагрузить файл composer.
В качестве альтернативы вы можете зарегистрировать макрос как сервис или загрузить его как mixin.
PS: Я намерен создать библиотеку для этого в будущем.
Тесты
/**
* Use "DatabaseEloquentFactoryTest.php" as base:
* https://github.com/laravel/framework/blob/de42f9987e01bfde50ea4a86becc237d9c8c5c03/tests/Database/DatabaseEloquentFactoryTest.php
*/
class FactoryMacrosTest extends TestCase
{
function test_belongs_to_relationship()
{
$posts = FactoryTestPostFactory::times(3)
->hasParent(FactoryTestUserFactory::new(['name' => 'Taylor Otwell']), 'user')
->create();
$this->assertCount(3, $posts->filter(function ($post) {
return $post->user->name === 'Taylor Otwell';
}));
$this->assertCount(3, FactoryTestUser::all());
$this->assertCount(3, FactoryTestPost::all());
}
}
TL; DR;
В Laravel 9 этого достичь невозможно. for()
Использует единую модель для всех экземпляров.
Есть PR, чтобы исправить это поведение, но PR был закрыт, и я не уверен, что он когда-либо будет реализован:https://github.com/laravel/framework/pull/44279
Ответ №3:
Метод laravel magic factory for
позволяет вам заполнить базу данных одной записью из внешней таблицы. Смотрите ссылку на документацию https://laravel.com/docs/8.x/database-testing#belongs-to-relationships
В вашем случае использование forPost()
и forWebsite()
позволит вам заполнить базу данных одним идентификатором из таблицы Post и таблицы Website.
Если вы хотите использовать разные идентификаторы, используйте вместо этого этот синтаксис Link::factory()->count(3)->make()
Ответ №4:
У меня была похожая проблема, и я смог заставить ее работать только тогда, когда я подключился к afterCreating()
на одной фабрике. Это позволяет мне создавать / сохранять идентификатор каждой модели, а затем присоединять к модели ссылок
Я выбираю начать с WebsiteFactory, но вы также можете начать с PostFactory, поскольку это модели «самого высокого родителя». Если вы попытаетесь создать ссылку без website_id и post_id, я полагаю, вы получите сообщение об ошибке с запросом для обоих.
class WebsiteFactory extends Factory
{
public function definition(){...}
public function configure()
{
return $this->afterCreating( function (Website $website){
// the website model is created, hence after-creating
// attach Website to a new Post
$post = Post::factory()->hasAttached($website)->create();
// create new links to attach to both
$links = Link::factory()->for($website)->for($post)->count(3)->create();
});
Вы можете оставить PostFactory и LinkFactory как простые блоки определения (или добавить другие материалы, если хотите). Теперь, когда вы создаете новый веб-сайт через фабрику, он создаст новую запись и 3 новые ссылки. Например, теперь вы можете запустить
php artisan tinker
$w = Website::factory()->create(); // one website-one post-3 links
$ws = Website::factory()->count(5)->create(); // 5 website-5 post- 15 links
Ознакомьтесь с обратными вызовами Factory здесь (документы 9.x, но они также есть в 8.x):
https://laravel.com/docs/9.x/database-testing#factory-callbacks
Ответ №5:
AppModelsCategory::factory(10)
->has(Product::factory()->count(10), 'products')
->create();
Комментарии:
1. Ваш ответ может быть улучшен путем добавления дополнительной информации о том, что делает код и как это помогает OP.
2. вы можете указать категорию, а затем назначить дочернему продукту (в нашем случае) имя объекта в дочерней модели
Ответ №6:
Было бы лучше, если бы вы поиграли с кодом. Вы поймете лучше.
$user = User::factory()
->has(Post::factory()->count(3), 'posts')
->create();
Приведенный выше код создаст три сообщения для одного пользователя. Он вставит три строки post и строку user. С другой стороны, приведенный ниже код, кажется, будет вставлен в три сообщения для пользователя с именем Джессика Эрчер, то есть он не будет вставлять пользователя.
$posts = Post::factory()
->count(3)
->for(User::factory()->state([
'name' => 'Jessica Archer',
]))
->create();