[3.x] Laravel 11 support
TODO:
- [x] Address installation given the changes to the Laravel app skeleton
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.
You can add this to the bootstrap/app.php file now as part of the App Builder
->withProviders([
// Add providers here....
])
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',
)
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
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
InitializeTenancyByDomainmiddleware 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 See the link I posted above. You can apply the InitializeTenancyByDomain middleware on a route group in your routes/web.php file.
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.
~~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.
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
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.
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.
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');
});
});
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'));
}
)
The using approach does seem to work, though I believe it overrides the web/api/health/broadcasting/... options for withRouting().
Instead of using should use then to add additional route files.
https://laravel.com/docs/11.x/routing#routing-customization
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.
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'));
}
});
}
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.
Keep this thread Laravel 11-related, support questions go on our Discord.
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?
This is on purpose, so that you buy the saas boilerplate!
The boilerplate doesn't use L11. It will be updated after we have the time to go over all the project structure changes.
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.
If you use top-level .* routes, you need to be careful about the route registration order.
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',
)
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
@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?
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.
Docs updated https://github.com/stancl/tenancy-docs/commit/3f60cdbe58057986b23ffcda4273014791ebe1d5
The
usingapproach does seem to work, though I believe it overrides theweb/api/health/broadcasting/... options forwithRouting().
It's true.
I believe that using 'then' is more productive.
This way keep 'up/health' and others