illuminatech/db-role
Laravel package for managing database roles per connection, letting you switch the active role at runtime to enforce permissions, read-only access, and tenant isolation without opening extra connections.
Install
composer require illuminatech/db-role
There is no service provider to publish beyond Laravel’s auto-discovery; the package is a small set of traits and contracts you apply to your own models.
Mental model
This package implements class-table inheritance (a.k.a. joined-table inheritance): one base table holds identity and shared columns, and one or more “role” tables hold role-specific columns, each with a 1:1 foreign key back to the base row. A User might have a Profile role, an Employee role, or both — instead of a wide users table full of nullable columns or a type discriminator.
Where to look first
README.md — the smallest working example.src/ — the Role / HasRoles (or similarly named) traits and the role contract. Read these before writing migrations; they define the attribute/relation names the package expects.tests/ — the fixture models are the best documentation of the intended wiring, including edge cases the README skips.First use case
A users table plus a profiles table with user_id. Apply the parent trait to User, apply the role trait to Profile, declare the relation, then attach/save the role through the parent. Migrations are yours to write — the package does not generate schema, it only orchestrates the save/load/delete of the paired rows.
Composition over a wide table. Reach for this when role columns are sparse (most users have no profile) or when roles are genuinely optional and you want NOT NULL constraints to mean something. It is the right tool when you would otherwise be writing ->whereNotNull('company_name') to decide whether a user is an employee.
Base model as the aggregate root. Treat the base model as the single entry point: create/save the base row, then save the role alongside it. Reads should go base → role. Avoid loading a role row independently and walking back to the parent, except in admin/reporting code.
Optional roles as a nullable relation. Model “does this entity play this role?” as $model->role !== null or via whatever hasRole() helper the trait exposes, rather than a boolean column. This keeps the source of truth in the FK itself.
Delegation for convenience. If role attributes are read constantly (e.g. $user->bio), add explicit accessors on the base model that forward to the role, with a sensible default when the role is absent. Do not add magic __get fallbacks across all attributes — it makes debugging and toArray() unpredictable.
Eager loading. Always with('profile', 'employee') when listing base models in a table or API response. Each role is a separate query; a page of 50 users with three roles is 150 extra queries if you forget.
Transactional writes. Saving a base row and its role touches two tables. Wrap multi-role creation in DB::transaction(), and be deliberate about the FK onDelete behavior — cascadeOnDelete() is usually what you want so orphaned role rows cannot accumulate.
Integration tips
belongsTo back to the parent. Business logic belongs on the base model or a service.This is a low-traffic package. Fifteen stars and a NOASSERTION license mean you should read the source (it is small) and pin the version in composer.json. Vendor it or fork it if you depend on it for a long-lived product; the surface area is small enough that owning it is realistic.
Laravel version support is not guaranteed. Check composer.json constraints against your framework version before adopting, and re-check on every Laravel major upgrade. There is no large community to catch breakage for you.
No schema helpers. You write the migrations, the FK, the unique index on the role’s parent key, and the cascade rules. A missing unique index on profiles.user_id will silently allow duplicate roles and produce confusing “which one loaded?” behavior.
Eager-load naming must match. Relation names are conventional, not enforced. If you rename profile() to userProfile(), every trait method that assumes the default name breaks. Prefer the defaults, or override consistently.
Absent roles are null, not empty models. Guard against $user->profile->bio with null-safe access or a defaulted accessor. This is the single most common runtime error with role/optional-relation designs.
Deletes need thought. Deleting the base row should delete roles (FK cascade). Deleting a role while keeping the base is a legitimate “remove this capability” operation — make sure your UI and any counters/denormalized fields handle that transition, not just creation.
Debugging. Because a logical entity spans multiple rows, dd($user) only shows half the picture. When chasing a bug, log both the base and role queries; Laravel’s query log or Telescope will show them as separate statements and it is easy to miss that the role simply was not saved.
Extension points. The traits are the extension points: override relation names, add your own save<Role>() wrapper, or compose a custom trait that batches multiple role saves in one transaction. If you find yourself fighting the trait, a hand-written hasOne plus a small service class is often clearer than bending the package.
When not to use it. If every entity has every role, or the role columns are few and non-sparse, a plain nullable-columns table is simpler and faster. This package earns its keep only when roles are optional and genuinely distinct.
How can I help you explore Laravel packages today?