depalmo's avatar

Relationship many-to-many on the same model

I have a bit complicated task to code and would appreciate any help. The problem is that I have one model, let's call it Report and I need for it to relate back to itself. Bottom line is that I need to link various reports between each other and they should be available to be listed when needed on both ends. I did make a belongsToMany relationship and it's loading well one way but not the other. I also can't save it because it tries to validate data in wrong table and not in the pivot table. When I say that it's loading one way I mean that if Report A is linked to Report B, Report A will show the relation, but Report B won't.

Migration:

Schema::create('reports_related', function (Blueprint $table) {
    $table->foreignId('report_id')->constrained()->cascadeOnUpdate()->cascadeOnDelete();
    $table->foreignId('related_report_id')->constrained('reports')->cascadeOnUpdate()->cascadeOnDelete();
});

Model:

public function related(): BelongsToMany|Collection|Report
{
    return $this->belongsToMany(
        Report::class,
        'reports_related',
        'report_id',
        'related_report_id'
    );
}

UpdateController.php:

$report->related()->delete(); // clear old relations, since we do not have sync() available
foreach ($request->related_ids as $id) {
    $report->related()->updateOrCreate([
        'report_id' => $report->id,
        'related_report_id' => $id,
    ], [
        'report_id' => $report->id,
        'related_report_id' => $id,
    ]);
}

But I am thinking that this may not be the correct way of doing it and would appreciate any help I can get.

0 likes
3 replies
LaryAI's avatar
Level 58

The issue you're facing is that the relationship is only loading in one direction. To make it work both ways, you need to define the relationship on both ends of the model.

Here's how you can modify your Report model to establish a many-to-many relationship with itself:

use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Report extends Model
{
    public function related(): BelongsToMany
    {
        return $this->belongsToMany(
            Report::class,
            'reports_related',
            'report_id',
            'related_report_id'
        )->withTimestamps();
    }

    public function relatedTo(): BelongsToMany
    {
        return $this->belongsToMany(
            Report::class,
            'reports_related',
            'related_report_id',
            'report_id'
        )->withTimestamps();
    }
}

By adding the relatedTo method, you can now access the related reports from the other end of the relationship.

To save the relationships, you can simplify your UpdateController.php code as follows:

$report->related()->sync($request->related_ids);

This will automatically handle deleting old relations and creating new ones based on the provided related_ids array.

Make sure to update your migration file to include the timestamps() method:

Schema::create('reports_related', function (Blueprint $table) {
    $table->foreignId('report_id')->constrained()->cascadeOnUpdate()->cascadeOnDelete();
    $table->foreignId('related_report_id')->constrained('reports')->cascadeOnUpdate()->cascadeOnDelete();
    $table->timestamps();
});

With these changes, you should be able to establish a many-to-many relationship between reports in both directions.

tisuchi's avatar
tisuchi
Best Answer
Level 70

@depalmo This is my suggestion.

Update your model to include two relationships: one for related and another for inverseRelated.

public function related(): BelongsToMany
{
    return $this->belongsToMany(
        Report::class,
        'reports_related',
        'report_id',
        'related_report_id'
    );
}

public function inverseRelated(): BelongsToMany
{
    return $this->belongsToMany(
        Report::class,
        'reports_related',
        'related_report_id',
        'report_id'
    );
}

In your UpdateController

// Clear old relations
$report->related()->detach();

// Attach new relations
foreach ($request->related_ids as $id) {
    $report->related()->attach($id);
    // The inverse relation ensures symmetry in the relationship
    Report::find($id)->related()->attach($report->id);
}
1 like
depalmo's avatar

@tisuchi Thank you! I never actually realized I could relate all models between each other and then simply load the relationship.

2 likes

Please or to participate in this conversation.