Search code examples
phplaravel-5dependency-injection

Dependency injection inside model factory


This is my first question, so I would also appreciate hints on how to ask properly.

So, In my Laravel app, I have a database table with users. For start, I wanted to have a model factory for it. So I took a standard code from laravel doc page:

$factory->define(App\User::class, function (Faker\Generator $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->email,
        'password' => bcrypt(str_random(10)),
        'remember_token' => str_random(10),
    ];
});

And I changed it to:

$factory->define(App\User::class,
                    function(Faker\Generator $faker) {

    return [
        'name' => $faker->name(),
        'email' => $faker->safeEmail(),
        'password' => bcrypt(str_random(10)),
        'phone_number' => $faker->phoneNumber(),
        'remember_token' => str_random(10),
        'account_type' => 0,
    ];

});

So far, everything works. But I wanted it to be more sophisticated, and I decided to use more specific kind of Faker class, to generate Italian data. I changed it to:

$factory->define(App\User::class,
                    function(Faker\Generator $faker,
                             Faker\Provider\it_IT\PhoneNumber $fakerITPN,
                             Faker\Provider\it_IT\Person $fakerITPER,
                             Faker\Provider\it_IT\Internet $fakerITInt) {

    return [
        'name' => $fakerITPER->name(),
        'email' => $fakerITInt->safeEmail(),
        'password' => bcrypt(str_random(10)),
        'phone_number' => $fakerITPN->phoneNumber(),
        'remember_token' => str_random(10),
        'account_type' => 0,
    ];

});

In seeder class I wrote:

factory(App\User::class)->create();

And then, after I used Artisan, command:

artisan migrate:refresh --seed -vvv

I get following error (just the head, for clearance):

[ErrorException]                                                                                                                             
  Argument 2 passed to Illuminate\Database\Eloquent\Factory::{closure}() must be an instance of Faker\Provider\it_IT\PhoneNumber, array given  

Exception trace:
 () at /home/vagrant/php/housing/database/factories/ModelFactory.php:19
 Illuminate\Foundation\Bootstrap\HandleExceptions->handleError() at /home/vagrant/php/housing/database/factories/ModelFactory.php:19
 Illuminate\Database\Eloquent\Factory::{closure}() at n/a:n/a
 call_user_func() at /home/vagrant/php/housing/vendor/laravel/framework/src/Illuminate/Database/Eloquent/FactoryBuilder.php:130
 Illuminate\Database\Eloquent\FactoryBuilder->Illuminate\Database\Eloquent\{closure}() at /home/vagrant/php/housing/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:2308
 Illuminate\Database\Eloquent\Model::unguarded() at /home/vagrant/php/housing/vendor/laravel/framework/src/Illuminate/Database/Eloquent/FactoryBuilder.php:133
 Illuminate\Database\Eloquent\FactoryBuilder->makeInstance() at /home/vagrant/php/housing/vendor/laravel/framework/src/Illuminate/Database/Eloquent/FactoryBuilder.php:105
 Illuminate\Database\Eloquent\FactoryBuilder->make() at /home/vagrant/php/housing/vendor/laravel/framework/src/Illuminate/Database/Eloquent/FactoryBuilder.php:83
 Illuminate\Database\Eloquent\FactoryBuilder->create() at /home/vagrant/php/housing/database/seeds/UsersTableSeeder.php:24
 UsersTableSeeder->run() at /home/vagrant/php/housing/vendor/laravel/framework/src/Illuminate/Database/Seeder.php:42

Clearly, there is something wrong with dependency injection, but I don't know what. I know, that in this case I could just manually create instances of classes I need, but I want to know, how to do it properly. Can anyone help?


Solution

  • If you take a look at the documention of faker @ https://github.com/fzaninotto/Faker#localization, you'll see that you can simply assign the proper localization as a parameter to create.

    In your case, just use:

    Faker\Factory::create('it_IT');
    

    You don't need to add more parameters in the anonymous function when you define the factory.

    Edit:

    Just to add on the issue on dependency injection. If you trace the source code, it does not do any dependency injection underneath.

    $factory->define(...)
    

    Only sets an array of definitions

    public function define($class, callable $attributes, $name = 'default')
    {
        $this->definitions[$class][$name] = $attributes;
    }
    

    Calling

    Faker\Factory::create();
    

    or

    factory(App\User::class)->create();
    
    $factory->of($class)
    

    calls "of" method that instantiate FactoryBuilder (see lines 169-172 of Illuminate\Database\Eloquent\Factory.php)

    public function of($class, $name = 'default')
    {
        return new FactoryBuilder($class, $name, $this->definitions, $this->faker);
    }
    

    after that, it chains "create" method of FactoryBuilder that calls "make" method which also calls "makeInstance"

    protected function makeInstance(array $attributes = [])
    {
        return Model::unguarded(function () use ($attributes) {
            if (! isset($this->definitions[$this->class][$this->name])) {
                throw new InvalidArgumentException("Unable to locate factory with name [{$this->name}].");
            }
    
            $definition = call_user_func($this->definitions[$this->class][$this->name], $this->faker, $attributes);
    
            return new $this->class(array_merge($definition, $attributes));
        });
    }
    

    Notice "call_user_func" inside "makeInstance", that is the one responsible for calling the anonymous function created as the 2nd argument to define (inside ModelFactory.php). It specifically pass only 2 arguments to the callable function, these are:

    ...$this->faker, $attributes);
    

    Only 1 faker is passed on the first argument and an array of attributes on the 2nd argument (this is the one you saw on your ErrorException earlier)

    That means you can only define your factory in this way:

    $factory->define(App\User::class, 
        function (Faker\Generator $faker, $attributes=array()) {
    
        return [
            'name' => $faker->name,
            'email' => $faker->email,
            'password' => bcrypt(str_random(10)),
            'remember_token' => str_random(10),
        ];
    });
    

    If you really need other classes, you can initialize it outside of "define" and use it in the function like this:

    $sampleInstance = app(App\Sample::class);
    
    $factory->define(App\User::class, 
        function (Faker\Generator $faker, $attributes=array()) use($sampleInstance){
    
        //...do something here 
        //...or process the $attributes received
        //...or call a method like
        $sampleData = $sampleInstance->doSomething();        
    
        return [
            'someField' => $sampleData,
            'name' => $faker->name,
            'email' => $faker->email,
            'password' => bcrypt(str_random(10)),
            'remember_token' => str_random(10),
        ];
    });