Laravel 8 множественные отношения для фабрики

#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 , например.

Установка

  1. Добавьте файл в свой composer.json :
 {
  ...
  "autoload": {
    "files": [
      "database/factoryMacros.php"
    ]
  }
}
  
  1. Запустите 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();