Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Extended Relationships Laravel Package

mr-punyapal/laravel-extended-relationships

Extra Eloquent relationship helpers for Laravel that cut database queries, improve performance, and reduce duplicate code. Requires PHP 8.3+ and Laravel 12/13, with easy Composer installation.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up (2 minutes). The package is a drop-in trait — no service provider config to publish, no migrations, no config file.

composer require mrpunyapal/laravel-extended-relationships

Note the Packagist name is lowercase mrpunyapal/... while the GitHub org is MrPunyapal; use the lowercase form in composer.json. Version 3.x targets PHP 8.3+ and Laravel 12/13 only — if you are still on Laravel 11 or PHP 8.2, pin ^2.3.

Then add the trait to the model that owns the relationship:

use MrPunyapal\LaravelExtendedRelationships\HasExtendedRelationships;

class Post extends Model
{
    use HasExtendedRelationships;
}

Where to look first. The README is the whole manual — there are only four methods, so read them in this order because they pair up:

  1. belongsToManyKeys + hasManyKeys (the audit-column pair)
  2. hasManyArrayColumn + belongsToArrayColumn (the array-column pair)

If your app uses Laravel Boost, publish the shipped skill instead of guessing:

composer require laravel/boost --dev
php artisan boost:install
# later, if the package was added after Boost:
php artisan boost:update --discover

That gives you the laravel-extended-relationships-development skill, which is genuinely the fastest way to get correct signatures out of an AI assistant for this package.

Your first real use case: audit columns. Almost every Laravel codebase has created_by, updated_by, deleted_by on a table and three near-identical belongsTo relations on the model, followed by $post->load('creator', 'updater', 'deleter') — three queries for one row's metadata. Replace all three with one relation:

// app/Models/Post.php
public function auditors()
{
    return $this->belongsToManyKeys(
        related: User::class,
        foreignKey: 'id',
        relations: [
            'created_by' => 'creator',
            'updated_by' => 'updater',
            'deleted_by' => 'deleter',
        ],
    );
}
$post = Post::with('auditors')->first();

$post->auditors->creator; // User or null
$post->auditors->updater;
$post->auditors->deleter;

One query instead of three, and lazy loading (Post::find(7)->auditors->creator) works identically, so you can retrofit it without touching every call site that already relies on lazy access.

Implementation Patterns

Pattern 1 — Consolidate audit columns into one relation, then delete the old ones. The single highest-value refactor. Before: creator(), updater(), deleter() on the model plus with(['creator','updater','deleter']) scattered across controllers, resources, and Blade views. After: one auditors() relation, one entry in with(). Do the inverse on User at the same time so you can answer "what did this user touch?" in one query:

// app/Models/User.php
public function audited()
{
    return $this->hasManyKeys(
        related: Post::class,
        relations: [
            'created_by' => 'created',
            'updated_by' => 'updated',
            'deleted_by' => 'deleted',
        ],
        localKey: 'id',
    );
}
$user->audited->created; // Collection of posts
$user->audited->updated;
$user->audited->deleted;

This is the pattern that survives contact with real code: it makes an "activity" screen trivially cheap instead of a union of three queries.

Pattern 2 — Treat the grouped result as a named bag, not a collection. $post->auditors is not a Collection; it exposes your mapping values as properties. That means you write $post->auditors->creator in views and API resources, and you cannot ->map(), ->pluck(), or ->count() across the group the way you would a normal relation. When you genuinely need to iterate, be explicit:

// API resource
return [
    'creator' => $post->auditors->creator?->only('id', 'name'),
    'updater' => $post->auditors->updater?->only('id', 'name'),
];

Pattern 3 — Model as eager-loading hygiene, not a query builder. The win comes from with('auditors') in list endpoints. A 50-row index page goes from 151 queries to 2. Audit every controller that serialises a list and add the relation to the eager-load array — this is the review checklist item that catches regressions.

Pattern 4 — JSON array columns for "tag"-style references. When a table stores a list of foreign keys inline (feature flags, allowed company IDs, member lists):

class User extends Model
{
    use HasExtendedRelationships;

    protected $casts = ['companies' => 'array'];

    public function myCompanies()
    {
        return $this->hasManyArrayColumn(
            related: Company::class,
            foreignKey: 'id',
            localKey: 'companies',
        );
    }
}

And the inverse on Company, which is the pattern people forget exists:

public function companyFounders()
{
    return $this->belongsToArrayColumn(
        related: User::class,
        foreignKey: 'id',
        localKey: 'companies',
        isString: true, // JSON stored as ["7","71"] but keys are ints
    );
}

Pattern 5 — Workflow for introducing it into an existing app. (1) Add the trait to one model. (2) Define the new relation alongside the old ones — no rename, no breakage. (3) Swap with() arrays in one endpoint. (4) Assert the query count with DB::listen or Telescope. (5) Delete the old relations last, in a separate commit, once nothing references them. Migrating relation-by-relation beats a big-bang change because the four methods are additive.

Integration tips. The relations behave like first-class Eloquent relations for with() and lazy loading, so they slot into existing policies, API resources, and ->load() calls without changing your data shape — the only difference is the grouped accessor. Combine with scoped eager loads carefully: nested constraints like with(['auditors' => fn ($q) => $q->select('id','name')]) should be verified in a test before you rely on the column selection, since the relation resolves a grouped bag rather than a plain set.

Gotchas and Tips

The second argument is not what you think it is. In belongsToManyKeys, foreignKey: 'id' is the key on the related table used to match all the mapped values — the columns on your table are the keys of the relations array (created_by, …). In hasManyKeys, localKey: 'id' is your model's key and the relations keys are columns on the related table. Reading the README's example as "foreignKey is my column" produces empty results and a long afternoon. Say the direction out loud before writing the array.

Name collisions are permanent. The values in relations (creator, updater, …) are exposed as properties on the resolved bag. If Post already has a real creator() relation, keep both concepts distinct or you will write $post->creator in one place and $post->auditors->creator in another and confuse the next developer. Prefer an auditors / audited naming pair and reserve those for this package.

hasManyArrayColumn needs the cast, and nothing else will tell you. Without protected $casts = ['companies' => 'array'] the column comes back as a string and the relation silently misbehaves. This is the number one support question for this package.

String vs int keys in JSON columns. If your column stores ["7","71"] but your keys are integers, belongsToArrayColumn returns nothing unless you pass isString: true. Conversely, do not pass isString: true on integer-stored arrays. Check what the column actually contains with SELECT companies FROM users WHERE id = 71 before debugging the relation.

Don't expect indexes to rescue array-column relations. Array containment over a JSON/text column is not a normal indexable whereIn on a primary key; on large tables this scans. If the relation is on a hot path or the array grows unbounded, model it as a proper pivot table with a real belongsToMany and use this package only for the long tail of legacy columns. Choosing a JSON column here is a schema decision, not a package decision.

Null foreign keys. Audit columns are frequently nullable (deleted_by IS NULL for live rows). Always code defensively with ?-> on the grouped accessors, and add a test fixture with a null column before shipping — the failure mode (exception vs. null property) is worth knowing for your exact version.

Upgrade traps. The 1.x → 2.x release renamed the namespace to MrPunyapal\LaravelExtendedRelationships and renamed both the trait (LaravelExtendedRelationships → HasExtendedRelationships) and three of the four methods (HasManyWithColumnKeyArray → HasManyArrayColumn, BelongsToManyWithManyKeys → BelongsToManyKeys, HasManyWithManyKeys → HasManyKeys). If you find a tutorial using the old names, it is 1.x. The 2.x → 3.x release drops Laravel 11 and PHP 8.2, so check your CI matrix before bumping.

Debugging in one step. Wrap the endpoint in a listener and assert the count is exactly 2 (parent + one eager load), not 3+:

DB::listen(fn ($q) => logger($q->sql));

If you see three queries, with() is missing somewhere upstream, or a policy/resource is touching $post->auditors on a model loaded without it.

Extension points are deliberately thin. There is no config to tune and no macro surface — the extension point is the trait plus the four relation classes. If you need behaviour outside those four shapes (constraints in eager loads, aggregates on grouped relations, whereHas against the grouped relation), stop and use a normal hasMany/belongsToMany instead. Treat the package as a narrow vocabulary for four specific query shapes; reaching past that is where the friction lives.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo