Eloquent Custom Relations: Pivots, HasManyThrough &amp; Raw Joins | Mohamed Said        [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://www.msaied.com) [ Home ](https://www.msaied.com) [ Projects ](https://www.msaied.com/projects) [ Articles  ](https://www.msaied.com/articles) [ Certificates ](https://www.msaied.com/certificates) [ Contact ](https://www.msaied.com#contact-section) 

       [  ](https://github.com/EG-Mohamed)       

 [ Home ](https://www.msaied.com) [ Projects ](https://www.msaied.com/projects) [ Articles ](https://www.msaied.com/articles) [ Certificates ](https://www.msaied.com/certificates) [ Contact ](https://www.msaied.com#contact-section) 

  [ home ](https://www.msaied.com)    [ articles ](https://www.msaied.com/articles)    Eloquent Custom Relations: Polymorphic Pivots, HasManyThrough Tricks, and Raw Join Relations        On this page       1. [  Why Eloquent's Built-in Relations Are Not Always Enough ](#why-eloquents-built-in-relations-are-not-always-enough)
2. [  Case 1: A Raw-Join Relation for Denormalised Reporting Tables ](#case-1-a-raw-join-relation-for-denormalised-reporting-tables)
3. [  Case 2: HasManyThrough Across a Polymorphic Pivot ](#case-2-hasmanythrough-across-a-polymorphic-pivot)
4. [  Case 3: Polymorphic Pivot with Extra Columns ](#case-3-polymorphic-pivot-with-extra-columns)
5. [  Eager Loading Gotchas ](#eager-loading-gotchas)
6. [  Takeaways ](#takeaways)

  ![Eloquent Custom Relations: Polymorphic Pivots, HasManyThrough Tricks, and Raw Join Relations](https://cdn.msaied.com/210/b47272214946c6adcd02ddf74b7df816.png)

  #laravel   #eloquent   #database   #orm  

 Eloquent Custom Relations: Polymorphic Pivots, HasManyThrough Tricks, and Raw Join Relations 
==============================================================================================

     16 Jun 2026      3 min read    ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said  

       Table of contents

1. [  01   Why Eloquent's Built-in Relations Are Not Always Enough  ](#why-eloquents-built-in-relations-are-not-always-enough)
2. [  02   Case 1: A Raw-Join Relation for Denormalised Reporting Tables  ](#case-1-a-raw-join-relation-for-denormalised-reporting-tables)
3. [  03   Case 2: HasManyThrough Across a Polymorphic Pivot  ](#case-2-hasmanythrough-across-a-polymorphic-pivot)
4. [  04   Case 3: Polymorphic Pivot with Extra Columns  ](#case-3-polymorphic-pivot-with-extra-columns)
5. [  05   Eager Loading Gotchas  ](#eager-loading-gotchas)
6. [  06   Takeaways  ](#takeaways)

 Why Eloquent's Built-in Relations Are Not Always Enough
-------------------------------------------------------

Eloquent ships with nine relation types, but real-world schemas rarely fit neatly into any of them. You end up writing `DB::select` calls, appending attributes in `getXAttribute`, or firing N+1 queries because the "right" relation doesn't exist. The fix is almost always a custom relation class — and it's less work than it looks.

---

Case 1: A Raw-Join Relation for Denormalised Reporting Tables
-------------------------------------------------------------

Imagine a `metrics` table that stores pre-aggregated rows keyed by `entity_type` and `entity_id`. You want `$post->metrics` to behave like a first-class relation — eager-loadable, constrainable, and serialisable.

Extend `Illuminate\Database\Eloquent\Relations\Relation` directly:

```php
namespace App\Relations;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;

class HasMetrics extends Relation
{
    public function addConstraints(): void
    {
        if (static::$constraints) {
            $this->query
                ->where('entity_type', $this->parent->getMorphClass())
                ->where('entity_id', $this->parent->getKey());
        }
    }

    public function addEagerConstraints(array $models): void
    {
        $this->query
            ->where('entity_type', $this->parent->getMorphClass())
            ->whereIn('entity_id', $this->getKeys($models));
    }

    public function initRelation(array $models, $relation): array
    {
        foreach ($models as $model) {
            $model->setRelation($relation, $this->related->newCollection());
        }
        return $models;
    }

    public function match(array $models, Collection $results, $relation): array
    {
        foreach ($models as $model) {
            $matched = $results->where('entity_id', $model->getKey());
            $model->setRelation($relation, $matched->values());
        }
        return $models;
    }

    public function getResults(): Collection
    {
        return $this->query->get();
    }
}

```

Register it on the model:

```php
public function metrics(): HasMetrics
{
    return new HasMetrics(
        (new Metric)->newQuery(),
        $this
    );
}

```

Now `Post::with('metrics')->get()` works without a single extra query.

---

Case 2: HasManyThrough Across a Polymorphic Pivot
-------------------------------------------------

`HasManyThrough` requires concrete foreign keys on the intermediate table. When that table is a polymorphic pivot (`taggables`, `mediables`), the standard relation breaks because it can't scope the intermediate join to a single `*_type`.

The cleanest fix is a scoped `hasManyThrough` using a custom intermediate model:

```php
// Intermediate model scoped to one morph type
class PostTaggable extends Model
{
    protected $table = 'taggables';

    protected static function booted(): void
    {
        static::addGlobalScope('type', fn (Builder $q) =>
            $q->where('taggable_type', Post::class)
        );
    }
}

// On the Post model
public function tags(): HasManyThrough
{
    return $this->hasManyThrough(
        Tag::class,
        PostTaggable::class,
        'taggable_id',   // FK on taggables -> posts
        'id',            // FK on tags
        'id',            // local key on posts
        'tag_id'         // local key on taggables
    );
}

```

The global scope on `PostTaggable` ensures the join is always filtered by `taggable_type`, making eager loading safe.

---

Case 3: Polymorphic Pivot with Extra Columns
--------------------------------------------

When a polymorphic pivot carries extra data (e.g., `order`, `metadata`), `MorphToMany` supports `withPivot()` just like `BelongsToMany`:

```php
public function media(): MorphToMany
{
    return $this->morphToMany(Medium::class, 'mediable')
        ->withPivot(['order', 'caption'])
        ->orderByPivot('order');
}

```

Access pivot data via `$medium->pivot->caption`. For typed pivot models, swap to `using(MediablePivot::class)` and cast columns there — keeping the domain logic off the parent model.

---

Eager Loading Gotchas
---------------------

- **`addEagerConstraints` must not call `addConstraints` logic** — the parent key set replaces the single-model constraint entirely.
- **`getKeys()` strips nulls** — guard against models without a primary key before calling it.
- Custom relations are not automatically recognised by `Model::$with`; declare them explicitly.

---

Takeaways
---------

- Extending `Relation` directly gives you full control over eager loading without raw DB calls.
- Scoped intermediate models solve the polymorphic `HasManyThrough` problem cleanly.
- `withPivot` + `using` keeps extra pivot data typed and testable.
- Custom relations serialise, cache, and constrain exactly like built-in ones.
- The four methods to implement are `addConstraints`, `addEagerConstraints`, `initRelation`, and `match`.

 Found this useful?

          [  ](https://twitter.com/intent/tweet?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Feloquent-custom-relations-polymorphic-pivots-hasmanythrough-tricks-and-raw-join-relations&text=Eloquent+Custom+Relations%3A+Polymorphic+Pivots%2C+HasManyThrough+Tricks%2C+and+Raw+Join+Relations) [  ](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fwww.msaied.com%2Farticles%2Feloquent-custom-relations-polymorphic-pivots-hasmanythrough-tricks-and-raw-join-relations) 

 Frequently Asked Questions 
----------------------------

  3 questions  

     Q01  Can custom Eloquent relations be used inside `with()` for eager loading?        Yes. As long as you implement `addEagerConstraints`, `initRelation`, and `match` correctly, Laravel's eager loader treats your custom relation identically to built-in ones. 

      Q02  Why does HasManyThrough fail with polymorphic pivot tables?        HasManyThrough joins the intermediate table without a `*_type` constraint, so it returns rows for all morph types. Scoping the intermediate model with a global scope that filters by the correct morph type fixes this without changing the relation signature. 

      Q03  Is there a performance cost to custom relation classes?        No measurable overhead beyond what any relation incurs. The query count is identical to a built-in relation when eager loading is used correctly. 

  Continue reading

 More Articles 
---------------

 [ View all    ](https://www.msaied.com/articles) 

 [ ![Laravel Telescope Alternatives: Building a Lightweight Request Inspector with Middleware](https://cdn.msaied.com/216/9b6d240010b80483f072902dafcd216c.png) laravel middleware debugging 

### Laravel Telescope Alternatives: Building a Lightweight Request Inspector with Middleware

Telescope is powerful but heavy for production. Learn how to build a focused, low-overhead request inspector u...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 16 Jun 2026     1 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-telescope-alternatives-building-a-lightweight-request-inspector-with-middleware) [ ![RAG Pipelines in Laravel: Chunking, Embedding, and Retrieval with pgvector](https://cdn.msaied.com/215/e037e13535aa77822f879ee829ec3f68.png) laravel ai pgvector 

### RAG Pipelines in Laravel: Chunking, Embedding, and Retrieval with pgvector

Build a production-ready Retrieval-Augmented Generation pipeline in Laravel using pgvector, OpenAI embeddings,...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 16 Jun 2026     3 min read  

  Read    

 ](https://www.msaied.com/articles/rag-pipelines-in-laravel-chunking-embedding-and-retrieval-with-pgvector) [ ![Laravel Pest: Architecture Tests, Mutation Testing, and Type Coverage in CI](https://cdn.msaied.com/214/0d4822fa8ee1765d0689e387dd849d92.png) laravel pest testing 

### Laravel Pest: Architecture Tests, Mutation Testing, and Type Coverage in CI

Go beyond feature tests. Learn how to enforce architectural rules, catch logic gaps with mutation testing, and...

  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MJ.jpg)  Mohamed Said 

 16 Jun 2026     4 min read  

  Read    

 ](https://www.msaied.com/articles/laravel-pest-architecture-tests-mutation-testing-and-type-coverage-in-ci) 

   [  ![Mohamed Said](https://cdn.msaied.com/01KT78WE565VEMM3PSNQAAB0MH.png)   Mohamed Said Laravel Backend Engineer  ](https://www.msaied.com)Senior Backend Engineer specializing in Laravel, scalable SaaS platforms, APIs, and cloud infrastructure. I build secure, high-performance web applications that help businesses grow.

Explore

- [Home](https://www.msaied.com)
- [Projects](https://www.msaied.com/projects)
- [Articles](https://www.msaied.com/articles)
- [Certificates](https://www.msaied.com/certificates)
- [Contact](https://www.msaied.com#contact-section)

Connect

- [   hello@msaied.com ](mailto:hello@msaied.com)
- [   +20 109 461 9204 ](tel:+201094619204)

© 2026 Mohamed Said. All rights reserved.

 [  ](https://github.com/EG-Mohamed) [  ](https://www.linkedin.com/in/msaiedm/) [  ](https://wa.me/201094619204) [  ](mailto:hello@msaied.com) [  ](https://drive.google.com/file/u/0/d/1MF20IPRJyzfy32mhEutjL5EpSls0w2Q8/view)
