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

Relations Laravel Package

ghanuz/relations

A Laravel package for working with Eloquent model relationships, providing helpers to define, query, and manage related records more cleanly. Simplify complex relations and keep your models concise and readable.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package via Composer (there is no service provider to register on modern Laravel — discovery handles it, but confirm against the README since the last tagged release was 2020):

composer require ghanuz/relations

The entry point is a trait you add to the model that owns the foreign keys. It exposes relationship methods that build Eloquent relations backed by FIND_IN_SET (for comma-separated columns) and JSON_CONTAINS (for JSON columns) instead of a pivot table.

use Illuminate\Database\Eloquent\Model;
use Ghanuz\Relations\Traits\HasRelations; // verify exact namespace in the README

class Post extends Model
{
    use HasRelations;

    public function tags()
    {
        return $this->belongsToManyInSet(Tag::class, 'tag_ids');
    }
}

With a posts.tag_ids column containing "3,17,42", $post->tags returns the matching Tag models, and $post->tags()->where('active', 1) composes normally because you are still working with a query builder underneath.

Where to look first: the trait itself and the custom relation classes it returns. This package is small — reading src/ end to end takes minutes and is far more reliable than the README, which may lag behind the code.

First use case: you have a legacy posts table with a category_ids varchar column and you need $post->categories to behave like a real relation (eager loading, counts, whereHas) without a schema migration. That is the sweet spot. If you are designing a schema from scratch, use a proper pivot table instead — see Gotchas.

Implementation Patterns

Comma-separated column on the parent (belongs-to-many semantics). The parent row stores the list of related IDs:

public function tags()
{
    return $this->belongsToManyInSet(Tag::class, 'tag_ids');
}

Comma-separated column on the related table (has-many semantics). Use this when many child rows each reference the parent via a denormalized list, e.g. comments.post_ids containing "1,4":

public function comments()
{
    return $this->hasManyInSet(Comment::class, 'post_ids');
}

JSON columns (MySQL 5.7+ / MariaDB 10.2+). Same shape, different backing function:

public function tags()
{
    return $this->belongsToManyInJson(Tag::class, 'meta->tag_ids');
}

Eager loading. This is the reason to reach for the package instead of a raw accessor — with() works, so list endpoints avoid N+1:

$posts = Post::with('tags')->paginate(25);

Filtering by relation. Because the returned object is a real relation, whereHas and has compose:

Post::whereHas('tags', fn ($q) => $q->where('slug', 'laravel'))->get();

Writing the list. There is no pivot table, so attach()/sync() do not exist. Manage the column explicitly — centralize it so trimming and ordering stay consistent:

public function syncTagIds(array $ids): void
{
    $this->tag_ids = implode(',', array_values(array_unique(array_map('intval', $ids))));
    $this->save();
}

API resources. Treat it like any relation; whenLoaded guards against accidental lazy loading:

'tags' => TagResource::collection($this->whenLoaded('tags')),

Scoping for multi-tenant or filtered lists. Chain constraints exactly as you would on a pivot relation; the package only changes the join predicate, not the builder.

Migration path. If the column is only a stopgap, wrap it behind a relation name you can later swap to belongsToMany without touching call sites — that lets you ship the package now and normalize the schema later.

Gotchas and Tips

MySQL-only. FIND_IN_SET and JSON_CONTAINS do not exist in Postgres or SQLite. Locally you may be fine on SQLite; your test suite will break the moment it hits the CI database. Consider a query macro or a test-only shim, or skip the package entirely if you are not committed to MySQL/MariaDB.

FIND_IN_SET cannot use an index on the id column the way a join can — the server evaluates the function per row of the related table. On large related tables this degrades into a scan per parent row. For a tags table with hundreds of rows it is invisible; for users with millions it is a production incident. Profile with EXPLAIN before shipping, and watch eager-loaded queries especially.

JSON has better options than comma-separated. MySQL 8 supports functional indexes and multi-valued indexes on JSON arrays; MariaDB does not implement the same syntax. If you control the schema, JSON arrays are the safer denormalized choice, but verify which engine you actually run — the two treat JSON_CONTAINS path syntax differently around scalars vs. arrays.

Duplicates and ordering. FIND_IN_SET returns at most one match per related row, so a field like "3,3,3" yields one Tag. Conversely, the result order is whatever the database returns — not the order of IDs in the column. If order matters, add an orderByRaw or accept that it is unspecified.

Whitespace and type coercion. "3, 17, 42" (with spaces) breaks FIND_IN_SET silently — no error, just missing results. Always implode(',') from integers and never hand-concatenate. Empty string vs. NULL are also not equivalent; decide on one and normalize on write.

No attach/detach/sync/withPivot. Any code you copy from a pivot-based relation will fail. Route all writes through a single method on the model (see the pattern above) so you cannot corrupt the column from a controller.

Soft deletes and global scopes. Confirm the custom relation applies the related model's global scopes — an old package may build the base query in a way that bypasses them. Test with a SoftDeletes model explicitly; this is the single easiest bug to ship unnoticed.

Maintenance risk. Six stars, zero dependents, no release since September 2020. There is no evidence of compatibility testing against Laravel 9/10/11. Pin the exact version in composer.json, and vendor or fork the trait if you depend on it long-term — it is only a few dozen lines, so forking is often cheaper than adopting.

Debugging. The generated SQL is the whole story: call ->toSql() on the relation to see the FIND_IN_SET clause, and use DB::listen() to catch the eager-load query. If results look wrong, log the raw column value first — malformed data is the cause far more often than the package.

Extension points. The returned relation extends Eloquent's relation classes, so overriding addConstraints() in a subclass is the clean way to add tenant scoping or default ordering without repeating it at every call site. Keep that subclass in your app, not in vendor.

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.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log