Skip to content

Instantly share code, notes, and snippets.

@nathandaly
Last active May 15, 2024 20:19
Show Gist options
  • Save nathandaly/44a83df5e3ea735eae91652c7a4894c9 to your computer and use it in GitHub Desktop.
Save nathandaly/44a83df5e3ea735eae91652c7a4894c9 to your computer and use it in GitHub Desktop.
Tenancy for Laravel & Filament V3 (Tenant per database)
<?php
/**
* Below is an the extended Filament resource your tenant panel resources
* will have to extend so that the queries are scoped properly.
*/
namespace App\Filament;
use Filament\Resources\Resource;
use Illuminate\Database\Eloquent\Builder;
class BaseResource extends Resource
{
public static function getEloquentQuery(): Builder
{
return static::getModel()::query();
}
}
@if(\Filament\Facades\Filament::getTenant() !== null)
<x-filament::button
color="danger"
icon="gmdi-logout"
:href="config('app.url') . '/admin/tenancy/tenants'"
tag="a"
>
Exit Tenant
</x-filament::button>
@endif
<?php
/**
* This middleware extends the Tenancy for Laravel one to catch
* tenant ID and scope future requests in the tenant panel.
*/
declare(strict_types=1);
namespace App\Http\Middleware;
use App\Context\Tenant\Models\Tenant;
use Illuminate\Contracts\Auth\Factory as AuthFactory;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Session\Middleware\AuthenticatesSessions;
use Illuminate\Http\Request;
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain as BaseMiddleware;
use Closure;
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
use Stancl\Tenancy\Tenancy;
class InitializeTenancyBySubdomain extends BaseMiddleware implements AuthenticatesSessions
{
public function __construct(
protected Tenancy $tenancy,
protected DomainTenantResolver $resolver,
protected AuthFactory $auth
) {
parent::__construct($tenancy, $resolver);
}
public function handle(Request $request, Closure $next): mixed
{
$route = $request->route();
$record = $route->parameter('tenant');
if ($record && $tenant = Tenant::find($record)) {
if ($domain = $tenant->domain) {
$this->initializeTenancy($request, $next, $domain->domain);
}
}
return $next($request);
}
protected function guard(): AuthFactory|Guard
{
return $this->auth;
}
}
<?php
/**
* The below is an example Tenant panel provider created with the following command:
* php artisan make:filament-panel tenant
*
* The following code assumes you already have a central app scoped admin panel.
*/
declare(strict_types=1);
namespace App\Providers\Filament;
use App\Context\Tenant\Models\Tenant;
use App\Filament\Pages\Login;
use App\Http\Middleware\InitializeTenancyBySubdomain;
use Filament\Facades\Filament;
use Filament\Http\Middleware\Authenticate;
use Filament\Http\Middleware\DisableBladeIconComponents;
use Filament\Http\Middleware\DispatchServingFilamentEvent;
use Filament\Pages;
use Filament\Panel;
use Filament\PanelProvider;
use Filament\Support\Colors\Color;
use Filament\Widgets;
use Illuminate\Contracts\View\View;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Middleware\SubstituteBindings;
use Illuminate\Session\Middleware\AuthenticateSession;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\Support\Facades\Blade;
use Illuminate\View\Middleware\ShareErrorsFromSession;
class TenantPanelProvider extends PanelProvider
{
public function panel(Panel $panel): Panel
{
return $panel
->id('tenant')
->path('tenant')
->viteTheme([
'resources/css/tailwind.css',
'resources/js/app.js',
'resources/css/filament/admin/theme.css',
])
->login(Login::class)
->tenant(Tenant::class)
->discoverResources(in: app_path('Filament/Tenant/Resources'), for: 'App\\Filament\\Tenant\\Resources')
->discoverPages(in: app_path('Filament/Tenant/Pages'), for: 'App\\Filament\\Tenant\\Pages')
->discoverWidgets(in: app_path('Filament/Tenant/Widgets'), for: 'App\\Filament\\Tenant\\Widgets')
->pages([
Pages\Dashboard::class,
])
->renderHook('panels::user-menu.before', fn (): View => view('components.button.exit-tenancy'))
->widgets([
Widgets\AccountWidget::class,
Widgets\FilamentInfoWidget::class,
])
->middleware([
InitializeTenancyBySubdomain::class,
EncryptCookies::class,
AddQueuedCookiesToResponse::class,
StartSession::class,
AuthenticateSession::class,
ShareErrorsFromSession::class,
VerifyCsrfToken::class,
SubstituteBindings::class,
DisableBladeIconComponents::class,
DispatchServingFilamentEvent::class,
])
->authMiddleware([
Authenticate::class,
]);
}
}
<?php
/**
* Below is an example of the Filament tenant resource in the central app.
* When you click on a tenant row your app will redirect to the tenant panel scoped to the selected tenant.
*/
declare(strict_types=1);
namespace App\Filament\Central\Tenancy;
use Filament\Resources\Resource;
class TenantsResource extends Resource
{
public static function table(Table $table): Table
{
return $table
->recordUrl(
fn (Model $record): string => route('filament.tenant.pages.dashboard', ['tenant' => $record]),
)
// Rest of your table definition...
;
}
// Rest of your resource code...
}
<?php
declare(strict_types=1);
namespace App\Models;
class User extends Authenticatable implements Syncable, MustVerifyEmail, HasTenants
{
use ResourceSyncing;
// Your other model logic...
/**
* Add the `HasTenants` interface.
* Add the below methods required by the above trait.
*/
public function getTenants(Panel $panel): Collection
{
return $this->tenants;
}
public function canAccessTenant(Model $tenant): bool
{
return true;
}
}
@bianchi
Copy link

bianchi commented Apr 28, 2024

Hi

Type of App\Http\Middleware\InitializeTenancyBySubdomain::$tenancy must not be defined (as in class Stancl\Tenancy\Middleware\InitializeTenancyByDomain)

In InitializeTenancyBySubdomain replace the code with:

 <?php

/**
 * This middleware extends the Tenancy for Laravel one to catch
 * tenant ID and scope future requests in the tenant panel.
 */

declare(strict_types=1);

namespace App\Http\Middleware;

use App\Models\Tenant;
use Illuminate\Contracts\Auth\Factory as AuthFactory;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Session\Middleware\AuthenticatesSessions;
use Illuminate\Http\Request;
use Stancl\Tenancy\Middleware\InitializeTenancyBySubdomain as BaseMiddleware;
use Closure;
use Stancl\Tenancy\Resolvers\DomainTenantResolver;
use Stancl\Tenancy\Tenancy;

class InitializeTenancyBySubdomain extends BaseMiddleware implements AuthenticatesSessions
{
    protected $tenancy;
    protected $resolver;

    public function __construct(
        Tenancy               $tenancy,
        DomainTenantResolver  $resolver,
        protected AuthFactory $auth
    )
    {
         $this->tenancy = $tenancy;
        $this->resolver = $resolver;
        parent::__construct($tenancy, $resolver);
    }

    public function handle($request, Closure $next): mixed
    {
        $route = $request->route();
        $record = $route->parameter('tenant');

        if ($record && $tenant = Tenant::find($record)) {
            if ($domain = $tenant->domain) {
                $this->initializeTenancy($request, $next, $domain->domain);
            }
        }

        return $next($request);
    }

    protected function guard(): AuthFactory|Guard
    {
        return $this->auth;
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment