Skip to content

Instantly share code, notes, and snippets.

@nathandaly
Last active April 28, 2024 23:37
Show Gist options
  • Star 21 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • 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;
}
}
@madushancs
Copy link

Hello nathandaly, Are there any your public repository for the Laravel, FilamentPHP & stancl/tenancy all together. Please kindly share link with me. I really appreciate it. Thank you

@coolsam726
Copy link

Hello nathandaly, Are there any your public repository for the Laravel, FilamentPHP & stancl/tenancy all together. Please kindly share link with me. I really appreciate it. Thank you

I hope this will help:
https://github.com/savannabits/filament-tenancy-starter

@madushancs
Copy link

Hello nathandaly, Are there any your public repository for the Laravel, FilamentPHP & stancl/tenancy all together. Please kindly share link with me. I really appreciate it. Thank you

I hope this will help: https://github.com/savannabits/filament-tenancy-starter

Thank you very much

@lamberto15
Copy link

Hello nathandaly, Are there any your public repository for the Laravel, FilamentPHP & stancl/tenancy all together. Please kindly share link with me. I really appreciate it. Thank you

I hope this will help: https://github.com/savannabits/filament-tenancy-starter

Hi Coolsam did you try it also with multi databases? im using multi db, using it with central domain it works properly, but when i try to add InitializeTenancyBySubdomain in the tenantPanel middleware, i got error Tenant could not be identified on domain test.

@mstrihi
Copy link

mstrihi commented Jan 28, 2024

Hi @nathandaly I see the video you share in filament community until now I just try to do same think for my next project but until now I don't get that works fine. if you can share only the starter for that like login tenant / exit, add new tenant will be awesome.
I already tried this starter as well https://github.com/savannabits/filament-tenancy-starter but not working very well and missing a lot of logics

@citricguy
Copy link

citricguy commented Feb 11, 2024

Does artisan route:cache work for you with this setup? I'm having trouble with route names being unique from central to tenants I think.

Unable to prepare route [admin/login] for serialization. Another route has already been assigned name [filament.admin.auth.login]

Edit: Ended up being caused by having multiple 'central_domains' set in my tenancy.php.

Thank you for putting this repo together, it was a huge help getting things moving!

@Waguilar33
Copy link

@mstrihi mind sharing the link with @nathandaly video... cannot find it in filament community :(

@batuhanhazarguler
Copy link

Hi

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

@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