Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

eggplantSword's avatar

Tenancy for Laravel with Spatie Permissions

I'm trying to create roles and permissions for Central and the Tenants, but when creating the tenant itself I get this error

There is no permission named surveys-read for guard admin.

It's correct but there shouldn't be one, that's the problem. The surveys-read permission is for the tenants using guard web and not for central users. the code fails on $tenant->save()

public function store(Request $request)
    {
        $validated = $request->validate([
            'name' => 'required|string',
            'description' => 'nullable|string',
            'domain' => 'required|alpha_dash|unique:domains,domain',
            'tenancy_db_name' => 'required|alpha_dash',
            'color_logo' => 'required',
            'white_logo' => 'required',
            'gps_token' => 'required'
        ]);

        $databaseNameExist = Tenant::all()
            ->where('tenancy_db_name', $validated['tenancy_db_name']);

        if ($databaseNameExist->count() > 0) {
            throw ValidationException::withMessages(['El nombre de la base de datos ya se encuentra registrado']);
        }

        $tenant = new Tenant();
        $tenant->fill($validated);
        $tenant->color_logo = request()->file('color_logo')->store('logos', 'public');
        $tenant->white_logo = request()->file('white_logo')->store('logos', 'public');
        $tenant->save();

        $domain = $validated['domain'] . '.' . $_SERVER['SERVER_NAME'];

        Domain::updateOrCreate(
            [ tenant_id' => $tenant->id ],            
			[  'domain' => $domain ]
        );

        return Redirect::route('home')->with('success', 'El Cliente ha sido creado.');
    }

They have this in their docs https://tenancyforlaravel.com/docs/v3/integrations/spatie/ But it doesn't work, I get a different error. This error covers the entire page and I can't do anything.

Object of class Closure could not be converted to string

It points to the method bootEvents in TenancyServiceProvider.php

<?php

declare(strict_types=1);

namespace App\Providers;

use App\Jobs\CreateFrameworkDirectoriesForTenant;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Spatie\Permission\PermissionRegistrar;
use Stancl\JobPipeline\JobPipeline;
use Stancl\Tenancy\Events;
use Stancl\Tenancy\Jobs;
use Stancl\Tenancy\Listeners;
use Stancl\Tenancy\Middleware;

class TenancyServiceProvider extends ServiceProvider
{
    // By default, no namespace is used to support the callable array syntax.
    public static string $controllerNamespace = '';

    public function events()
    {
        return [
            // Tenant events
            Events\CreatingTenant::class => [],
            Events\TenantCreated::class => [
                JobPipeline::make([
                    Jobs\CreateDatabase::class,
                    Jobs\MigrateDatabase::class,
                    Jobs\SeedDatabase::class,

                    // Your own jobs to prepare the tenant.
                    // Provision API keys, create S3 buckets, anything you want!
                    CreateFrameworkDirectoriesForTenant::class

                ])->send(function (Events\TenantCreated $event) {
                    return $event->tenant;
                })->shouldBeQueued(true), // `false` by default, but you probably want to make this `true` for production.
            ],
            Events\SavingTenant::class => [],
            Events\TenantSaved::class => [],
            Events\UpdatingTenant::class => [],
            Events\TenantUpdated::class => [],
            Events\DeletingTenant::class => [],
            Events\TenantDeleted::class => [
                JobPipeline::make([
                    Jobs\DeleteDatabase::class,
                ])->send(function (Events\TenantDeleted $event) {
                    return $event->tenant;
                })->shouldBeQueued(true), // `false` by default, but you probably want to make this `true` for production.
            ],

            // Domain events
            Events\CreatingDomain::class => [],
            Events\DomainCreated::class => [],
            Events\SavingDomain::class => [],
            Events\DomainSaved::class => [],
            Events\UpdatingDomain::class => [],
            Events\DomainUpdated::class => [],
            Events\DeletingDomain::class => [],
            Events\DomainDeleted::class => [],

            // Database events
            Events\DatabaseCreated::class => [],
            Events\DatabaseMigrated::class => [],
            Events\DatabaseSeeded::class => [],
            Events\DatabaseRolledBack::class => [],
            Events\DatabaseDeleted::class => [],

            // Tenancy events
            Events\InitializingTenancy::class => [],
            Events\TenancyInitialized::class => [
                Listeners\BootstrapTenancy::class,
            ],

            Events\EndingTenancy::class => [],
            Events\TenancyEnded::class => [
                Listeners\RevertToCentralContext::class,
                function (Events\TenancyEnded $event) {
                    \Spatie\Permission\PermissionRegistrar::$cacheKey = 'spatie.permission.cache';
                },
            ],

            Events\BootstrappingTenancy::class => [],
            Events\TenancyBootstrapped::class => [
                function (Events\TenancyBootstrapped $event) {
                    \Spatie\Permission\PermissionRegistrar::$cacheKey = 'spatie.permission.cache.tenant.' . $event->tenancy->tenant->id;
                }
            ],
            Events\RevertingToCentralContext::class => [],
            Events\RevertedToCentralContext::class => [],

            // Resource syncing
            Events\SyncedResourceSaved::class => [
                Listeners\UpdateSyncedResource::class,
            ],

            // Fired only when a synced resource is changed in a different DB than the origin DB (to avoid infinite loops)
            Events\SyncedResourceChangedInForeignDatabase::class => [],
        ];
    }

    public function register()
    {
        //
    }

    public function boot()
    {
        $this->bootEvents();
        $this->mapRoutes();

        $this->makeTenancyMiddlewareHighestPriority();
    }

    protected function bootEvents() //THIS METHOD
    {
        foreach ($this->events() as $event => $listeners) {
            foreach (array_unique($listeners) as $listener) { //THIS LINE
                if ($listener instanceof JobPipeline) {
                    $listener = $listener->toListener();
                }

                Event::listen($event, $listener);
            }
        }
    }

    protected function mapRoutes()
    {
        if (file_exists(base_path('routes/tenant.web.php'))) {
            Route::middleware('tenant')
                ->namespace(static::$controllerNamespace)
                ->group(base_path('routes/tenant.web.php'));
        }

        if (file_exists(base_path('routes/tenant.api.php'))) {
            Route::middleware('tenant')
                ->namespace(static::$controllerNamespace)
                ->group(base_path('routes/tenant.api.php'));
        }
    }

    protected function makeTenancyMiddlewareHighestPriority()
    {
        $tenancyMiddleware = [
            // Even higher priority than the initialization middleware
            Middleware\PreventAccessFromCentralDomains::class,

            Middleware\InitializeTenancyByDomain::class,
            Middleware\InitializeTenancyBySubdomain::class,
            Middleware\InitializeTenancyByDomainOrSubdomain::class,
            Middleware\InitializeTenancyByPath::class,
            Middleware\InitializeTenancyByRequestData::class,
        ];

        foreach (array_reverse($tenancyMiddleware) as $middleware) {
            $this->app[\Illuminate\Contracts\Http\Kernel::class]->prependToMiddlewarePriority($middleware);
        }
    }
}

What am I doing wrong?

0 likes
2 replies
franklin8619's avatar

Did you end up figuring this out? I'm seeing the same thing.

By removing the "array_unique" method here it stops the error, but I'm not sure how it might impact other things yet.

    protected function bootEvents()
    {
        foreach ($this->events() as $event => $listeners) {
            foreach ($listeners as $listener) {
                if (is_object($listener) && $listener instanceof JobPipeline) {
                    $listener = $listener->toListener();
                }

                Event::listen($event, $listener);
            }
        }
    }
eggplantSword's avatar

@franklin8619 I don't remember exactly what I did, but I did remove the array_unique and I think there was also something wrong with the guards when creating the permissions, be sure to check that the permissions you're creating have the correct guards.

Please or to participate in this conversation.