whilesmart / eloquent-owner-access
Pluggable polymorphic owner authorization for Laravel packages with owner_type/owner_id columns. Lets host apps decide who can see and modify owner-scoped records without coupling the underlying packages to a specific tenancy model.
Package info
github.com/whilesmartphp/eloquent-owner-access
pkg:composer/whilesmart/eloquent-owner-access
Requires
- php: ^8.2
- laravel/framework: ^11.0|^12.0
Requires (Dev)
- laravel/pint: ^1.22
- orchestra/testbench: ^9.0|^10.0
This package is auto-updated.
Last update: 2026-04-25 11:53:46 UTC
README
Pluggable polymorphic owner authorization for Laravel packages with owner_type / owner_id columns.
Domain packages (accounts, expenses, payments, invoices, products, customers, ...) expose CRUD endpoints scoped by a polymorphic owner. They should not have to know how the host app decides who owns what. This package gives them a single contract to consult and a default that preserves existing "trust the client" behavior, so adoption is incremental and safe.
Concept
One contract:
interface OwnerAuthorizer { public function authorize(?Authenticatable $user, string $ownerType, mixed $ownerId): bool; public function scope( Builder $query, ?Authenticatable $user, string $ownerTypeColumn = 'owner_type', string $ownerIdColumn = 'owner_id', ): Builder; }
authorize() is per-record (called from FormRequest::authorize() for store/update and from controllers for show/destroy). scope() constrains index queries.
The default binding is AllowAllAuthorizer (everything goes through). Hosts that want strict tenancy bind their own implementation.
Installation
composer require whilesmart/eloquent-owner-access
The service provider auto-registers via Laravel package discovery and bindIfs the default authorizer.
Usage in a package
In a StoreXRequest:
use Whilesmart\OwnerAccess\Concerns\AuthorizesOwnerRequest; class StoreAccountRequest extends FormRequest { use AuthorizesOwnerRequest; public function authorize(): bool { return $this->authorizeOwnerInRequest(); } public function rules(): array { /* ... */ } }
In an UpdateXRequest:
public function authorize(): bool { return $this->authorizeOwnerOfBoundModel('account'); }
In the controller:
use Whilesmart\OwnerAccess\Concerns\AuthorizesOwnerController; class AccountController extends Controller { use AuthorizesOwnerController; public function index(Request $request) { $query = Account::query(); $query = $this->scopeAccessibleOwners($query, $request->user()); // ... } public function show(Account $account, Request $request) { $this->authorizeAccessTo($account, $request->user()); // ... } public function destroy(Account $account, Request $request) { $this->authorizeAccessTo($account, $request->user()); $account->delete(); // ... } }
Update is already covered by UpdateXRequest::authorize(), so no controller change is needed there.
Usage in a host app
By default everything is allowed. To enforce tenancy, write an authorizer that consults your tenancy model and bind it:
namespace App\Authorization; use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Database\Eloquent\Builder; use Whilesmart\OwnerAccess\Contracts\OwnerAuthorizer; class WorkspaceMemberAuthorizer implements OwnerAuthorizer { public function authorize(?Authenticatable $user, string $ownerType, mixed $ownerId): bool { if ($user === null || $ownerType !== \App\Models\Workspace::class) { return false; } return $user->workspaces()->whereKey($ownerId)->exists(); } public function scope(Builder $query, ?Authenticatable $user, string $ownerTypeColumn = 'owner_type', string $ownerIdColumn = 'owner_id'): Builder { if ($user === null) { return $query->whereRaw('0 = 1'); } return $query->where($ownerTypeColumn, \App\Models\Workspace::class) ->whereIn($ownerIdColumn, $user->workspaces()->pluck('workspaces.id')); } }
Then in AppServiceProvider::register():
$this->app->bind( \Whilesmart\OwnerAccess\Contracts\OwnerAuthorizer::class, \App\Authorization\WorkspaceMemberAuthorizer::class, );
That single binding switches every consuming package over to strict mode at once.
Backwards compatibility
The provider uses bindIf, so any explicit binding the host registers (before or after) wins. Without an explicit binding, the default AllowAllAuthorizer keeps current behavior.
Testing
composer test
License
MIT.