torzer/awesome-landlord
torzer/awesome-landlord is a Laravel package for multi-tenant applications with a landlord/tenant architecture. It helps manage tenants, isolate databases, route by domain or subdomain, and switch tenant contexts, making SaaS and multi-tenant Laravel apps easier to build.
Install the package and register it the Laravel-5.2 way (service provider + facade alias in config/app.php); on 5.5+ auto-discovery should pick it up, but the package predates it, so verify.
composer require torzer/awesome-landlord
php artisan vendor:publish --provider="Torzer\Awesome\Landlord\LandlordServiceProvider"
php artisan migrate
Publish the config first (config/landlord.php) and set the two model classes and foreign keys:
// config/landlord.php
'landlord' => ['model' => App\User::class, 'foreign_key' => 'landlord_id'],
'tenant' => ['model' => Tenant::class, 'foreign_key' => 'tenant_id'],
Wire up the two traits — this is the "where to look first" step:
// app/User.php — owns tenants
use Torzer\Awesome\Landlord\Traits\Landlord;
// app/Tenant.php — represents a tenant (workspace/account)
class Tenant extends Model { /* landlord() relation to owner */ }
// app/Post.php — tenant-scoped data
use Torzer\Awesome\Landlord\Traits\Tenant;
Add a tenant_id (nullable, indexed, FK) column to every tenant-scoped table, then set the active tenant once per request and query normally:
use Torzer\Awesome\Landlord\Facades\Landlord;
Landlord::setTenant($tenant); // or setTenantById($id)
Post::all(); // automatically WHERE tenant_id = ?
That scoping is the first use case and the core value: you write ordinary Eloquent queries and tenancy is enforced by a global scope plus a creating hook that stamps tenant_id on insert.
Resolve the tenant in middleware. The most common workflow is a SetTenant middleware that runs after auth (or resolves from a subdomain/session), calls Landlord::setTenant(...), and lets the rest of the request stay ignorant of tenancy:
public function handle($request, Closure $next)
{
if ($tenant = $request->user()->currentTenant()) {
Landlord::setTenant($tenant);
}
return $next($request);
}
Landlord-side tenant management. The Landlord trait gives the user a tenants() relation, so CRUD over tenants is just Eloquent on the owner model ($user->tenants()->create([...])). Use this for onboarding: create tenant → assign user → set tenant → seed defaults.
Automatic stamping on create. The Tenant trait hooks model creation to fill the configured foreign key, so you should not pass tenant_id manually in ordinary create()/save() calls. Do pass it explicitly (or disable the hook) for imports/seeders.
Bypassing the scope deliberately. Admin panels, cross-tenant reports, and "which tenants have this record?" queries need an escape hatch. The package ships one (a withoutTenant-style helper or a removable global scope); wrap only the minimum surface:
// verify the exact helper name in your pinned version's README
$allPosts = Post::withoutGlobalScope(...)->get();
Queues and console. Tenant state is request-scoped and not serialized. In jobs, receive the tenant id and call Landlord::setTenant() again in handle() before touching scoped models. In console commands, loop tenants explicitly:
foreach (Tenant::all() as $tenant) {
Landlord::setTenant($tenant);
// per-tenant work
}
Testing. Set the tenant in setUp() (or a test trait) so factories and assertions hit the same scope production uses; add at least one test asserting tenant A cannot read tenant B's rows.
Eager loading. Because the scope is a where, with('comments') on a scoped parent is filtered correctly — but only if the related model also uses the Tenant trait. Apply it consistently or relations will leak.
stancl/tenancy / tenancy/tenancy before committing to this for new work.NULL (returns nothing) or skip the scope entirely (returns everything). Test both, and fail closed in middleware if security matters.Model::insert(), raw DB::table() queries, and query-builder writes are not scoped and not stamped — you must set tenant_id yourself.DB::raw join, or a model without the Tenant trait silently sees all rows. Add composite indexes ((tenant_id, ...)) and consider FK constraints so cross-tenant rows are at least detectable.selectRaw, whereRaw, DB::select) ignore the scope; re-add tenant_id manually.Tenant trait, or you can never list all tenants to loop over. Keep tenant scoping on tenant-owned data only.php artisan config:cache) is fine, but if you change landlord.php while cached, tenant resolution will silently keep the old model/fk — clear the cache when debugging "wrong tenant" bugs.landlord_id and tenant_id keys are config-driven; if relations return null, check the config matches the actual columns, not the trait defaults.->toSql() on a scoped query will show the injected tenant_id where clause; if it's missing, the trait or global scope isn't registered on that model. Log Landlord::getTenantId() early in the request to confirm resolution order.How can I help you explore Laravel packages today?