1

I used the one-to-many polymorphic relationship like described in the laravel documentation, to be able to relate different parent models to one child model. i have assumed, that i would be able to assign different parent models to the same child model. but this does not work. as soon as i create a new relation with another parent model to the same child, the old relation is replaced.

Example:

A, B and C are parent models, each with one data-record (id=1).

X is the child model with one data-record (id=1)

I can't do something like that with the common methods:

A(id=1) <-> X(id=1)

B(id=1) <-> X(id=1)

C(id=1) <-> X(id=1)

Since the last creation of a relation always overwrites the previous one. In this example one relation would remain (C(id=1) <-> X(id=1))

I am able to do that with a many-to-many polymorphic implementation - but this is actually not what i want, since i do not want the parent models to be able to have more than one relation to the child model. (altough i could rule that out by creating a composite key within the *able table on the corresponding fields)

This is the actual code, that should assign one image to multiple parent models (but only the last save inside the loop remains - if i add a break at the end of the loop, the first one is saved):

public function store(StoreImageRequest $request)
{
    $validated = $request->validated();

    $image = $validated['image'];
    $name = isset($validated['clientName']) ? $image->getClientOriginalName() : $validated['name'];
    $fileFormat = FileFormat::where('mimetype','=',$image->getClientMimeType())->first();

    $path = $image->store('images');

    $imageModel = Image::make(['name' => $name, 'path' => $path])->fileFormat()->associate($fileFormat);
    $imageModel->save();

    $relatedModels = Image::getRelatedModels();

    foreach($relatedModels as $fqcn => $cn) {
        if(isset($validated['model'.$cn])) {
            $id = $validated['model'.$cn];
            $models[$fqcn] = call_user_func([$fqcn, 'find'], [$id])->first();
            $models[$fqcn]->images()->save($imageModel);
        }
    }
}
lsblsb
  • 1,292
  • 12
  • 19
  • maybe it makes sense to also add the implementation of getRelatedModels in order to actually fully understand your code. On a first glance I'd say the `$models[$fqcn]->images()->save($imageModel);` line is the problem. I would guess that the imageModel is updated after the save and calling it multiple times will update the references as well. Maybe add your relation ship definitions as well to the code. You could quickly try `$models[$fqcn]->images()->sync([$imageModel->id]);` – Frnak Jul 02 '21 at 13:45
  • @Frnak i don't think that this should be a problem - since i do and want to save different references. Therefore the imageModel should handle that correctly. But I'll give it a try. Are you sure about the intended behaviour of that relation type - should it be able to relate a specific child to multiple parents as i asume? – lsblsb Jul 06 '21 at 08:12

1 Answers1

1

Well, it's a bit more clear what you're trying to do now. The thing is, even though you want to enforce that a maximum of one of each parent is attached to the child, you're still actually trying to create a ManyToMany PolyMorphic relationship. Parents can have mutiple images and images can have multiple parents (one of each).

Without knowing the data structure, it could be viable, if the parents all have similar structures to consolidate them into one table, and the look into the relation HasOne ofMany to enforce that an image only have one of each parent.

If you insist on a Polymorphic relationship, I would do the following

    // child
    Schema::create('images', function (Blueprint $table) {
        $table->id();
        $table->timestamps();
    });

    // parents
    Schema::create('parent_ones', function (Blueprint $table) {
        $table->id();
        $table->timestamps();
    });


    Schema::create('parent_twos', function (Blueprint $table) {
        $table->id();
        $table->timestamps();
    });

    Schema::create('parent_trees', function (Blueprint $table) {
        $table->id();
        $table->timestamps();
    });

    // morph table
    Schema::create('parentables', function (Blueprint $table) {
        $table->id();
        $table->unsignedBigInteger('image_id');
        $table->foreign('image_id')->references('id')->on('images');
        $table->string('parentable_type');
        $table->unsignedBigInteger('parentable_id');
        $table->timestamps();
        $table->unique(['image_id', 'parentable_type', 'parentable_id'], 'uniq_parents');
    });

The unique constraint on the table enforces that an image can only have one of each parent type.

Model relations

   // Child

   public function parent_one(): MorphToMany
    {
        return $this->morphedByMany(ParentOne::class, 'parentable');
    }

    public function parent_two(): MorphToMany
    {
        return $this->morphedByMany(ParentTwo::class, 'parentable');
    }

    public function parent_tree(): MorphToMany
    {
        return $this->morphedByMany(ParentTree::class, 'parentable');
    }


    // parents
    public function images(): MorphToMany
    {
        return $this->morphToMany(Image::class, 'parentable')->withTimestamps();
    }

Then it's up to you code to handle that it does not try to attach a parent to an image if it already has a parent of that type.

This could either be done in your controller or if the parent isn't replaceable, you could add it as validation in you Request.

A solution could be installing this package (untested by me). MorphToOne


Old answer

It sounds to me that you have flipped the One To Many relation in the wrong direction.

Your child model should implement morphTo and your parent models should implement morphMany

a 
    id - integer
    title - string
    body - text

b
    id - integer
    title - string
    url - string

c
    id - integer
    title - string
    url - string

x
    id - integer
    body - text
    commentable_id - integer
    commentable_type - string

Child Model

public class X extends Model
{
    public function commentable(): MorphTo
    {
        return $this->morphTo();
    }
}

Parent Models

public class A extends Model
{
    public function comments(): MorphMany
    {
        return $this->morphMany(X::class, 'commentable');
    }
}

Adding to the relation:

$a = A::firstOrFail();
$x = X::firstOrFail();

// Attaches
$a->comments()->attach($x);

// Sync, removes the existing attached comments
$a->comments()->sync([$x]);

// Sync, but do not remove  existing
$a->comments()->sync([$x], false);
$a->comments()->syncWithoutDetaching([$x]);

  • Hi Steffen, no - this is not the case. I build it like you did. But i add the relation not by using attach() or sync() (see my updated question with the actual code). Maybe it behaves like a sync with removal - i will try that. – lsblsb Jun 30 '21 at 13:26
  • Hi Steffen, thanks. But my problem is the other way around. I want to assign different parent IDs to one specific child ID - and exactly this does not work in my case with the one-to-many polymorphic relationship. – lsblsb Jul 17 '21 at 07:58
  • Based on the information you've given i would say my answer is the right one. A parent can have multiple children and a child can have only one of each parent. That's a many to many relation constrained to one going from the child to each parent. If that's not the case, you need to expand on the initial question with your model structure. – Steffen Thomsen Jul 17 '21 at 11:26