Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3.x] Laravel 11 support #1180

Merged
merged 12 commits into from
Mar 12, 2024
Merged

[3.x] Laravel 11 support #1180

merged 12 commits into from
Mar 12, 2024

Conversation

stancl
Copy link
Member

@stancl stancl commented Jan 27, 2024

This PR adds Laravel 11 support.

@stancl stancl marked this pull request as draft January 27, 2024 21:42
@stancl stancl marked this pull request as ready for review January 27, 2024 22:17
@stancl stancl marked this pull request as draft January 27, 2024 22:51
@stancl
Copy link
Member Author

stancl commented Jan 27, 2024

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.

@JustSteveKing
Copy link

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

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

@nunohelfrich
Copy link

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',
    )

@SamuelNitsche
Copy link

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

@randuran
Copy link

randuran commented Mar 3, 2024

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

@SamuelNitsche
Copy link

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

@stancl
Copy link
Member Author

stancl commented Mar 4, 2024

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 stancl marked this pull request as ready for review March 12, 2024 11:45
@stancl
Copy link
Member Author

stancl commented Mar 12, 2024

Blocked on laravel/framework#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 stancl merged commit 72b1b48 into 3.x Mar 12, 2024
3 checks passed
@stancl stancl deleted the laravel-11 branch March 12, 2024 14:04
@zeolimaldonado
Copy link

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

@stancl
Copy link
Member Author

stancl commented Mar 13, 2024

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
Copy link
Member Author

stancl commented Mar 13, 2024

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.

@zeolimaldonado
Copy link

zeolimaldonado commented Mar 13, 2024

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');
    });
});

@mfakhrys
Copy link

mfakhrys commented Mar 19, 2024

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'));
    }
)

@stancl
Copy link
Member Author

stancl commented Mar 21, 2024

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

@krekas
Copy link

krekas commented Mar 24, 2024

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

@stancl
Copy link
Member Author

stancl commented Mar 24, 2024

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.

@Clareapumami
Copy link

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'));
        }
    });
}

@barazeet

This comment was marked as off-topic.

@stancl
Copy link
Member Author

stancl commented Mar 28, 2024

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

@colinmackinlay
Copy link

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?

@Michael140020
Copy link

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

@stancl
Copy link
Member Author

stancl commented Apr 6, 2024

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

@yusufkaracaburun
Copy link

yusufkaracaburun commented Apr 7, 2024

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.

@stancl
Copy link
Member Author

stancl commented Apr 7, 2024

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

@yusufkaracaburun
Copy link

yusufkaracaburun commented Apr 8, 2024

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',
    )

@geoffreyrose
Copy link

geoffreyrose commented Apr 11, 2024

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

@LeoMachado94

This comment was marked as spam.

@stancl
Copy link
Member Author

stancl commented Apr 12, 2024

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
Copy link
Member Author

stancl commented Apr 13, 2024

Docs updated stancl/tenancy-docs@3f60cdb

@tiagofrancafernandes
Copy link

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

@ram2228
Copy link

ram2228 commented Aug 28, 2024

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'));
    }
)

@ram2228
Copy link

ram2228 commented Aug 28, 2024

It is working for me

@yogeshgalav
Copy link

Does this function in boot method works?
$this->makeTenancyMiddlewareHighestPriority();

Because I think we need to declare all these middleware in bootsrape/app.php
Please update recommended code in documentation.

@stancl
Copy link
Member Author

stancl commented Oct 21, 2024

The quickstart guide is up to date.

@archtechx archtechx locked as resolved and limited conversation to collaborators Oct 21, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.