tenancy icon indicating copy to clipboard operation
tenancy copied to clipboard

[3.x] Laravel 11 support

Open stancl opened this issue 2 years ago • 4 comments

TODO:

  • [x] Address installation given the changes to the Laravel app skeleton

stancl avatar Jan 27 '24 21:01 stancl

Tried the installation with the new Laravel 11 app skeleton and seems like everything works fine. The only different step is that you need to create an app.php config file and configure the service providers like this:

use Illuminate\Support\ServiceProvider;

return [
    'providers' => ServiceProvider::defaultProviders()->merge([
        App\Providers\TenancyServiceProvider::class,
    ])->toArray(),
];

Will keep this PR open until Laravel 11 release. To test Tenancy 3.x with Laravel 11, you can use:

composer require stancl/tenancy:dev-laravel-11

If you run into any issues, let me know in this thread.

stancl avatar Jan 27 '24 23:01 stancl

You can add this to the bootstrap/app.php file now as part of the App Builder

->withProviders([
  // Add providers here....
])

JustSteveKing avatar Jan 28 '24 10:01 JustSteveKing

What about RouteServiceProvider? Cant acces my tenant's routes, it keeps showing me my central routes.. I recreated Route ServiceProvider from Laravel 10.x and added the changes in the docs but i think the problem is here:

->withRouting(
        web: __DIR__.'/../routes/web.php',
        // api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        // channels: __DIR__.'/../routes/channels.php',
        health: '/up',
    )

nunohelfrich avatar Feb 01 '24 02:02 nunohelfrich

What about RouteServiceProvider? Cant acces my tenant's routes, it keeps showing me my central routes.. I recreated Route ServiceProvider from Laravel 10.x and added the changes in the docs but i think the problem is here:

->withRouting(
        web: __DIR__.'/../routes/web.php',
        // api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        // channels: __DIR__.'/../routes/channels.php',
        health: '/up',
    )

Did you apply the InitializeTenancyByDomain middleware to the routes? See: https://tenancyforlaravel.com/docs/v3/routes/#tenant-routes

SamuelNitsche avatar Feb 11 '24 13:02 SamuelNitsche

What about RouteServiceProvider? Cant acces my tenant's routes, it keeps showing me my central routes.. I recreated Route ServiceProvider from Laravel 10.x and added the changes in the docs but i think the problem is here:

->withRouting(
        web: __DIR__.'/../routes/web.php',
        // api: __DIR__.'/../routes/api.php',
        commands: __DIR__.'/../routes/console.php',
        // channels: __DIR__.'/../routes/channels.php',
        health: '/up',
    )

Did you apply the InitializeTenancyByDomain middleware to the routes? See: https://tenancyforlaravel.com/docs/v3/routes/#tenant-routes

Any progress on this? Laravel 11 does not include a RouteServiceProvider by default

randuran avatar Mar 03 '24 19:03 randuran

@randuran See the link I posted above. You can apply the InitializeTenancyByDomain middleware on a route group in your routes/web.php file.

SamuelNitsche avatar Mar 04 '24 08:03 SamuelNitsche

Think by default the route SP was used to change central routes — scope them to the central domain.

The tenancy middleware is best to use directly in your tenant routes, yeah.

And without a route SP, you can just scope your central routes to the central domain directly in web.php/api.php. The provider is not needed, it can just simplify your route files.

stancl avatar Mar 04 '24 08:03 stancl

~~Blocked on https://github.com/laravel/framework/issues/50467~~. The change only affects tests, package behavior should be unaffected so this branch is safe to use (composer require stancl/tenancy:dev-laravel-11).

Once we can get the tests to pass I'll merge this into 3.x and tag a release.

stancl avatar Mar 12 '24 12:03 stancl

Think by default the route SP was used to change central routes — scope them to the central domain.

The tenancy middleware is best to use directly in your tenant routes, yeah.

And without a route SP, you can just scope your central routes to the central domain directly in web.php/api.php. The provider is not needed, it can just simplify your route files.

It doesn't work at all, i can access to my tenant routes but i can't access to my central routes with the same URI

zeolimaldonado avatar Mar 13 '24 16:03 zeolimaldonado

Hmm, this works:

// config/app.php
return [
    'providers' => ServiceProvider::defaultProviders()->merge([
        App\Providers\TenancyServiceProvider::class,
    ])->toArray(),
];
// routes/web.php
Route::domain('l11-test.test')->get('/', function () {
    return view('welcome');
});
// routes/tenant.php
Route::middleware([
    'web',
    InitializeTenancyBySubdomain::class,
    PreventAccessFromCentralDomains::class,
])->group(function () {
    Route::get('/abc', function () {
        return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
    });
});

But when you change /abc to / the central / doesn't seem to work, even though there should be no conflict when domain() is used.

My guess would be the tenant routes get registered after the central routes, which is why it results in a 404 from the PreventAccessFromCentralDomains.

So the Tenancy SP needs to be registered in some way so that it executes after the default route SP.

stancl avatar Mar 13 '24 16:03 stancl

Changing TSP mapRoutes() to:

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

Seems to work, though I'd like to try a few different solutions before concluding if this is the way to go.

stancl avatar Mar 13 '24 16:03 stancl

I did the changes on TSP and it works correctly on routes with web but when i tried to do the same on 'api' it fails

routes/tenant.php

Route::middleware([
    'api',
    InitializeTenancyBySubdomain::class,
    PreventAccessFromCentralDomains::class,
])->group(function () {
    Route::get('/', function () {
        return 'This is your multi-tenant application. The id of the current tenant is ' . tenant('id');
    });
});

routes/api.php

Route::domain('test-app.test')->get('/', function () { return 'welcome'; });

UPDATE: i move the tenant routes to routes/api.php and it seems to work, tenant needs to be declared after the central routes to work

Route::domain('test-app.test')->get('/', function () {
    return 'welcome from api';
});

Route::middleware([
    InitializeTenancyBySubdomain::class,
    PreventAccessFromCentralDomains::class,
])->group(function () {
    Route::get('/', function () {
        return 'This is the tenant from api ' . tenant('id');
    });
});

zeolimaldonado avatar Mar 13 '24 16:03 zeolimaldonado

Just sharing my setup. It works in my project.

Firstly, I add TSP in bootstrap/providers.php file

<?php

return [
    App\Providers\AppServiceProvider::class,
    App\Providers\TenancyServiceProvider::class,
];

Secondly, In TSP, I comment out mapRoutes

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

    $this->makeTenancyMiddlewareHighestPriority();
}

And finally, in bootstrap/app.php file, following the Laravel 11 documentation, i modify withRouting function into:

->withRouting(
    // web: __DIR__.'/../routes/web.php',
    commands: __DIR__ . '/../routes/console.php',
    health: '/up',
    using: function () {
        $centralDomains = config('tenancy.central_domains');

        foreach ($centralDomains as $domain) {
            Route::middleware('web')
                ->domain($domain)
                ->group(base_path('routes/web.php'));
        }

        Route::middleware('web')->group(base_path('routes/tenant.php'));
    }
)

mfakhrys avatar Mar 19 '24 07:03 mfakhrys

The using approach does seem to work, though I believe it overrides the web/api/health/broadcasting/... options for withRouting().

stancl avatar Mar 21 '24 11:03 stancl

Instead of using should use then to add additional route files. https://laravel.com/docs/11.x/routing#routing-customization

krekas avatar Mar 24 '24 18:03 krekas

This isn't about registering an additional file, that's already possible in the TenancyServiceProvider. We're looking for ways to scope the central routes to central domains in a nice way.

stancl avatar Mar 24 '24 18:03 stancl

Using both @mfakhrys and @stancl codes... Firstly, I add TSP in bootstrap/providers.php file

<?php

return [
    App\Providers\AppServiceProvider::class,
    App\Providers\TenancyServiceProvider::class,
];

Secondly, in bootstrap/app.php file, following the Laravel 11 documentation, I modify withRouting function into:

 ->withRouting(
        // web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
        using: function () {
            $centralDomains = config('tenancy.central_domains');

            foreach ($centralDomains as $domain) {
                Route::middleware('web')
                    ->domain($domain)
                    ->group(base_path('routes/web.php'));
            }
        }
 )

This overrides the default web routing, but this is exactly what we want to achieve. Api routes will be defined in the same way, but I think an api.php file will have to be included in the routes. I'm not using that.

Finally, in TSP, we change mapRoutes() into

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

Clareapumami avatar Mar 28 '24 12:03 Clareapumami

I'm using passport for an api-only laravel application and I need to use oauth2 both at the tenant and central app (it integrates with a mobile app and a web app at the same time)

I'm stuck at defining the universal middleware in bootstrap/app.php because in the feature code of UniversalRoutes the only identifiers present are domain and subdomain identifiers. I'm using RequestData. If this isn't the place to ask, apologies. I'm a bit desperate for some guidance.

barazeet avatar Mar 28 '24 20:03 barazeet

Keep this thread Laravel 11-related, support questions go on our Discord.

stancl avatar Mar 28 '24 20:03 stancl

L11 saas-boilerplate issue I'm wrestling with is the relocation of exception handling from app/Exceptions/Handler.php to bootstrap\app.php

My version of Handler.php has this:

class Handler extends ExceptionHandler
{
    protected $dontReport = [
        TenantCouldNotBeIdentifiedOnDomainException::class,
        NotASubdomainException::class,
    ];

    protected $dontFlash = [
        'current_password',
        'password',
        'password_confirmation',
    ];

    public function render($request, Throwable $e): Response
    {
        if (
            $e instanceof TenantDatabaseDoesNotExistException ||
            (tenant()
                && ! tenant('ready')
                && $e instanceof QueryException)
            ||
            (tenant()
                && ! tenant('ready')
                && $e instanceof ViewException
                && $e->getPrevious() instanceof QueryException)
        ) {
            return response()->view('errors.building');
        }

        if ($e instanceof TenantCouldNotBeIdentifiedException || $e instanceof NotASubdomainException) {
            return to_route('central.landing');
        }

        return parent::render($request, $e);
    }
}

I know it's not quite the latest version but the issues of converting it over to L11 bootstrap/app.php seem to be the same.

Anyone had success with converting a saas-boilerplae based application to L11?

colinmackinlay avatar Mar 29 '24 11:03 colinmackinlay

This is on purpose, so that you buy the saas boilerplate!

Michael140020 avatar Apr 06 '24 15:04 Michael140020

The boilerplate doesn't use L11. It will be updated after we have the time to go over all the project structure changes.

stancl avatar Apr 06 '24 15:04 stancl

how did you guys setup your central api routes? in my web.php I have: Route::get('/{any}', [AppController::class, 'central'])->where('any', '.*');

and my routing in bootstrap/app.php is like:

->withRouting(
//                web: __DIR__ . '/../routes/web.php',
        api: __DIR__ . '/../routes/api.php',
        commands: __DIR__ . '/../routes/console.php',
        channels: __DIR__ . '/../routes/channels.php',
        health: '/up',
        then: function (): void {
            $centralDomains = config('tenancy.central_domains');

            foreach ($centralDomains as $domain) {
                Route::middleware('web')
                    ->domain($domain)
                    ->group(base_path('routes/web.php'));
            }
        }
    )

so when i e.g. send a request to /api/users, it uses the web.php route instead of the api.php route.

yusufkaracaburun avatar Apr 07 '24 09:04 yusufkaracaburun

If you use top-level .* routes, you need to be careful about the route registration order.

stancl avatar Apr 07 '24 14:04 stancl

No, I want to use it like I use web routes. Is this the correct way, because it works?

    ->withRouting(
        //                web: __DIR__ . '/../routes/web.php',
        //        api: __DIR__ . '/../routes/api.php',
        using: function (): void {
            $centralDomains = config('tenancy.central_domains');

            foreach ($centralDomains as $domain) {
                Route::prefix('api')
                    ->domain($domain)
                    ->middleware('api')
                    ->group(base_path('routes/api.php'));
            }

            foreach ($centralDomains as $domain) {
                Route::middleware('web')
                    ->domain($domain)
                    ->group(base_path('routes/web.php'));
            }

            //            Route::middleware('web')->group(base_path('routes/tenant.php'));
        },
        commands: __DIR__ . '/../routes/console.php',
        channels: __DIR__ . '/../routes/channels.php',
        health: '/up',
    )

yusufkaracaburun avatar Apr 08 '24 11:04 yusufkaracaburun

Here is what I got to work. Initially tested it in a fresh Laravel 11 install and then the actual project I have. Both seem to work as intended.

First register two additional service providers

// bootstrap/providers.php

<?php

return [
    App\Providers\AppServiceProvider::class,
    App\Providers\RouteServiceProvider::class,
    App\Providers\TenancyServiceProvider::class,
];

Next remove the web routes from being registered in bootstrap/providers.php

// bootstrap/providers.php

<?php

use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        // web: __DIR__.'/../routes/web.php',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        //
    })
    ->withExceptions(function (Exceptions $exceptions) {
        //
    })->create();

Register the web routes, but only for central domains

// app/Providers/RouteServiceProvider.php

<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Route;

class RouteServiceProvider extends ServiceProvider
{
    /**
     * The path to the "home" route for your application.
     *
     * Typically, users are redirected here after authentication.
     *
     * @var string
     */
    public const HOME = '/';

    /**
     * Define your route model bindings, pattern filters, and other route configuration.
     */
    public function boot(): void
    {
        $this->routes(function () {
            $this->mapWebRoutes();
        });
    }

    protected function mapWebRoutes()
    {
        foreach ($this->centralDomains() as $domain) {
            Route::middleware('web')
                ->domain($domain)
                ->namespace($this->namespace)
                ->group(base_path('routes/web.php'));
        }
    }

    protected function centralDomains(): array
    {
        return config('tenancy.central_domains', []);
    }
}

// Using the default TenancyServiceProvider, no changes needed there

What seems to do the trick is not registering web in providers.php and do that instead inside of a RouteServiceProvider which will register routes only on the central domains

geoffreyrose avatar Apr 11 '24 06:04 geoffreyrose

@stancl @geoffreyrose @nunoh123 @SamuelNitsche @randuran @JustSteveKing I've done a fresh install now and the central routes still seem to overwrite the tenant routes, has this been resolved? Why am I still experiencing the issues discussed in the comments?

LeoMachado94 avatar Apr 11 '24 23:04 LeoMachado94

Don't spam ping when asking questions. The approaches outlined above work: you just have to make sure your tenant routes get registered after central routes, either in using or by deferring the tenant route registration as I mentioned above.

I'll update the v3 docs soon to rewrite the installation guide.

stancl avatar Apr 12 '24 00:04 stancl

Docs updated https://github.com/stancl/tenancy-docs/commit/3f60cdbe58057986b23ffcda4273014791ebe1d5

stancl avatar Apr 13 '24 22:04 stancl

The using approach does seem to work, though I believe it overrides the web/api/health/broadcasting/... options for withRouting().

It's true.

I believe that using 'then' is more productive.

This way keep 'up/health' and others

tiagofrancafernandes avatar May 28 '24 14:05 tiagofrancafernandes