Search code examples
phplaraveleloquentrelationshiplaravel-5.3

Force create an eloquent model through a relationship


In a small project with Laravel 5.3 and Stripe, I am trying to force create a Subscription on a User through a hasOne relationship:

// User.php
public function subscription() {
    return $this->hasOne('App\Subscription');
}

public function subscribe($data) {
    return $this->subscription()->forceCreate(
        // $data contains some guarded fields;
        // create() will simply ignore them...
    );
}

However, I get :

Call to undefined method Illuminate\Database\Query\Builder::forceCreate()

Even though forceCreate() is a valid Eloquent method.

Any ideas how I can simulate this behavior? Or should I just save a Subscription manually assigning each field? The complication is that certain fields should be kept guarded, e.g. stripe_id.

EDIT

My quick' n' dirty solution:

// User.php @ subscribe($data)
return (new Subscription())
        ->forceFill([
            'user_id' => $this->id,
            // $data with sensitive guarded data
        ])
        ->save();

I'm sure there is a better way though!


Solution

  • The call to $this->subscriptions() returns an instance of HasMany class, not a Model.

    Contrary to create and createMany, there's no forceCreate() implemented in HasOneOrMany class. And thus, there's no first-hand API to use in such situations.

    You see? when you call $this->subscriptions()->create(), you're calling the create method on a HasMany instance, not a Model instance. The fact that Model class has a forceCreate method, has nothing to do with this.

    You could use the getRelated() method to fetch the related model and then call forceCreate() or unguarded() on that:

    public function subscribe($data) {
        return $this->subscription()
                    ->getRelated()
                    ->forceCreate($data);
    }
    

    But there's a serious downside to this approach; it does not set the relation as we're fetching the model out of the relation. To work around it you might say:

    public function subscribe($data)
    {
        // $data['user_id'] = $this->id;
        // Or more generally:  
        $data[$this->courses()->getPlainForeignKey()] = $this->courses()->getParentKey();
    
        return $this->courses()
                    ->getRelated()
                    ->forceCreate($data);
    }
    

    Ehh, seems too hacky, not my approach. I prefer unguarding the underlying model, calling create and then reguarding it. Something along the lines of:

    public function subscribe($data)
    {
        $this->courses()->getRelated()->unguard();
    
        $created = $this->courses()->create($data);
    
        $this->courses()->getRelated()->reguard();
    
        return $created;
    }
    

    This way, you don't have to deal with setting the foreign key by hand.

    laravel/internals related discussion:
    [PROPOSAL] Force create model through a relationship