Application multi-tenant sur Laravel

multi-tenant-laravel

Multi-tenant c’est quoi ?

On pourrait traduire « tenant » par « titulaire » en français. Ca permet donc de gérer en un seul point d’entrée, les données de plusieurs clients, qui doivent avoir accès uniquement aux données dont ils sont les propriétaires. Pour faire simple, prenons un exemple :

Vous avez une table addresses, dans laquelle tous les champs d’une adresse sont disponibles. Chaque adresse appartient  à un utilisateur (via un champ user_id) : le client final. Mais dans notre cas, on va ajouter un champ tenant_id, pour que votre client à vous, ait la possibilité d’administrer cette adresse.

Votre application a un back-office unique pour tout le monde, mais peut avoir plusieurs domaines (un par client) pour la vitrine de vos clients à vous.

Définir le contexte

On va partir du principe que vous avez déjà créé le fichier de migration, et le modèle App\Tenant, qui contient sa clé primaire, un champ domain, ainsi qu’un champ name. Ca sera plus facile de les utiliser à terme.

Partant de là, il faut créer une classe Context :

<?php

namespace App\Tenant;

use App\Tenant;

/**
 * Class Context
 * @package App\Tenant
 */
class Context implements TenantInterface
{

 /**
 * Tenant Model
 * @var Tenant
 */
 private $tenant;

 /**
 * Context output. Can be null or Tenant object
 * @var void
 */
 public $context;

 /**
 * Context constructor.
 *
 * @param Tenant $tenant
 */
 public function __construct(Tenant $tenant)
 {
 $this->tenant = $tenant;
 $this->context = $this->getContext();

 }

 /**
 * Get context by host name.
 * @return null || Tenant
 */
 private function getContext()
 {
 if ((php_sapi_name() == "cli")) {
 return null;
 } else {
 $host = (string)$_SERVER['HTTP_HOST'];
 }

 $tenant = $this->tenant->where('domain', $host)->first();

 if (is_null($tenant)) {
 return null;
 }

 return $tenant;
 }
}

Dans cette classe, on injecte le modèle Tenant. On va en avoir besoin pour requêter la base, et savoir si le host demandé correspond bien à un titulaire (tenant) enregistré.

Pour éviter les soucis en mode CLI, on va devoir tester le mode, pour retourner un contexte null au lieu d’une erreur. Ca évitera de perdre l’usage de la commande php, et votre ami php artisan dans le même temps.

Si vous avez bien configuré apache/nginx, il est impossible qu’un host soit demandé à votre serveur web, sans qu’il n’ait été explicitement enregistré. Du coup, 2 cas de figure sont possibles. Le contexte est null parce que vous êtes en CLI, ou alors votre host est celui du back-office.
Parce que oui, en étant en BO, le host n’est pas dans la table tenants.
On va se servir de cette subtilité pour avoir un contexte null dans le cas où l’on se trouve sur le host du BO.

Rendre le contexte disponible dès le chargement de la requête, dans toute l’application

Maintenant que le contexte est géré, il va falloir l’enregistrer dans container de Laravel. On va donc créer une classe ContextProvider :

<?php

namespace App\Providers;

use App\Tenant;
use App\Tenant\Context;
use Illuminate\Support\ServiceProvider;

class ContextProvider extends ServiceProvider
{
 /**
 * Bootstrap the application services.
 *
 * @return void
 */
 public function boot()
 {
 //
 }

 /**
 * Register the application services.
 *
 * @return void
 */
 public function register()
 {
 $this->app['context'] = $this->app->share(function ($app) {

 return (new Context(new Tenant()));

 });

 $this->app->bind(\App\Tenant\TenantInterface::class, function ($app) {

 return $app['context'];

 });
 }
}

Pour résumer, on crée une clé context, qui retourne une instance de la classe Context, sur laquelle on peut injecter la bonne dépendance.
Ensuite, pour pouvoir injecter le contexte dans l’appli (c’est plus propre) on va utiliser l’IoC container, pour binder une interface (TenantInterface dans notre cas), qu’on pourra ensuite injecter dans n’importe quel constructeur. Pensez à vraiment créer l’interface \App\Tenant\TenantInterface.

Il ne reste que le fichier routes.php à mettre à jour :

$domains = \App\Tenant::all()->pluck('domain')->toArray();

Route::group(['middleware' => ['web']], function () use ($domains) {

    foreach ($domains as $domain) {

       //add routes
    }
});

Bisous

metrogeek

Laisser un commentaire