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

Eloquent Relativity Laravel Package

imanghafoori/eloquent-relativity

Laravel package that lets you declare Eloquent relationships without importing related model classes. Relativity infers the target model from the method name, so hasMany, hasOne, belongsTo and belongsToMany calls stay concise and your models keep fewer use statements.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire up

composer require imanghafoori/eloquent-relativity

The package is tiny and does not require a service provider registration on Laravel 5.5+ (auto-discovery). Publish the config only if you need to change the relatives namespace:

php artisan vendor:publish --tag=relativity

Where to look first (in this order)

  1. The Relative base class — this is what your relation definitions extend, and it proxies Eloquent's hasMany / hasOne / belongsTo / morphMany builders, so anything you write today inside a model works inside a relative.
  2. The HasRelatives trait — the one-line opt-in added to models. Understanding its __get/__call resolution tells you exactly how a relation name maps to a relative method.
  3. The config file for the relatives namespace (typically App\Relatives), which controls where the package looks for {Model}Relative classes.

First use case: shrink a bloated model

Pick the model with the most relation methods — usually User. Move one relation out and verify the public API is unchanged:

// before
class User extends Model
{
    public function posts() { return $this->hasMany(Post::class); }
    public function profile() { return $this->hasOne(Profile::class); }
}

// after
use Imanghafoori\Relativity\HasRelatives;

class User extends Model
{
    use HasRelatives;
}
namespace App\Relatives;

use Imanghafoori\Relativity\Relative;
use App\Models\Post;

class UserRelative extends Relative
{
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

Then confirm the two things every existing caller relies on still work:

$user->posts;                              // lazy-loaded collection
$user->posts()->where('published', true)->get();  // query builder chain
User::with('posts')->get();                // eager loading

If those three pass, the refactor is behaviourally transparent. Do one relation at a time; this is not a big-bang migration.

Implementation Patterns

Relatives as the only place that knows the graph

The real payoff is that User no longer imports Post, Profile, Comment, etc. The relative class becomes the single seam where the object graph is declared. That makes it the natural place to swap implementations later — for tests, for a feature-flagged model, or for renaming a domain model without touching the consumer.

Keep this discipline: a relative should contain only relation definitions. Scopes, accessors, and business logic stay on the model (or move to dedicated query/action classes). Otherwise you've just relocated the bloat.

Everything Eloquent does keeps working — treat relations identically

Because resolution happens at access time, the following are unaffected and should be part of your test suite during migration:

User::with('posts.comments')->find($id);       // nested eager loading
User::has('posts')->get();                     // whereHas / has
User::whereHas('posts', fn ($q) => $q->where('published', true))->get();
$user->loadMissing('posts');
$user->posts()->create([...]);                 // relation writes
$user->posts()->sync([...]);                   // for many-to-many relatives

Convention vs explicit registration

The default resolution is naming-convention based (App\Models\User → App\Relatives\UserRelative). For atypical namespaces or domain modules, register the mapping explicitly in config rather than relying on the convention — explicit beats a silent "relation not found" at runtime. Always run php artisan config:clear after changing the namespace config, and config:cache in production.

Organise relatives by domain, not by model count

If you have hundreds of models, mirror your domain folders:

app/Relatives/Billing/InvoiceRelative.php
app/Relatives/Support/TicketRelative.php

This keeps the decoupling benefit real — you can reason about a slice of the graph without opening model files.

Testing and fakes

The clean integration point is the relative class. In tests, bind a test relative (or override the config mapping) to point at a lightweight or in-memory model, and the calling code is none the wiser:

// tests/TestCase.php
config(['relativity.namespace' => 'Tests\\Relatives']);

For integration tests, keep using RefreshDatabase and assertDatabaseHas — nothing about the persistence layer changes.

Incremental adoption

Adopt per model, per aggregate root. Start with models that have the most relations and the fewest inbound dependencies. Leave leaf models alone; the churn isn't worth it. A good rule of thumb: extract when a model exceeds ~15 relation methods or when relation definitions dominate the file's line count.

Gotchas and Tips

Your IDE and static analysis go blind

This is the single biggest day-to-day cost. $user->posts is now resolved magically, so PhpStorm and Larastan will flag undefined properties and lose autocompletion on relation queries. Mitigations:

  • Add @property-read Collection<int, Post> $posts and @method \Illuminate\Database\Eloquent\Relations\HasMany posts() docblocks to the model.
  • barryvdh/laravel-ide-helper will not generate these for you — it reads the model file, which no longer contains the relation. Write them by hand or generate them from the relatives directory with a small custom command.
  • Without this, Larastan at level 5+ will flood your CI. Budget for the docblocks up front.

Name collisions resolve ambiguously

A relative method named the same as a real model method, attribute, or accessor will behave unpredictably — the trait's magic __get competes with Eloquent's own accessor resolution. Namespace relation names clearly (posts, not count) and grep the model for collisions before moving anything.

Resolution failures are runtime, not compile-time

If the relative class or method is missing, you get an exception at query time — often deep inside a controller, sometimes only in production paths your tests don't cover. Smoke-test every relation you move with a one-line feature test:

public function test_user_relations_resolve(): void
{
    $user = User::factory()->hasPosts(1)->create();
    $this->assertNotNull($user->posts);
}

Lazy loading guards still apply

Model::preventLazyLoading() (and preventAccessingMissingAttributes()) hooks into Eloquent's relation resolution, so the package respects them. Good — but it means a relation you moved from a model to a relative will still throw LazyLoadingViolationException if you forget to eager load. That's a feature, not a bug; keep the guard on in non-production.

Config cache is the usual "it worked locally" culprit

Changing the relatives namespace and forgetting php artisan config:cache in deploy is the most common failure mode. Add it to your deploy script alongside the other cache commands.

Migration order matters for whereHas chains

If you move posts on User but Post still declares comments inline, nested with('posts.comments') works fine. But if you later move comments to PostRelative, re-run your nested eager-load tests — that's the level at which resolution regressions hide.

Extension points

The package is intentionally small: the Relative base class, the HasRelatives trait, and the config namespace. If you need behaviour beyond that (custom resolution rules, class maps, per-tenant relative namespaces), extend the base class or swap the config binding — do not patch the vendor folder. The surface area is small enough that a thin wrapper in your app is cheaper than a fork.

Maturity / risk assessment

148 stars, zero dependents, last release April 2024, single maintainer. It's a clean, focused idea with a tiny codebase you can read end-to-end in an afternoon — that's the mitigating factor. For a long-lived production codebase, either vendor it into a packages/ directory you control or make the abstraction yours (a thin interface + resolver) so you're never blocked on upstream. It's best adopted as a convention-plus-small-helper rather than as load-bearing infrastructure, and it's genuinely useful on teams large enough that model files have become merge-conflict hotspots.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin