Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relationship attributes not being added to model instance before saving? #95

Open
borfast opened this issue Apr 2, 2015 · 5 comments
Open

Comments

@borfast
Copy link

borfast commented Apr 2, 2015

My factory.php has this:

$factory('User', [
    'email' => $faker->email,
]);

$factory('Identity', [
    'user_id' => 'factory:User',
    'name' => $faker->name,
]);

My Identity model has this:

public static function boot()
{
    parent::boot();
    static::creating(function ($identity) {
        $email = $identity->user->email;
        //....
    }
}

Finally, in one of my tests there's this:

$identity = Factory::create('Identity');

Executing that test results in an exception in the Identity code, specifically in the $email = $identity->user->email line:

ErrorException: Trying to get property of non-object

Turns out $identity->user is null, even though it is specified in the factory definition.
If I comment out the boot() method in my Identity model, I get an SQL integrity constraint violation:

Integrity constraint violation: 1048 Column 'user_id' cannot be null

In the end there is a User record in my database but no Identity. My first thought was that relationship attributes are not being added to the Identity model instance before trying to save it, which could cause both problems, but if that's the case, how would the library be able to automatically and recursively create the related objects?

Am I missing something?

@nathan-isaac
Copy link

I am getting a similar error.

$factory('User', [
    'email' => $faker->email,
]);

$factory('Profile', [
    'user_id' => 'factory:User',
    'name' => $faker->name,
]);
$profile = TestDummy::attributesFor('Profile', $overrides);

This will return something like this

[
  'user_id' => "factory:User",
  'name' => "Foo"
]

Is there a reason why the user_id is returning factory:User instead of the actual user id?

Thanks in advance.

@borfast
Copy link
Author

borfast commented May 11, 2015

@JeffreyWay, could you chime in here, please?

@nathan-isaac
Copy link

For now a solution for the attributesFor method is to just do something like this.

$user = TestDummy::create('User');
$profile = TestDummy::attributesFor('Profile', ['user_id' => $user->id]);

It would be cool to have this done automatically but this works for now.

@phroggyy
Copy link

@borfast @nisaac2fly: Having just submitted a PR for TD, I spent quite some time with the source code last night, so I'll try to go through how TD works behind the scenes when different commands are called.

Let's suppose we have the following setup:

$factory('User', [
    'email' => $faker->email,
]);

$factory('Identity', [
    'user_id' => 'factory:User',
    'name' => $faker->name,
]);

We then run TestDummy::create('Identity')... What happens next is the following

TL;DR

  1. Model is created
  2. Related models are created, saved, and assigned to the model
  3. The model is saved

Long version

  1. The create('Identity') method calls persist('Identity')
  2. persist('Identity') will first call build('Identity')
    1. build will first get the attributes of our factory, using $attributes = getAttributes('identity', []) (any overrides are passed in as the second parameter). This is fetched straight from your factory definition, so user_id = factory:User right now...
    2. build gets the class name of our model (since named factories is a possibility), $class = $this->getFixture($name)->name;
    3. build calls on EloquentModel::build($class, $attributes) (unless you use a custom implementation of the IsPersistible interface)
      1. build in our EloquentModel will return the model with the attributes filled (by calling on EloquentModel::fill)
    4. We now have an instance of our model, with user_id = factory:User
  3. persist('Identity') will now call the assignRelationships method to actually set our relationship. This is the step in which we actually get the user created (by going through persist on our related model) and retrieve its id.
  4. Save is run on our model instance.

Hopefully, this gave some insight. Now, to the problem(s) mentioned:

The boot method is run in the __construct() of our Identity. This means that, since we actually instantiate the model inside of EloquentModel when creating the entry, we will run the boot() method before we parse any relationships. Hence, user_id is still factory:User at this point, and you henceforth run into mentioned issue.

I might try to work on a way to create related models first, but it is really quite difficult since that means changing the order of operations on TestDummy and not creating the instance before assigning relationships.

@nathan-isaac
Copy link

@phroggyy Thank you for the informative explanation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants