Route cloning
Route cloning is a feature for creating “tenant versions” of central routes. Typically that means:
/{tenant}path prefixtenant.name prefix'tenant'“flag” middleware
In other words, route cloning is typically used with path identification. This is not a requirement though as the feature is highly configurable — see the examples below.
Example use with path identification
Section titled “Example use with path identification”Assuming you’ve read the universal routes page, let’s try to reimplement universal routes, but in a way that supports path identification.
Imagine that you’re integrating with a third-party package that has standard routes without any
{tenant} parameters:
Route::group([ 'middleware' => config('posts_package.middleware'),], function () { Route::get('/posts', [PostController::class, 'index'])->name('package.posts.index'); Route::get('/posts/{post}', [PostController::class, 'show'])->name('package.posts.show');});In this example, we can change the middleware the package applies on its routes.
Let’s start by adding the path identification middleware and 'universal' since we want this
route to work in both central and tenant contexts:
[ InitializeTenancyByPath::class, 'universal',]Now we can run the cloning action:
protected function mapRoutes(){ $this->app->booted(function () { if (file_exists(base_path('routes/tenant.php'))) { RouteFacade::namespace(static::$controllerNamespace) ->middleware('tenant') ->group(base_path('routes/tenant.php')); }
$this->cloneRoutes(); });}
protected function cloneRoutes(){ /** @var CloneRoutesAsTenant $cloneRoutes */ $cloneRoutes = $this->app->make(CloneRoutesAsTenant::class);
$cloneRoutes->cloneRoutesWithMiddleware(['universal'])->handle();}Notice the highlighted segment. We’ve told the package to clone routes that have the universal
middleware.
This action will create a copy of all routes have the universal middleware flag, and “aren’t tenant”,
meaning they don’t have a {tenant} parameter and their name doesn’t start with tenant.. It will
apply the following changes:
- Prefix the route path with
/{tenant}/ - Prefix the route name with
tenant. - Apply the
tenantflag (to make tenancy initialization work if we’d be using early identification) and remove existing flags, likeuniversal
The example routes above would be cloned like this:
// Original routes — centralRoute::get('/posts', [PostController::class, 'index'])->name('package.posts.index');Route::get('/posts/{post}', [PostController::class, 'show'])->name('package.posts.show');
// Cloned routes — tenantRoute::get('/{tenant}/posts', [PostController::class, 'index'])->name('tenant.package.posts.index');Route::get('/{tenant}/posts/{post}', [PostController::class, 'show'])->name('tenant.package.posts.show');The original routes will be accessible in the central app (even if they have the identification
middleware) because they have the universal flag. They will not be accessible in the tenant app since there’s no {tenant} parameter that could be specified.
The cloned routes will only be accessible in the tenant app (since they have the {tenant}
parameter) and tenancy will be initialized on them.
The route names are prefixed with tenant. to avoid collisions while still letting you use named routes.
In these setups, you’d also often use the UrlGenerator
bootstrapper which can automatically convert route('package.posts.index') calls to
route('tenant.package.posts.index', ['tenant' => tenant()->getTenantKey()]) calls when you’re in
the tenant context.
Specifying tenant middleware manually
Section titled “Specifying tenant middleware manually”Following the previous example, let’s refine it a bit: instead of making the central route universal — we’ve done that just to have a way to pass the tenant identification middleware to the cloned route — we can pass the tenant middleware manually.
We will set this middleware on the group:
[ InitializeTenancyByPath::class, 'universal', 'clone',]And clone routes like this:
protected function cloneRoutes(){ // ...
$cloneRoutes = $this->app->make(CloneRoutesAsTenant::class); $cloneRoutes->cloneRoutesWithMiddleware(['universal'])->handle(); $cloneRoutes->addTenantMiddleware([InitializeTenancyByPath::class])->handle();}cloneRoutesWithMiddleware() defaults to ['clone'] which is an empty middleware group, just
like the central, tenant, and universal flags, except it has no meaning outside of the
cloning context — it’s just used to mark a certain route/route group as clonable by the action.
With this change, the original route will just have an unused clone middleware group instead of
being unnecessarily marked as universal, and the cloned tenant route will still have the desired
tenant identification middleware.
Configuration
Section titled “Configuration”The action is extensively documented by docblocks, so all configuration options will not be covered here, but here are some examples of how the action can be used:
shouldClone(fn (Route): bool)to provide a custom callback for determining which routes should be clonedcloneRoutesWithMiddleware([...])demonstrated above, defaults to['clone']. The default “should clone” logic looks at thiscloneUsing(fn (Route|string): void)to replace the default cloning logic with your own implementationaddTenantParameter()whether the cloned route should be prefixed with{tenant}domain()domain scope to be applied on the cloned route, relevant with multi-domain tenancycloneRoute()/cloneRoutes()to clone individual routes
Example use with domain identification
Section titled “Example use with domain identification”You could use route cloning with domain identification in a scenario where you’re using a third-party library for authentication and you want to register the same routes in both contexts.
If the routes were simply required, as in:
require __DIR__.'/auth.php';You could simply place that statement in both routes/web.php and routes/tenant.php. If they
are registered by the package itself though, cloning can be helpful.
You could also use universal routes, and in many apps, that does work very well for auth routes.
The main reason for wanting identical-yet-separate auth routes in the two contexts is that you may
want to be able to reference e.g. route('auth.login') as the central route from any context —
such that the route() call references a domain-bound
route.
For custom setups like this, since they vary a lot, you’re encouraged to take a look at the
CloneRoutesAsTenant class for usage information. Make sure to read the paragraph about domain()
scopes in the class docblock.