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

earmsby's avatar

2-way relationship in Filament?

I have a model called Contract. Contract has a many-to-many relationship with itself called relatedContracts. The pivot table for this has 2 fields: contract_id & related_contract_id.

In my view for Contracts in Filament I have a relation manager called RelatedContractsRelationManager.php. This works fine if I am on the contract with the ID in the contract_id field -> it returns all contracts with the id in the related_contract_id field. But if I am viewing a contract that is in the related_contract_id field, of course it does not show contracts with ids matching the "contract_id" field.

Is there any way to show both sides of the relationship. i.e. if the id is found in related_contract_id, then show all matching "contract_id" contract and if the id is found in the "contract_id" field, show all the contracts with matching "related_contract_id"

This seems like it would be some sort of union query but I'm not sure how to have filament pull this. Thoughts?

0 likes
3 replies
LaryAI's avatar
Level 58

Certainly! You're right: with a self-referential many-to-many relationship, a single "relatedContracts" relation will only return one side of the association based on how you set up the relationship. To show both sides (i.e., treat the relation as bidirectional), you have a couple of options.

Solution Overview:

  1. Define two relationships on your Contract model: one for each direction.
  2. Create a query that unions both, so you can easily fetch all "related" contracts regardless of the direction.
  3. For Filament, you can use a custom query in your RelationManager to fetch this union.

1. Setup on the Model

Assuming your pivot table is named contract_contract, with contract_id and related_contract_id fields:

class Contract extends Model
{
    public function relatedContractsA()
    {
        return $this->belongsToMany(
            Contract::class,
            'contract_contract',
            'contract_id',
            'related_contract_id'
        );
    }

    public function relatedContractsB()
    {
        return $this->belongsToMany(
            Contract::class,
            'contract_contract',
            'related_contract_id',
            'contract_id'
        );
    }

    // Unified method to get all related contracts both ways
    public function getAllRelatedContractsAttribute()
    {
        return $this->relatedContractsA->merge($this->relatedContractsB);
    }
}

2. Filament Custom Relation Query

In your RelatedContractsRelationManager.php, instead of using the default relatedContractsA() or relatedContractsB(), you can override the query to return a union:

use Filament\Resources\RelationManagers\RelationManager;

class RelatedContractsRelationManager extends RelationManager
{
    // ...

    public static function getRelationshipQuery($ownerRecord)
    {
        $id = $ownerRecord->id;

        // Get contracts where this contract is the "contract_id"
        $a = \App\Models\Contract::query()
            ->whereHas('relatedContractsA', function($q) use ($id) {
                $q->where('contracts.id', $id);
            });

        // Get contracts where this contract is the "related_contract_id"
        $b = \App\Models\Contract::query()
            ->whereHas('relatedContractsB', function($q) use ($id) {
                $q->where('contracts.id', $id);
            });

        // Union the two queries
        return $a->union($b);
    }
}

Note: Filament expects Eloquent Relations in relation managers, so you may need to use a custom widget, table, or override the resource's related list display if you can't directly inject a union in the RelationManager. Alternatively, you might create a view accessor and list contracts via a Card or Table on the contract's detail page, using $record->all_related_contracts.

3. Simpler Approach: Accessor + Custom Table

Instead of using a Relation Manager, you can display related contracts like this in your Filament resource:

// In your ContractResource/Pages/ViewContract.php

use Filament\Resources\Pages\Page;
use Filament\Tables;
use Filament\Tables\Columns\TextColumn;

class ViewContract extends Page implements Tables\Contracts\HasTable
{
    use Tables\Concerns\InteractsWithTable;

    // ...

    protected function getTableQuery()
    {
        // $this->record is your current Contract model instance
        $ids = $this->record->all_related_contracts->pluck('id')->unique();

        return \App\Models\Contract::query()->whereIn('id', $ids);
    }

    protected function getTableColumns(): array
    {
        return [
            TextColumn::make('name'),
            // ... other columns
        ];
    }
}

Summary

  • Define both "directions" of the relationship.
  • Use an accessor (all_related_contracts) to merge both.
  • Display related contracts (using this accessor) in Filament via a table or a custom widget/page.

If RelationManager is a must:
You will need a custom RelationManager that can work with that unioned query, or consider using Polymorphic Relation Managers with custom queries as shown above.

Let me know if you need a more specific example for your Filament resource/page or further clarification!

earmsby's avatar

I don't have a requirement to use the RelationManager, but I'm confused about how to implement the "Accessor + Custpm Table" method shown in the AI repsonse.

Here is my current ViewContract.php code

namespace App\Filament\Resources\Contracts\Pages;

use App\Filament\Resources\Contracts\ContractResource;
use Filament\Actions\EditAction;
use Filament\Resources\Pages\ViewRecord;

class ViewContract extends ViewRecord
{
    protected static string $resource = ContractResource::class;

    protected function getHeaderActions(): array
    {
        return [
            EditAction::make()
            ->slideOver()
            ->mutateDataUsing(function (array $data): array {
                $data['last_mod_by'] = auth()->id();
                return $data;
            }),
        ];
    }
}

This seems quite different from the example, especially class ViewContract extends ViewRecord vs. class ViewContract extends Page implements Tables\Contracts\HasTable shown in the example. How would I incorporate the code for the related contracts into my existing view or do I replace it entirely?

earmsby's avatar

I'm still struggling to make something work to show ALL the related contracts for any given contract.

I attempted to use a relation manager on the Contract view. In my Contract model I have this:

public function relatedContractsA()
    {
        return $this->belongsToMany(
            Contract::class,
            'contract_contract',
            'contract_id',
            'related_contract_id'
        );
    }

    public function relatedContractsB()
    {
        return $this->belongsToMany(
            Contract::class,
            'contract_contract',
            'related_contract_id',
            'contract_id'
        );
    }

    // Unified method to get all related contracts both ways
    public function getAllRelatedContractsAttribute()
    {
        return $this->relatedContractsA->merge($this->relatedContractsB);
    }

Then I have a relation manager on the Contract filament resource called: RelatedContractsRelationManager.php which has this function to create a Union query:

    public static function getRelationshipQuery($ownerRecord)
    {
        $id = $ownerRecord->id;

        // Get contracts where this contract is the "contract_id"
        $a = Contract::query()
            ->whereHas('relatedContractsA', function ($q) use ($id) {
                $q->where('contracts.id', $id);
            });

        // Get contracts where this contract is the "related_contract_id"
        $b = Contract::query()
            ->whereHas('relatedContractsB', function ($q) use ($id) {
                $q->where('contracts.id', $id);
            });

        // Union the two queries
        return $a->union($b);
    }

RelatedContractsRelationManager.php then uses that function like so:

When I attempt to view the relation in the browser, I get this error:

SQLSTATE[42000]: Syntax error or access violation: 1250 Table 'contracts' from one of the SELECTs cannot be used in global ORDER clause (Connection: mysql, SQL: (select * from `contracts` where exists (select * from `contracts` as `laravel_reserved_8` inner join `contract_contract` on `laravel_reserved_8`.`id` = `contract_contract`.`related_contract_id` where `contracts`.`id` = `contract_contract`.`contract_id` and `contracts`.`id` = 17)) union (select * from `contracts` where exists (select * from `contracts` as `laravel_reserved_9` inner join `contract_contract` on `laravel_reserved_9`.`id` = `contract_contract`.`contract_id` where `contracts`.`id` = `contract_contract`.`related_contract_id` and `contracts`.`id` = 17)) order by `contracts`.`id` asc limit 10 offset 0)

I'm assuming that Filament adds the ORDER BY clause with LIMIT and OFFSET. So, I'm not sure how I can the custom query to work.

If I could figure out the accessor method that Larry AI suggests, I'd give that a try but I don't really understand how to implement that. Hoping someone can point me in the right direction either about solving the custom query on the relation manager or using the accessor method instead of a relation manager.

Please or to participate in this conversation.