Multi Tenant Mode by URL Param

Hello dear community,

i have a working application, written in cakephp 4.4 and would like to implement a multi-tenant capability.
For this I have a database-table with the tenants, which always have a short name.
With a setting in the configuration I would switch on this multi-tenant mode.

The short name from the tenant (e.g. company1) is specified via the URL: https://domain.de/company1/controller/action/

The tenant, which is then used in the database queries, should be fetched from the URL, possibly via the routing settings.

In addition, I also have an API in the normal single tenant mode, accessible at https://domain.de/api/.
It is configured in the routes.php by using the prefix method.
The API should also be accessed here via /company1/api/users/…

How can I best implement this?

Best regards

Marc

This feels to me like a custom middleware which sets different attributes depending on what request is currently being processed.

See e.g. the AuthenticationMiddleware

The request object is then being passed down to the controller, which then can do its logic dependent on the information present in the request.

So you load the tenant entity (if available) in the custom middleware, set it as an attribute in the request and your controller actions can check if that custom request attribute is present and do their related logic dependent on that.

Its at least one possible way to implement that :grin:

Thanks for the quick reply.
And in this middleware do I define my own route again?

Danke dir :wink:

Which routes should be available is configured in your config/routes.php or in one of your plugins

public function routes(RouteBuilder $routes): void

method as you can see here

Your middleware shouldn’t be meddling with that part imho.

I now have these lines of code:

in routes.php something like that:

$routes->setRouteClass(DashedRoute::class);
    
if(Configure::read('Config.multi_tenant_mode')) {
    $scope = '/{tenant}';
} else {
    $scope = '/';
}

$routes->scope($scope, function (RouteBuilder $builder) {
        
      $builder->connect('/', ['controller' => 'topics', 'action' => 'index']);

      $builder->prefix('Api', function (RouteBuilder $routes) {
      
                  $routes->setExtensions(['json', 'xml']);
                  $routes->resources('topics');

And a CustomHtml Helper, because the links are generated without a tenant by default:

class CustomHtmlHelper extends HtmlHelper
{
    public function link($title, $url = null, array $options = []): string
    {
        // Prüfe, ob der Mandant in den URL-Parametern vorhanden ist
        $tenant = $this->getView()->getRequest()->getParam('tenant');

        if (!empty($tenant)) {
            if(is_array($url)) {
                // Füge den "mandant" als named Parameter hinzu
                $url['tenant'] = $tenant;
            } else {
                // Füge den Mandant der URL bei einem String als Präfix an.
                $url = $tenant . '/' . $url;
            }
        }

        return parent::link($title, $url, $options);
    }
}

In my request, I now get the “tenant” parameter, which I can use in the controller.

I get now several problems, e.g. in the login-method from the Authentication Plugin:

“Login URL /test/users/login did not match /users/login.”

I found a solution, but now the next problem :smiley:

In Application::bootstrap():

Router::addUrlFilter(function (array $params, ServerRequest $request) {
    if ($request->getParam('tenant') && !isset($params['tenant'])) {
        $params['tenant'] = $request->getParam('tenant');
    }
    return $params;
});

and in the authentication service, replace the url string with :


'loginUrl' => Router::url([
    'controller' => 'users',
    'action' => 'login',
])

but now the debug-console is not working and throws an error:

2023-09-06 14:49:37 error: [Cake\Routing\Exception\MissingRouteException] A route matching "array (
  'controller' => 'users',
  'action' => 'login',
  'plugin' => 'DebugKit',
  '_ext' => NULL,
)" could not be found. in /var/www/html/vendor/cakephp/cakephp/src/Routing/RouteCollection.php on line 326
Exception Attributes: array (
  'url' => 'array (
  \'controller\' => \'users\',
  \'action\' => \'login\',
  \'plugin\' => \'DebugKit\',
  \'_ext\' => NULL,
)',
  'context' => 
  array (
    '_scheme' => 'http',
    '_host' => 'localhost',
    '_port' => NULL,
    '_base' => '',
    'params' => 
    array (
      'pass' => 
      array (
        0 => 'a35f784d-04a4-446c-931c-9bc537aa38e7',
      ),
      'controller' => 'Requests',
      'action' => 'view',
      'plugin' => 'DebugKit',
      '_matchedRoute' => '/debug-kit/toolbar/*',
      '_ext' => NULL,
    ),
  ),
)
Stack Trace:
- /var/www/html/vendor/cakephp/cakephp/src/Routing/Router.php:497
- /var/www/html/src/Application.php:162
.......

Request URL: /debug-kit/toolbar/a35f784d-04a4-446c-931c-9bc537aa38e7
Referer URL: http://localhost/test/users/login
Client IP: 172.24.0.1

Please can anybody help…? Is it the right way to implement a multi-tenancy? :upside_down_face: :upside_down_face:

Controller names should be upper-cased, like Users, not users. Not sure if that’ll resolve this, but maybe…

You are the app developer and you have specific requirements for your multi-tenancy application.

The framework offers multiple different ways how you can approach your problems. There is no “single one way” to do things because 5 different approaches each have their own benefits and drawbacks.

We can’t tell you if its “the right way” because we are (and of course should not) be responsible for your app.

That’s right, I know.

I really want and have to develop it exactly as described, maybe the tenant set by a subdomain, e.g. https://tenant.domain.de. The fact is, the URL must define which tenant-context I am in.

Apparently CakePHP does not provide this by default, so I’m asking if there is another solution or if I’m on the right track here and I just have to deal with the many sub-problems.

i find that in cakephp some things break the application and then it will result in cakephp throwing the missing route exception

for instance the eventmanager if you have implementedevents and write:

model.afterCreate=> changeArticle

but then you write the method

adjustArticle

it will throw a missing route exception as well…

maybe it should throw a missing method exception instead…

<?php
namespace Myplugin\Event;

use Cake\Event\EventListenerInterface;
use Cake\ORM\TableRegistry;
use Cake\Core\Configure;
use Cake\Log\Log;

class ArticleListener implements EventListenerInterface
{
    
    public function implementedEvents() : array
    {
        return ['Model.Articles.afterCreate' => 'changeArticle'];
    }
    
    public function adjustArticle($event, $article)
    {
       ......
    }
}

This results in a missing route exception when you have this in config bootstrap in plugin

/* Register our Listeners */
use Cake\Event\EventManager;

use Myplugin\Event\ArticleListener;
$article = new ArticleListener();
EventManager::instance()->on($article);

Maybe not related to initial post but its also throwing the missing route exception when the error really is something totally different.