Framework: Polymorphic relations with custom names do not load correctly

Created on 9 Jul 2018  路  8Comments  路  Source: laravel/framework

  • Laravel Version: 5.6.26
  • PHP Version: 7.2
  • Database Driver & Version: mysql MariaDB (10.2.9-MariaDB-10.2.9+maria~jessie - mariadb.org binary distribution)

Description:

I have the following tables:

Table: users

| id | email | password | active | user_profile_type | user_profile_id | remember_token | created_at | updated_at | deleted_at |
|----|----------------------------|----------|--------|------------------------|-----------------|----------------|---------------------|---------------------|------------|
| 1 | macey.[email protected] | xxx | 1 | standard_user_profiles | 1 | 2BNbGXSnE7 | 2018-07-08 15:20:00 | 2018-07-08 15:20:00 | NULL |
| 2 | [email protected] | xxx | 0 | premium_user_profiles | 1 | GY3EHWm0Js | 2018-07-08 15:20:01 | 2018-07-08 15:20:01 | NULL |
| 3 | quinten.[email protected] | xxx | 0 | standard_user_profiles | 2 | Lw46mB2Wek | 2018-07-08 15:20:01 | 2018-07-08 15:20:01 | NULL |

Table: standard_user_profiles

| id | language_id | academic_prefix | first_name | last_name | academic_suffix | sex | created_at | updated_at | deleted_at |
|----|-------------|-----------------|------------|-------------|-----------------|--------|---------------------|---------------------|------------|
| 1 | 139 | NULL | Albin | Stoltenberg | NULL | male | 2018-07-08 15:20:00 | 2018-07-08 15:20:00 | NULL |
| 2 | 63 | NULL | Jermey | Konopelski | NULL | male | 2018-07-08 15:20:00 | 2018-07-08 15:20:00 | NULL |
| 3 | 156 | NULL | Demario | Ruecker | NULL | female | 2018-07-08 15:20:00 | 2018-07-08 15:20:00 | NULL |

I have to following models:

Model: User

class User extends Authenticatable
{
    use Notifiable, SoftDeletes;

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'email',
        'password',
        'active',
        'user_profile_id'.
        'user_profile_type',
    ];

    /**
     * The attributes that should be hidden for arrays.
     *
     * @var array
     */
    protected $hidden = [
        'password',
        'remember_token',
        'user_profile_type',
        'user_profile_id',
    ];

    /**
     * The attributes that should be mutated to dates.
     *
     * @var array
     */
    protected $dates = [
        'created_at',
        'updated_at',
        'deleted_at',
    ];

    /**
     * The attributes that should be cast to native types.
     *
     * @var array
     */
    protected $casts = [
        'active' => 'boolean',
    ];

    /**
     * @return \Illuminate\Database\Eloquent\Relations\MorphTo
     */
    public function profile()
    {
        return $this->morphTo('user_profile');
    }
}

Model: StandardUserProfile

class StandardUserProfile extends AbstractUserProfile
{
    use SoftDeletes;

    protected $fillable = [
        'language_id',
        'first_name',
        'last_name',
        'academic_prefix',
        'academic_suffix',
        'sex',
    ];

    protected $dates = [
        'created_at',
        'updated_at',
        'deleted_at',
    ];

    /**
     * @return \Illuminate\Database\Eloquent\Relations\MorphOne
     */
    public function user()
    {
        return $this->morphOne(User::class, 'user_profile', 'user_profile_type', 'user_profile_id');
    }
}

morphMap in ServiceProvider

Relation::morphMap(
  [
    PremiumUserProfile::class,
    StandardUserProfile::class,
  ]
);

Steps To Reproduce:

This is the output from: User::find(1)->profile

Relationship is accessable correctly here.

>>> User::find(1)->profile
=> App\Models\UserProfiles\StandardUserProfile {#3170
     id: 1,
     language_id: 139,
     academic_prefix: null,
     first_name: "Albin",
     last_name: "Stoltenberg",
     academic_suffix: null,
     sex: "male",
     created_at: "2018-07-08 15:20:00",
     updated_at: "2018-07-08 15:20:00",
     deleted_at: null,
   }

This is the output from: User::with('profile')->find(1)

This kind of loading is not possible, because related model is loaded in the wrong property user_profile, property profile returns null

>>> User::with('profile')->find(1)
=> App\Models\User {#3154
     id: 1,
     email: "[email protected]",
     active: 1,
     created_at: "2018-07-08 15:20:00",
     updated_at: "2018-07-08 15:20:00",
     deleted_at: null,
     profile: null,
     user_profile: App\Models\UserProfiles\StandardUserProfile {#3181
       id: 1,
       language_id: 139,
       academic_prefix: null,
       first_name: "Albin",
       last_name: "Stoltenberg",
       academic_suffix: null,
       sex: "male",
       created_at: "2018-07-08 15:20:00",
       updated_at: "2018-07-08 15:20:00",
       deleted_at: null,
     },
   }

It does work, if I change columns and everything else to profile_type and profile_id.
However, I didn't dive deeper into framework code, but this looks like a bug by loading polymorphic relations.

Most helpful comment

Your relationship works if you specify both column names and don't specify a $name:

public function profile() {
    return $this->morphTo(null, 'user_profile_type', 'user_profile_id');
}

I would have expected $this->morphTo('user_profile') to produce the same result. But apparently, that's a misconception.

Nevertheless, when using a custom relationship name, the "profile" => null entry shouldn't be there.

All 8 comments

@themsaid Can you take a look at this?

You explicitly test the relation name in DatabaseEloquentModelTest::testMorphToCreatesProperRelation() (morphToStubWithName and morphToStubWithNameAndKeys). Aren't these tests incorrect?

@staudenmeir I think, my specific case is missing in these tests.

Testing my case could look like the following snippet.
Because the relationship is called via name in morphTo() and have nothing to do with the keys some_name in the tables.

// additional test scenario in testMorphToCreatesProperRelation()

// $this->morphTo('name');
$relation5 = $model->morphToStubWithOtherName();
$this->assertEquals('some_name_id', $relation5->getForeignKey());
$this->assertEquals('some_name_type', $relation5->getMorphType());
$this->assertEquals('name', $relation5->getRelation());
public function morphToStubWithOtherName()
{
    return $this->morphTo('name');
}

I hope my thesis are correct.

Your relationship works if you specify both column names and don't specify a $name:

public function profile() {
    return $this->morphTo(null, 'user_profile_type', 'user_profile_id');
}

I would have expected $this->morphTo('user_profile') to produce the same result. But apparently, that's a misconception.

Nevertheless, when using a custom relationship name, the "profile" => null entry shouldn't be there.

@staudenmeir wow, that works! 馃憤

I can see the misconception, but this looks like many dependencies in the framework.

Can you confirm your service provider, please? You specify the classes, but don't actually alias them up?

Also, why is your relationship method called profile and the morph is called user_profile?

@deleugpn you mean the morphMap() in the ServiceProvider? Yes, it is included.

The idea was to have another "Profile"-Model relation to other Models except "User"-Model, nothing to do with the current structure. So maybe in future, we have "Customer"-Models, they may can also have "Profile"-Models, but not the same as "User"-Models. So then, I would call the relation "profile" again and get a kind of "BusinessCustomerProfile"-Model. In case of that, I can diverse the morph-fields in the database better, when naming "user_profile" and "customer_profile".

@deleugpn was referring to the missing keys/aliases in your morphMap().

Is defined now, but also the same logic as @staudenmeir has evaluated.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ghost picture ghost  路  3Comments

JamborJan picture JamborJan  路  3Comments

CupOfTea696 picture CupOfTea696  路  3Comments

iivanov2 picture iivanov2  路  3Comments

kerbylav picture kerbylav  路  3Comments