Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

jrdavidson's avatar

Attaching Polymorphic relationships with a Factory

I'm currently trying to figure out how I can attach a new champion to a title. I have different types of models that can be a champion of a title which is why there is a Polymorphic relationship. Right now I'm struggling to figure out to correctly add this new champion.

What I'm trying to do is create a title with a champion attached to it. There is a MorphPivot model that will hold the title_id and the championable_id and championable_type. When a champion is created it attaches the title and the champion and sets the wont_at field and the lost_at fields.

Any suggestions on what I'm doing wrong?

$title = Title::factory()
    ->hasAttached(
        TitleChampion::factory(),
        [
            'championable_id' => $playerA->id,
            'championable_type' => get_class($playerA),
            'won_at' => '2021-01-01',
            'lost_at' => '2021-03-01',
        ],
    )
    ->create();
dd($title->champions);
public function champions()
{
    return $this->morphedByMany(TitleChampion::class, 'championable');
}

public function champion()
{
    return $this->morphOne(TitleChampion::class, 'championable')->latestOfMany('won_at');
}
<?php

namespace App\Models;

use App\Models\Concerns\Unguarded;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\MorphPivot;

class TitleChampion extends MorphPivot
{
    use HasFactory,
        Unguarded;

    /**
     * The table associated with the model.
     *
     * @var string
     */
    protected $table = 'title_champions';
}
0 likes
28 replies
webrobert's avatar

@jrdavidson,

Here is the syntax for how I'm doing it right now for a project...

        // create discussions explicitly for me
        $user_count = User::count();
        foreach (range(1, 10) as $i) {
            Discussion::factory()
                ->has(
                    Reply::factory()
                        ->count(rand(1, 12))
                        ->state(new Sequence(
                            fn($sequence) => [
                                'user_id' => rand(1, $user_count),
                                'replyable_id'   => $i,
                                'replyable_type' => Discussion::class,
                            ])
                        )
                )
                ->hasVotes(rand(1, 22))
                ->create([ 'user_id' => 1 ]);
        }

then one more loop for general not specific to my user.

1 like
jrdavidson's avatar

@webrobert Problem is for my type its the type of the id you have it as something else. Your situation works for what your application needs. It doesn't have the same effect for mine.

webrobert's avatar
$title = Title::factory()
    ->hasAttached(
        TitleChampion::factory(),
        [
            'championable_id' => $playerA->id,
            'championable_type' => get_class($playerA),
            'won_at' => '2021-01-01',
            'lost_at' => '2021-03-01',
        ]  //,  removed comma
    )
    ->create();
dd($title->champions);
jrdavidson's avatar

@webrobert

I've tried that before and get this. error which doesn't even apply the pivot data.

SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: title_champions.title_id (SQL: insert into "title_champions" ("updated_at", "created_at") values (2021-09-09 15:39:21, 2021-09-09 15:39:21))

webrobert's avatar

because title_champions.title_id is null 🤷🏼‍♂️ Where are you setting that?

jrdavidson's avatar

I understand the error but don't understand why its not being set because it should be set from when the Title is created.

webrobert's avatar

what's happening in TitleChampion::factory() ?

jrdavidson's avatar

@webrobert I dont' have anything set up. I'm wondering if I still have it setup wrong because TitleChampion isn't a specific model. It can be one of 3 different types of models.

jrdavidson's avatar

@webrobert So with the hasAttached there's also an optional 3rd param for setting the relationship used and even adding that in its still not setting the values correctly.

jrdavidson's avatar

@webrobert

This works however problem with this that there could be many other types of champions. This is why its polymorphic.

Championable Types:
OneModel
AnotherModel
ThirdModel

Title

public function champions()
{
    return $this->morphedByMany(OneModel::class, 'championable', 'title_champions');
}
webrobert's avatar

well, it seems we're back on the other topic. My sense then and now was you haven't clearly defined your relationships. On your other thread, I was moving you to find that clarity. It seems a bit like you are just looking for code to paste in and go with it?

In my mind, this thread has been answered. You were referencing the wrong factory and had a rouge comma.

Before you chose Many To Many (Polymorphic) From the docs....


posts
    id - integer
    name - string

videos
    id - integer
    name - string

tags
    id - integer
    name - string

taggables
    tag_id - integer
    taggable_id - integer
    taggable_type - string

Notice there isn't a title_champions (pivot table?) there?

Many To Many (Polymorphic) should look something like this...


OneModel
    id - integer
    name - string

AnotherModel
    id - integer
    name - string

ThirdModel
    id - integer
    name - string

Champion
    id - integer
    name - string

Championable
    tag_id - integer
    Championable_id - integer
    Championable_type - string

am i missing something??

Or maybe the naming is all wrong...


OneModel
    id - integer
    name - string

AnotherModel
    id - integer
    name - string

ThirdModel
    id - integer
    name - string

Title
    id - integer
    name - string /// CHAMPION

Titlable
    tag_id - integer
    Titlable_id - integer
    Titlable_type - string

jrdavidson's avatar

@webrobert


OneModel
    id - integer
    name - string

AnotherModel
    id - integer
    name - string

ThirdModel
    id - integer
    name - string

Title
    id - integer
    name - string

title_champions
    title_id - integer
    championable_id - integer
    championable_type - string (OneModel::class, AnotherModel::class, ThirdModel::class)

jrdavidson's avatar

@webrobert That's my current database structure for the related tables. According to the docs that would make this a polymorphic many-to-many but with the logic doesn't match somehow.

What I'm wanting to do is for a given title retrieve all of the champions that have held the title. So it can be variable model class.

webrobert's avatar

your factory doesn't work because it's Either not defined AND/OR you haven't added all the relationships to the correct models...

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Title extends Model
{
    /**
     * Get all of the "ones" that are assigned this title.
     */
    public function ones()
    {
        return $this->morphedByMany(OneModel::class, 'title_champions', 'title_id');
    }

    /**
     * Get all of the "anothers" that are assigned this title.
     */
    public function anothers()
    {
        return $this->morphedByMany(AnotherModel::class, 'title_champions', 'title_id');
    }
}

then you have to define it in each of the other THREE models


<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class One extends Model
{
    /**
     * Get all of the tags for the post.
     */
    public function titles()
    {
        return $this->morphToMany(Title::class, 'title_champions', 'title_id');
    }
}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Another extends Model
{
    /**
     * Get all of the tags for the post.
     */
    public function titles()
    {
        return $this->morphToMany(Title::class, 'title_champions', 'title_id');
    }
}

title_champions and title_id i might have missed placed. SO double-check those are in the correct placements.

webrobert's avatar
Level 51

@jrdavidson

morphToMany($related, $name, $table = null, $foreignPivotKey = null,)

so it needs to be this way..

   return $this->morphToMany(AnotherModel::class, 'championable', 'title_champions'  );

and probably the morphedByMany one is that way too...

jrdavidson's avatar

@webrobert I appreciate your responses here. I think I need to rethink the database structure because I don't have needed logic to have methods on the title for the different models. The methods for getting the titles for each of the different models is correct so that part is still correct. I need to make sure that it when I call $title->champions it will have a collection of different models of whatever it retrieved from the type.

jrdavidson's avatar

@webrobert This should be what my final result should look like.

^ array:3 [
  0 => array:12 [
    "id" => 1
    ...
    "created_at" => "2021-09-09T20:43:33.000000Z"
    "updated_at" => "2021-09-09T20:43:33.000000Z"
    "deleted_at" => null
    "pivot" => array:3 [
      "title_id" => "1"
      "championable_id" => "1"
      "championable_type" => "App\Models\ThirdModel"
    ]
  ]
  1 => array:12 [
    "id" => 1
    ...
    "created_at" => "2021-09-09T20:43:33.000000Z"
    "updated_at" => "2021-09-09T20:43:33.000000Z"
    "deleted_at" => null
    "pivot" => array:3 [
      "title_id" => "1"
      "championable_id" => "1"
      "championable_type" => "App\Models\OneModel"
    ]
  ]
  2 => array:12 [
    "id" => 2
    ...
    "created_at" => "2021-09-09T20:43:33.000000Z"
    "updated_at" => "2021-09-09T20:43:33.000000Z"
    "deleted_at" => null
    "pivot" => array:3 [
      "title_id" => "1"
      "championable_id" => "2"
      "championable_type" => "App\Models\AnotherModel"
    ]
  ]
]
jrdavidson's avatar

@webrobert I'm having a problem understanding how I would use this. Let me give you the real data models of what I'm working with. This way we can understand each other better. What I'm wanting to do is grab all championable models that have won a specific model regardless of their type.

Wrestlers
 - id
- name

Tag Teams

 - id
 - name

Titles

 - id
 - name

Title_Champions

 - title_id
-  championable_id
-  championable_type
- won_at
 - lost_at

I'm wanting the data to be somewhat like this unless there is a better alternative with your suggestion.

Title_Champions

| title_id | championable_id | championable_type    | won_at     | lost_at    |
|----------|-----------------|----------------------|------------|------------|
| 1        | 1               | \App\Models\Wrestler | 2021-01-01 | null       |
| 1        | 2               | \App\Models\Wrestler | 2020-01-01 | 2021-01-01 |
| 2        | 1               | \App\Models\TagTeam  | 2021-01-01 | null       |
jrdavidson's avatar

@webrobert I marked one of your responses as being the solution even though I don't have a solution that fixes the problem.

webrobert's avatar

@jrdavidson I understand it doesn’t solve the actual problem but the solution for the question

Attaching Polymorphic relationships with a Factory

Was answered above.

We can address the actual problem in the new thread 👍

Please or to participate in this conversation.