santigarcor/laravel-ownable
A Laravel package that adds polymorphic ownership to Eloquent models, letting any model be assigned to users or other entities. It provides traits, relationships, and helpers to query owned records and manage ownership across your application.
Install the package with Composer and verify the version constraint matches your Laravel version (the last tagged release is from late 2023, so check composer show santigarcor/laravel-ownable after install before relying on it in a new Laravel 11/12 app):
composer require santigarcor/laravel-ownable
Add the Ownable trait to the model that represents the owner — almost always User, but it can be any model that acts as an owner (a Team, Organization, Account, etc.):
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Santigarcor\LaravelOwnable\Ownable;
class User extends Model
{
use Ownable;
}
The first use case to wire up is authorizing an action on a child record in a controller or policy. Instead of hand-rolling $comment->post->user_id === auth()->id(), ask the owner directly:
public function update(Comment $comment)
{
abort_unless(auth()->user()->owns($comment), 403);
$comment->update(request()->validated());
}
If the package ships a config file (config/ownable.php), publish it early with php artisan vendor:publish --tag=ownable-config so ownership-chain behavior is visible and version-controlled from day one. If no config is published, read the trait source in vendor/santigarcor/laravel-ownable/src — it is small enough to skim in a few minutes, and you will need to know exactly which methods (owns, ownedBy, etc.) it exposes and how it resolves relations.
Policy integration. The cleanest day-to-day use is inside a policy so ownership logic lives in one place and is reused by controllers, Blade @can, and API resources:
class CommentPolicy
{
public function update(User $user, Comment $comment): bool
{
return $user->owns($comment);
}
}
Ownership through an intermediate relation. The main reason to reach for this package over a one-line user_id comparison is indirect ownership — a Comment belongs to a Post, which belongs to a User. Verify in the source how the trait walks relations; typically you either define the ownership chain on the model or pass the resolved chain. A robust pattern is to keep a single canonical "owner" relation on each model:
class Comment extends Model
{
public function owner(): User
{
return $this->post->owner;
}
}
Then check $user->owns($comment->owner) rather than reimplementing traversal in every controller.
Guard clauses in form requests. Ownership checks pair well with authorize() in a FormRequest, keeping controllers thin:
public function authorize(): bool
{
return $this->user()->owns($this->route('comment'));
}
Blade and API resources. Because policies call owns, you get consistent Blade behavior for free:
@can('update', $comment)
<a href="{{ route('comments.edit', $comment) }}">Edit</a>
@endcan
Testing. Write one test per ownership shape (direct, indirect, polymorphic, unrelated owner) using factories. These tests document the package's behavior for your team and will catch a silent breakage if you later upgrade Laravel or replace the package:
it('lets the post author manage its comments', function () {
$post = Post::factory()->create();
$comment = Comment::factory()->for($post)->create();
expect($post->user->owns($comment))->toBeTrue();
});
Migration path. Since the package is small and thinly maintained, wrap usage behind your own contract (OwnershipResolver or a base Ownable trait that uses the package trait) so a future swap is a single-file change rather than a find-and-replace across policies.
composer.json and add a small integration test suite; treat the package as a convenience trait, not infrastructure.user_id values. With UUIDs or ULIDs mixed with integer keys, or when one side is a string from a request and the other an int from the DB, a loose == may pass where strict === would not. Confirm which the trait uses and cast consistently ($model->user_id vs $model->user->id).morph_type is compared, not just the id — otherwise a Post id 5 and a Team id 5 look identical. Test this explicitly.@can per row, eager-load the chain:$comments = Comment::with('post.user')->paginate();
For large lists, prefer a query constraint (whereHas('post', fn ($q) => $q->where('user_id', $id))) over the trait, since the trait loads models into memory to answer.
owns() on a model that has not been saved will have a null foreign key and generally return false, which can be confusing in store flows — branch on exists before authorizing.owns or uses another package's Ownable trait, methods silently clash. Alias on import if needed: use Ownable { owns as packageOwns; }.false, dump the resolved foreign keys and morph types rather than the whole models:dump($comment->user_id, $comment->post?->user_id, get_class($comment->owner));
Enable DB::listen or ->dd() on the relevant query to see whether the relation was loaded or queried.
owns to consult a custom resolver (role-based ownership, delegated ownership, team membership) and call the package method as a fallback. This keeps the public API your policies depend on stable regardless of the package's fate.How can I help you explore Laravel packages today?