Giter VIP home page Giter VIP logo

laravel-factories-reloaded's Introduction

Laravel Factories Reloaded 🏭

Latest Version on Packagist v1 Total Downloads MIT Licensed

This package generates class-based model factories, which you can use instead of the ones provided by Laravel.

Screenshot of the command

Laravel 8 Support

The new version v3 now supports Laravel 8 and PHP 8+. Since L8 changed their own implementation of factories a lot, this new v2 version ONLY works with Laravel 8. If you are not using L8 yet, use the latest 1.* version of this package, if you need PHP 7 support, use the latest 2.* version.

Benefits

  • use the features you already love from Laravel factories (create, make, times, states)
  • automatically create new class factories for specific or All your models
  • automatically import defined default data and states from your Laravel factories
  • and many more...

📺 I've recorded some videos to give you an overview of the features.

⚠️ Note: Interested in WHY you need class-based factories? Read here.

Installation

You can install the package via composer:

composer require --dev christophrumpel/laravel-factories-reloaded

To publish the config file run:

php artisan vendor:publish --provider="Christophrumpel\LaravelFactoriesReloaded\LaravelFactoriesReloadedServiceProvider"

It will provide the package's config file where you can define multiple paths of your models, the path of the newly generated factories, the namespace of your old Laravel factories, as well as where your old Laravel factories are located.

Configuration

Laravel Factories Namespace

Factories are namespaced since Laravel 8, and the default factories namespace is Database\Factories. If your laravel factories namespace is Database\Factories\ClassFactories, you can customize it at the config file as below:

'vanilla_factories_namespace' => 'Database\Factories\ClassFactories',

It will resolve your old laravel factory class as Database\Factories\ClassFactories\UserFactory, instead of Database\Factories\UserFactory.

Usage

Generate Factories

First, you need to create a new factory class for one of your models. This is done via a newly provided command called make:factory-reloaded.

php artisan make:factory-reloaded

You can pick one of the found models or create factories for all of them.

Command Options

If you want to define options through the command itself, you can do that as well:

php artisan make:factory-reloaded --models_path="app/Models"  --factories_path="tests/ClassFactories" --factories_namespace="Tests\ClassFactories"

Currently, you can only define one location for your models this way.

Define Default Model Data

Similar to Laravel factories, you can define default data for your model instances. Inside your new factories, there is a getDefaults method defined for that. The Faker helper to create dummy data is available as well.

public function getDefaults(Faker $faker): array
{
    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'email_verified_at' => now(),
        'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
        'remember_token' => Str::random(10),
        'active' => false,
    ];
}

Use New Factories

Let's say you have created a new user factory. You can now start using it and create a new user instance. Similar to Laravel factories, the create method will persist in a new model.

$user = UserFactory::new()->create();

If you like to get an instance that is not persisted yet, you can choose the make method.

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

To create multiple instances, you chain the times() method before the create or make method.

$users = UserFactory::new()
    ->times(4)
    ->create();

States

You may have defined states in your old Laravel factories.

$factory->state(User::class, 'active', function () {
    return [
        'active' => true,
    ];
});

While creating a new class factory, you will be asked if you like those states to be imported to your new factories. If you agree, you can immediately use them. The state active is now a method on your UserFactory.

$user = UserFactory::new()
    ->active()
    ->create();

Relations

Often you will need to create a new model instance with related models. This is now pretty simple by using the with method:

$user = UserFactory::new()
    ->with(Recipe::class, 'recipes', 3)
    ->create();

Here were are getting a user instance that has three related recipes attached. The second argument here defines the relationship name.

⚠️ Note: For this to work, you need to have a new RecipeFactory already created.

You can also define extras for the related models when using related model factories directly.

$user = UserFactory::new()
    ->withFactory(RecipeFactory::new()->withCustomName(), 'recipes', 3)
    ->create();

You can create many related models instances by chaining withs.

$recipe = RecipeFactory::new()
    ->with(Group::class, 'group')
    ->with(Ingredient::class, 'ingredients')
    ->with(Ingredient::class, 'ingredients', 3)
    ->create();

Here we are getting a recipe that has a group and four ingredients.

⚠️ Note: Up to the version 1.0.8, only the last with relation is built.

In Laravel factories, you could also define a related model in your default data like:

$factory->define(Ingredient::class, function (Faker $faker) {
    return [
        'name' => $faker->name,
        'recipe_id' => factory(Recipe::class),
    ];
});

This can also be achieved in our new factory classes.

public function getDefaults(Faker $faker): array
{
    return [
        'name' => $faker->name,
        'recipe_id' => factory(Recipe::class),
    ];
}

Or even better through an instance of a new factory class.

public function getDefaults(Faker $faker): array
{
    return [
        'name' => $faker->name,
        'recipe_id' => RecipeFactory::new(),
    ];
}

⚠️ Note: I wouldn't recommend any of these options because you do not see that additional models are persisted in your tests. Please use the given "with" method create a dedicated method for creating a relation yourself.

Callbacks

In Laravel, you are able to define factory callbacks for afterCreating and afterMaking. You can do something similar also with factory classes. Since both the make and create method are inside your factory class, you are free to add code there:

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

public function make(array $extra = []): Group
{
    return $this->build($extra, 'make');
}

It depends on what you want to achive, but personally I would add a method to your factory which you call from within your test. This way it is more obvious what is happening.

Immutability

You might have noticed that when this package imports a state for you, it will clone the factory before returning.

public function active(): UserFactory
{
    return tap(clone $this)->overwriteDefaults([
        'active' => true,
    ]);
}

This is recommended for all methods which you will use to setup your test model. If you wouldn't clone the factory, you will always modify the factory itself. This could lead into problems when you use the same factory again.

To make a whole factory immutable by default, set the $immutable property to true. That way, every state change will automatically return a cloned instance.

class UserFactory
{
    protected string $modelClass = User::class;
    protected bool $immutable = true;

    // ...

    public function active(): UserFactory
    {
        return $this->overwriteDefaults([
            'active' => true,
        ]);
    }
}

In some context, you might want to use a standard factory as immutable. This can be done with the immutable method.

$factory = UserFactory::new()
    ->immutable();

$activeUser = $factory
    ->active()
    ->create();

$inactiveUser = $factory->create();

Note: with and withFactory methods are always immutable.

What Else

The best thing about those new factory classes is that you own them. You can create as many methods or properties as you like to help you create those specific instances that you need. Here is how a more complex factory call could look like:

UserFactory::new()
    ->active()
    ->onSubscriptionPlan(SubscriptionPlan::paid)
    ->withRecipesAndIngredients()
    ->times(10)
    ->create();

Using such a factory call will help your tests to stay clean and give everyone a good overview of what is happening here.

Why Class-Based Factories?

  • They give you much more flexibility on how to create your model instances.
  • They make your tests much cleaner because you can hide complex preparations inside the class.
  • They provide IDE auto-completion which you do not get have with Laravel factories.

Testing

composer test

Changelog

Please see CHANGELOG for more information about what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security

If you discover any security-related issues, please email [email protected] instead of using the issue tracker.

Credits

Some of the implementations are inspired by Brent's article about how they deal with factories at Spatie.

And a big thanks goes out to Adrian who helped me a lot with refactoring this package.

License

The MIT License (MIT). Please see License File for more information.

laravel-factories-reloaded's People

Contributors

barnabaskecskes avatar bertvanhoekelen avatar christophrumpel avatar code-distortion avatar dbushy727 avatar deligoez avatar devmsh avatar edgrosvenor avatar erannl avatar guidohendriks avatar irakan avatar joshmoreno avatar lloople avatar mohammedalkutrani avatar nuernbergera avatar pkboom avatar shaffe-fr avatar simonschaufi avatar szepeviktor avatar thijsvdanker avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

laravel-factories-reloaded's Issues

Using model in data like in Laravel factories

In Laravel factories you can do this:

$user = User::first();

factory(Group::class)->create([
    'user_id' => $user,
]);

I would suggest adding this here as well. This would make my life easier at least, as I'm using stuff like this pretty often:

TeamFactory::new()->create([
    'user_id' => UserFactory::new()->create([
        'name' => 'Foo bar',
    ]),
]);

Which doesn't work so I have to do this what is pretty ugly:

TeamFactory::new()->create([
    'user_id' => UserFactory::new()->create([
        'name' => 'Foo bar',
    ])->id,
]);

I could do the work, just wanted to know what you think about it before I do.

Creating a model instance with a relation but generating extra data without relation

Hi,
that's say I create n model instances and each with a relation, but there output 2n rows of data in the table, and n rows in the relation table.

If I run ProductFactory::new()->withFactory(InfoFactory::new(),'info')->create(); from tinker.
The products table:

id    name    price
1    wine    100
2    bread    50

The infos table:

id    product_id    description
1    1    lkdakdlkdl;ks

If I run ProductFactory::new()->withFactory(InfoFactory::new(),'info',1)->times(10)->create(); from tinker.
The products table:

id    name    price
1    wine    100
2    bread    50
3    meat    15
...(skip 4 to 18)...
19 fish 20
20 pizza 80

The infos table:

id    product_id    description
1    1    yummy
2    3    good
3    5    soft
4    7    bad
...(skip 5 to 7)
8    15    healthy
9    17    funny
10    19    delicious

These are two models:

class Product extends Model
{
    public function info()
    {
        return $this->hasMany(Info::class);
    }
}
class Info extends Model
{
    public function product(){
        return $this->belongsTo(Product::class);
    }
}

The return of getDefaults method of ProductFactory:

return [
    'name' => $faker->name,
    'price' => $faker->numberBetween(1, 100),
];

The return of getDefaults method of InfoFactory:

return [
    'product_id' => ProductFactory::new(),
    'description' => $faker->text(15),
];

I don't know why, any idea?

Nested model configuration

Is it currently possible to achieve something like this?

We have the relationship:

Order -> hasMany -> OrderItems

I want to create an order with a status of new and then create 4 order items with a status of allocated. Is it possible to hook into the orderItemFactory method from within the with method, and apply the allocated status?

// Test
OrderFactory::new()
    ->withOrderItems(5)
    ->create();

// OrderFactory.php
public function withOrderItems($qty)
{
    return $this->with(
        OrderItem::class,
        'orderItems',
        $qty
    );
}

// OrderItemFactory.php
public function allocated()
{
    return tap(clone $this)->overwriteDefaults([
        'state' => 'allocated',
    ]);
}

Deep nested relationships don't get persisted

Hi, I have quite a deep tree of objects I want to test and realized that only the outer object with ONE level deeper gets persisted but from the 2nd level on the objects are only relations to the parent object but are NOT in the database.

My code looks like this:

$root = FactoryA::new()
    ->withFactory(
        FactoryB::new()
            ->withFactory(
                FactoryC::new()
                    ->withFactory(
                        FactoryD::new()
                        'd'
                    ),
                'c'
            ),
        'b'
    )
    ->create();

Now I want to run database tests on D records but they are not persisted. Is there a workaround to persist them afterwards, is this intended behavior or is this a bug?

The problem lies here that deeper levels are called with "make" instead of "create":

$relatedModels = $factories->map->make();
because the $creationType parameter gets overwritten with "make"

PHP 8 Support

Is there any chance you can bump require version to support PHP 8?

It's great that you've made things compatible with with Laravel 8, perhaps something in keeping with their approach ("^7.4|^8.0")?

Thanks for the hard work on this package, love it. Not a fan of the Laravel 8 factories approach.

Generate Command Produced A Class with Extra ")" on return statement

Hey Ive Experimenting on your package

this is my laravel factories

<?php

/**
 * @var \Illuminate\Database\Eloquent\Factory $factory
 */

use App\Models\User;
use Illuminate\Support\Str;
use Faker\Generator as Faker;

/*
|--------------------------------------------------------------------------
| 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) {
    // JSON string
    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)
    ];
});

$factory->state(User::class, 'referrals', function () {
    $referralsJson = '[{"name":"Jonathan Suh","id":1},{"name":"William Philbin","id":2},{"name":"Allison McKinnery","id":3}]';

    return [
        'referrals' => $referralsJson
    ];
});

$factory->state(User::class, 'settings', function () {
    $settingsArray = [
        'api_key'    => 'test',
        'secret_key' => 'secret'
    ];
    $settingsString = json_encode($settingsArray);

    return [
        'settings' => $settingsString
    ];
});

$factory->state(User::class, 'address', function () {
    $addressArray = [
        'no'        => 53,
        'street'    => 'Santol Ext.',
        'country'   => 'Philippines',
        'region'    => 3,
        'province'  => 'Zambales',
        'city'      => 'Olongapo',
        'baranggay' => 'New Cabalan',
        'zip'       => 12345
    ];
    $addressString = json_encode($addressArray);

    return [
        'address' => $addressString

    ];
});

$factory->state(User::class, 'payment', function () {
    return [
        'payment' => [
            'name'   => 'BDO',
            'amount' => 12000,
            'paid'   => true
        ]
    ];
});

The Generate Factory Afterwards has This Typo

<?php

namespace Tests\Factories;

use App\Models\User;
use Christophrumpel\LaravelFactoriesReloaded\BaseFactory;
use Faker\Generator as Faker;
use Illuminate\Support\Str;
class UserFactory extends BaseFactory
{

    protected string $modelClass = User::class;

    public function create(array $extra = []): User
    {
        return parent::build($extra);
    }

    public function make(array $extra = []): User
    {
        return parent::build($extra, 'make');
    }

    public function getDefaults(Faker $faker): array
    {
        // JSON string
        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)
        ];

    }
    public function referrals(): UserFactory
    {
        $clone = clone $this;
        $referralsJson = '[{"name":"Jonathan Suh","id":1},{"name":"William Philbin","id":2},{"name":"Allison McKinnery","id":3}]';
    
        return [
            'referrals' => $referralsJson
        ]);
    
        return $clone;
    }
    public function settings(): UserFactory
    {
        $clone = clone $this;
        $settingsArray = [
            'api_key'    => 'test',
            'secret_key' => 'secret'
        ];
        $settingsString = json_encode($settingsArray);
    
        return [
            'settings' => $settingsString
        ]);
    
        return $clone;
    }
    public function address(): UserFactory
    {
        $clone = clone $this;
        $addressArray = [
            'no'        => 53,
            'street'    => 'Santol Ext.',
            'country'   => 'Philippines',
            'region'    => 3,
            'province'  => 'Zambales',
            'city'      => 'Olongapo',
            'baranggay' => 'New Cabalan',
            'zip'       => 12345
        ];
        $addressString = json_encode($addressArray);
    
        return [
            'address' => $addressString
    
        ]);
    
        return $clone;
    }
    public function payment(): UserFactory
    {
        $clone = clone $this;
        $clone->overwriteDefaults([
            'payment' => [
                'name'   => 'BDO',
                'amount' => 12000,
                'paid'   => true
            ]
        ]);
    
        return $clone;
    }
}

Add optional model argument

I have a project with dozens of models. Selecting them from a list would be cumbersome. Would you be open to accepting a PR that adds an optional argument to which one would pass the model and, if provided, would skip the selection menu and just create the factory? I'd be happy to add this to the command and update the docs to reflect the change.

Getting this exception when I run composer update

> Illuminate\Foundation\ComposerScripts::postUpdate
> php artisan ide-helper:generate
Exception: get_class() expects parameter 1 to be object, null given
Skipping \Christophrumpel\LaravelCommandFilePicker\LaravelCommandFilePickerFacade.

->times() breaks most functionality added to factory

UserFactory::new()
    ->with(\App\Review::class, 'reviews', 10)
    ->times(10)
    ->create();

Expected
10 users with 10 reviews each

Actual
10 users with 0 reviews

It looks like ->times() is just getting the attribute data and creating models from that, but the factories might have additional methods, properties, and logic in the create/make methods. Might be able to fix this and simplify the code by cloning the factory X times, passing it to CollectionFactory, and then calling create/make on each clone individually. Any thoughts or concerns before I work on a PR in the next few days?

Cannot resolve factories when vanilla factory namespace is not Database\Factories\

Here is my vanilla factory:

namespace Database\Factories\Zoo;

class AnimalFactory extends Factory
{

}

when I did factory-reloaded, there showed an error:
github

I think is because in LaravelFactoryExtractor.php, there use class_basename($className) (i.e., AnimalFactory) to resolve factory class name, so it tried to resolve the file Database\Factories\AnimalFactory, instead of Database\Factories\Zoo\AnimalFactory.

I hope this pr can help to fix it.

php artisan make:factory-reloaded command produce this error

php artisan make:factory-reloaded 
 Please pick a model: 
  [0] App\Models\User 
 > 
 [ERROR] Value "" is invalid 
 Please pick a model: 
  [0] App\Models\User 
 > 0 
   TypeError 
  Argument 1 passed to Christophrumpel\LaravelFactoriesReloaded\LaravelFactoryExtractor::getClosureContent() must be callable, array given, called in /home/uriah/www/hyip/vendor/christophrumpel/laravel-factories-reloaded/src/LaravelFactoryExtractor.php on line 208 
  at vendor/christophrumpel/laravel-factories-reloaded/src/LaravelFactoryExtractor.php:70 
    66|             )->implode('    ') 
    67|         ); 
    68|     } 
    69| 
  > 70|     protected function getClosureContent(callable $closure): array 
    71|     { 
    72|         $reflFunc = new ReflectionFunction($closure); 
    73|         $this->parseUseStatements($reflFunc); 
    74|         $startline = $reflFunc->getStartLine(); 
      +1 vendor frames 
  2   [internal]:0 
      Christophrumpel\LaravelFactoriesReloaded\LaravelFactoryExtractor::Christophrumpel\LaravelFactoriesReloaded\{closure}() 
      +5 vendor frames 
  8   [internal]:0 
      Christophrumpel\LaravelFactoriesReloaded\FactoryCollection::Christophrumpel\LaravelFactoriesReloaded\{closure}() 

$extra data not work with times

When override getDefaults data with times function, seem to be empty array in $extra variable

UserFactory::new()
    ->times(10)
    ->create(['age' => 30]);

Bug: locale from app.faker_locale not loaded

In the default factories from Laravel.

I can specify my faker_locale in config/app.php to be something like

'faker_locale' => 'en_SG',

And I get to do:

'mobile_phone' => $faker->unique()->mobileNumber,

However in your Factory classes, I will just get the following error:

InvalidArgumentException with message 'Unknown formatter "mobileNumber"'

I need to do this in the getData method for it to work.

public function getData(Generator $faker): array
    {
        $faker->addProvider(new \Faker\Provider\en_SG\PhoneNumber($faker));

        return [
            'mobile_phone' => $faker->unique()->mobileNumber,
            // other items.
        ];
    }

It will be good if you auto add the locale we chose in config('app.faker_locale') like what default Laravel does.

Thank you

Publish Config

Can you document how to publish the config? I have my models in a Models directory and can't figure out how to change the config so factories-reloaded can find them.

Mass Assignment

Hi,

On Laravel's factories, Mass assignment protection is automatically disabled when creating models on our tests (https://laravel.com/docs/7.x/database-testing).

It seems like it's not the case with this package, on my model i have columns on the $guarded protected property, and i can't set them with :

CompanyFactory::new()->create([
    'name' => 'Yannick',
]);

Here the name will not be "Yannick" but the default value set on the Factory itself.

No models in list

I have a brand new laravel installation with two models and a weird behavior. When I call php artisan make:factory-reloaded I get the following output

 Please pick a model:
  [0] 

with a blinking prompt next to the [0] and no chance to interact. I have to shut down the terminal.
Any ideas?

Further development of this packages:

After released of Laravel 8 with class based factory, we can do almost everything with laravel itself that this packages intended to do. For example, here is my laravel 8 Factory class that I recently moved from this package to Laravel Factory Class.

<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Database\Eloquent\Model;
use App\Domain\Artefact\Models\Jobs;
use App\Domain\Users\Enums\Role;
use Tests\Factories\UserFactory;
use Illuminate\Support\Str;
use Carbon\Carbon;

class JobsFactory extends Factory
{
    protected $user;

    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = Jobs::class;

    /**
     * Define the model's default state.
     *
     * @return array
     */
    public function definition()
    {
        $title = $this->faker->sentence;
        $user  = $this->user ?? UserFactory::new()->withRole(Role::COMPANY)->create();
        return [
            'user_id' => $user->id,
            'status' => 'Publish',
            'verification' => 'Verified'
        ];
    }

    public function setUser($user)
    {
        $this->user  = $user;

        return $this;
    }

    public function create($attributes = [], ?Model $parent = null)
    {
        $job = parent::create($attributes, $parent);

        // Futher create relationship

        return $job;
    }
}

So How do you move further along with this packages ?

Support closures

public function getDefaults(Faker $faker): array
{
    return [
        'name' => $faker->name,
        'country_id' => CountryFactory::new(), // this will still be created in database even if I overwrite it
    ];
}
public function country(Country $country)
{
    return tap(clone $this)->overwriteDefaults([
        'country_id' => $country->id,
    ]);
}

If I used the country method, it will actually overwrite the country_id value but it will still create a Country instance that was already in the defaults and save it in database.

UserFactory::new()->country($country)->create();
dd(count(Country::all())); // 2

Before, when I was using the default laravel factories I used closures to solve this issue:

return [
    'name' => $faker->name,
    'country_id' => function () {
        return factory(Country::class)->create()->id;
    },
];

Some options for states???

Hey @christophrumpel thanks for a good package.

I want to know your opinion on how to handle the states in the factories, for now I and trying with

  • one class for each state (which can be many classes for some models)
  • overwriting the attribute (each time that a no default state is needed is necessary to overwrite)

I'm thinking is something like an apply method that accept a state string and mutate the data.

Save related models where foreign key is not nullable

The current implementation of with() and the examples in the docs only work when the foreign key is nullable() like group_id on the recipes table.

        Schema::create('recipes', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->unsignedBigInteger('group_id')->nullable();

When it is not nullable it is not possible to create the related model before using it in saveMany().

A function like make() on Laravel's FactoryBuilder would probably help.

Would this be the right direction or is there another clean way you can think of?

overwriteDefaults only apply last one

So If I have:

UserFactory::new()
->active()
->admin()
->create();

public function active()
{
    return tap(clone $this)->overwriteDefaults([
        'active' => true,
    ]);
}
public function admin()
{
    return tap(clone $this)->overwriteDefaults([
        'admin' => true,
    ]);
}

It will only apply the last method that has overwriteDefaults which is ->admin() and ignore ->active() overwriteDefaults.

Automated code review for Laravel Factories Reloaded

Hi!
I am a member of the team developing monocodus — a service that performs automatic code review of GitHub pull requests to help organizations ensure a high quality of code.
We’ve developed some useful features to the moment, and now we’re looking for early users and feedback to find out what we should improve and which features the community needs the most.

We ran monocodus on a pre-created fork of your repo on GitHub https://github.com/monocodus-demonstrations/laravel-factories-reloaded/pulls, and it found some formatting issues (according to PHP-CS-FIXER). I hope that this information will be useful to you and would be happy to receive any feedback here or on my email [email protected].

If you want to try our service, feel free to follow the link: https://www.monocodus.com
The service is entirely free of charge for open source projects. Hope you’ ll like it :)

Better default belongTo relation syntax

Based on the issue discussed here, this is a quick workaround and separation, sorry for not having the time to open a concrete PR and write tests for the new functionality.

class ChapterFactory extends BaseFactory
{
    public function getData(Generator $faker): array
    {
        $number = 0;
        return [
            'title' => $faker->title,
            'number' => ++$number,
            'story_id' => factory(Story::class), // Laravel default style
            'story_id' => StoryFactory::new(), // Factory reloaded style
        ];
    }
}

To make this happen I modified the BaseFactory@build method to

    protected function build(array $extra = [], string $creationType = 'create')
    {
        $data = collect($this->getData($this->faker))->map(function ($item) {
            return self::isFactory($item) ? $item->create()->id : $item;
        })->toArray();

        $model = $this->modelClass::$creationType(array_merge($data, $extra));

        if ($this->relatedModel) {
            $model->{$this->relatedModelRelationshipName}()
                ->saveMany($this->relatedModel);
        }

        return $model;
    }

    public static function isFactory($item): bool
    {
        return $item instanceof FactoryBuilder || $item instanceof BaseFactory;
    }

Then, to create chapter with default story, the usge will be as simple as:

ChapterFactory::new()->create();

UPDATE:

While I'm refactoring my tests, I need to use this:

ChapterFactory::new()->with(Page::class,'pages',3)->create();

But my test fails because of page factory new syntax, so I make extra modification to the BaseFactory@with method:

    public function with(string $relatedModelClass, string $relationshipName, int $times = 1)
    {
        $clone = clone $this;

        $forignKey = $this->getForignKeyFromClassName($clone->modelClass);
        $clone->relatedModel = $this->getFactoryFromClassName($relatedModelClass)
            ->times($times)
            ->make([
                $forignKey => null
            ]);
        $clone->relatedModelRelationshipName = $relationshipName;

        return $clone;
    }
    
    private function getForignKeyFromClassName(string $modelClass)
    {
        $cleanSingularName = strtolower(Str::singular(last(explode("\\", $modelClass))));

        return "{$cleanSingularName}_id";
    }

Also, I modify the CollectionFactory@build

    private function build(array $extra = [], string $creationType = 'create'): Collection
    {
        return collect()
            ->times($this->times)
            ->transform(fn($value, $key) => $this->modelClass::$creationType($this->getData(array_merge($this->modelsDefaultData[$key], $extra))));
    }

    public function getData($data)
    {
        return collect($data)->map(function ($item) {
            return self::isFactory($item) ? $item->create()->id : $item;
        })->toArray();
    }

And everything goes fine after that. Absolutely with modification is not the best approach and we need to use some Laravel helper to know the exact foreign key because the developer may not use the default naming convention.

massive assignment on withFactory() method

Solve it!
Because chain method return $this, so, as long as I assign the same variable to itself, like this:

//This is a successful try
$zoo = ZooFactory::new()->assignZoo('The New Zoo');
$animals=['zebra', 'elephant', 'hippo', 'chimpanzee'];

foreach($animals as $animal){
    $zoo=$zoo->withFactory(AnimalFactory::new()->assignAnimal($animal), 'animals');
}

$zoo->create();

That's say if I want to add a zoo, and many animals in the zoo:

ZooFactory::new()
->assignZoo('The New Zoo')
->withFactory(AnimalFactory::new()->assignAnimal('zebra'), 'animals')
->withFactory(AnimalFactory::new()->assignAnimal('elephant'), 'animals')
->withFactory(AnimalFactory::new()->assignAnimal('hippo'), 'animals')
->withFactory(AnimalFactory::new()->assignAnimal('chimpanzee'), 'animals')
->create();

Then in the zoo database table will be:

id zoo
1 The New Zoo

the the animals table there will be:

id zoo_id animal
1 1 zebra
2 1 elephant
3 1 hippo
4 1 chimpanzee

I wonder if there is a clean way to refactor this code? ex:only need to give an array of animals ['zebra', 'elephant', 'hippo', 'chimpanzee'], and call the method ->withFactory() just for once.

I've tried foreach but it doesn't work. It only created a zoo, but didn't create animals in the database.

//This is a failure try
$zoo = ZooFactory::new()->assignZoo('The New Zoo');
$animals=['zebra', 'elephant', 'hippo', 'chimpanzee'];

foreach($animals as $animal){
    $zoo->withFactory(AnimalFactory::new()->assignAnimal($animal), 'animals');
}

$zoo->create();

Any idea?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.