archtechx / tenancy

Automatic multi-tenancy for Laravel. No code changes needed.
https://tenancyforlaravel.com
MIT License
3.62k stars 427 forks source link

InitializeTenancyByPath with livewire within tenant context throws exception #1212

Closed nickfls closed 5 months ago

nickfls commented 6 months ago

Bug description

Laravel throws Undefined array key 0 received when using InitializeTenancyByPath with livewire within tenant context.

Steps to reproduce

  1. tenancy identifies tenant by path:
    Route::subDomain('http://tenant.localhost.test')
    ->as('tenant.')
    ->middleware([
        'auth:tenant',
        PreventAccessFromCentralDomains::class,
        InitializeTenancyByPath::class,
    ])
    ->prefix('/{tenant}')
    ->group(function () {
        Route::get('/', TenantHomeController::class);
  2. Update laravel middleware in LivewireServiceProvider::boot()
    Livewire::setUpdateRoute(function ($handle) {
            return Route::post('/livewire/update', $handle)
                ->middleware([
                    'web',
                    'universal',
                   InitializeTenancyByPath::class
                ]);
        });
  3. Place a livewire component in tenant.localhost.test/{tenant}/
  4. trigger a livewire roundtrip (i.e wire:click a button)

Expected behavior

Undefined array key 0 in Middleware/InitializeTenancyByPath.php:40

Stack trace

Laravel version

11.4.0

stancl/tenancy version

3.8.2

stancl commented 6 months ago

The Undefined array key 0 comes from:

$route->parameterNames()[0]

the route itself has no route parameters — you have only applied middleware. You could try changing the route to /{tenant}/livewire/update or something along those lines.

nickfls commented 6 months ago

Would it be fair to say that InitializeTenancyByPath is not entirely compatible with livewire and one need to search for an alternative? .. when liveiwre is to be used both in central and in tenant apps?

stancl commented 6 months ago

Path identification is in general more difficult to get working with various third-party packages than e.g. domain identification because of this exact issue: the need for package routes to contain the {tenant} parameter, which isn't always something you can set.

In the case of Livewire, you can set it so I think you should be able to make this work.

But to use Livewire in both the central app and the tenant app you'd need a bit more logic to dynamically change the update route (so that it does have the parameter in the tenant app and doesn't have it in the central app).

Something like this might work:

class LivewireBootstrapper implements TenancyBootstrapper
{
    public function bootstrap(Tenant $tenant)
    {
        Livewire::setUpdateRoute(function ($handle) {
            return Route::post('/{tenant}/livewire/update', $handle)->middleware([
                'web',
                InitializeTenancyByPath::class
            ]);
        });
    }

    public function revert()
    {
        Livewire::setUpdateRoute(function ($handle) {
            return Route::post('/livewire/update', $handle)->middleware(['web']);
        });
    }
}
nickfls commented 6 months ago

@stancl I appreciate your time.

i think the Bootstrappers are called when the tenancy is initialized - that may be ok for the initial run (it isn't - for whatever reason the update route ONLY sets when defined in routes or within AppServiceProvider::boot() ), but routes are loaded before that moment and since tenancy is not defined yet, we are at the same spot.

livewire 3 docs say they will respect and keep the prefix on reload; however, i cannot fathom the spot where i need to run the Livewire::updateRoute(...)

stancl commented 6 months ago

Right that makes sense. We're adding the identification middleware to these routes so setting them inside bootstrappers (that run after identification) won't work. I remembered we had another solution for path identification, I'll find it and mention it here.

nickfls commented 5 months ago

Any luck so far, @stancl?

stancl commented 5 months ago

Ah sorry, meant to post here but didn't get to it.

Essentially, the setup we use in v4 is more dependent on some features added in v4, but can be replicated with a bit of work:

This will lead LW creating a path to either the central route or the tenant route based on the current context on the initial render, and then subsequent requests will respect that URL.

abikali commented 5 months ago

I'm currently facing the exact same problem. I tried doing something like this:

Livewire::setUpdateRoute(function ($handle) {
            $tenantIdentifier = request()->segment(1);
            return Route::post("/{tenant}/livewire/update", $handle)
                ->middleware(
                    [
                        'web',
                        'universal',
                        InitializeTenancyByPath::class,
                    ])
                ->defaults('tenant', $tenantIdentifier)
                ->name('livewire.update');
        });

Unfortunately it didn't work due to a Missing required parameter error. If anyone was able to make this work using v3 I would really appreciate it.

JulitoM3 commented 2 months ago

@abikali @stancl @nickfls Im facing this problem and this is what i did:

First i added the livewire update route to the TenancyServiceProvider

Livewire::setUpdateRoute(function ($handle) {
            //\Livewire\Mechanisms\HandleRequests\HandleRequests::class
            return Route::post('/livewire/update', $handle)
                ->middleware(
                    'web',
                    'universal',
                    \Stancl\Tenancy\Middleware\InitializeTenancyByPath::class, // or whatever tenancy middleware you use
                );
        });
        $this->makeTenancyMiddlewareHighestPriority();

Then i had to modify (or maybe create a new MW) InitializeTenancyByPath

public function handle(Request $request, Closure $next)
    {
        /** @var Route $route */

  /**
   *  We need to check if the request is a livewire request 
   *  because livewire requests are missing 'tenant' value in parameterNames array 
   *  and we need it to initialize the tenant
  **/
      if(Livewire::isLivewireRequest()) {

            $tenant = explode("/", $request->server('HTTP_REFERER'))[3];
            $tenant = YourTenantModel::find($tenant);
            \tenancy()->initialize($tenant);
            return $next($request);

        }

        $route = $request->route();

        // Only initialize tenancy if tenant is the first parameter
        // We don't want to initialize tenancy if the tenant is
        // simply injected into some route controller action.
        if ($route->parameterNames()[0] === PathTenantResolver::$tenantParameterName) {
         return  $this->initializeTenancy(
                $request, $next, $route
            );
        } else {

            throw new RouteIsMissingTenantParameterException;
        }

        return $next($request);

And thats all, livewire its working on central and tenant routes.