feat: Infrastructure multi-tenant avec isolation par sous-domaine

Une application SaaS éducative nécessite une séparation stricte des données
entre établissements scolaires. L'architecture multi-tenant par sous-domaine
(ecole-alpha.classeo.local) permet cette isolation tout en utilisant une
base de code unique.

Le choix d'une résolution basée sur les sous-domaines plutôt que sur des
headers ou tokens facilite le routage au niveau infrastructure (reverse proxy)
et offre une UX plus naturelle où chaque école accède à "son" URL dédiée.
This commit is contained in:
2026-01-30 23:34:10 +01:00
parent 6da5996340
commit 1fd256346a
71 changed files with 14390 additions and 37 deletions

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
return [
Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true],
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true],
];

View File

@@ -0,0 +1,5 @@
when@dev:
debug:
# Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser.
# See the "server:dump" command to start a new server.
dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%"

View File

@@ -0,0 +1,18 @@
# Tenants de développement
# Ces tenants sont automatiquement chargés en environnement dev
parameters:
tenant.dev_configs:
- tenantId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
subdomain: 'ecole-alpha'
databaseUrl: '%env(DATABASE_URL)%'
- tenantId: 'b2c3d4e5-f6a7-8901-bcde-f12345678901'
subdomain: 'ecole-beta'
databaseUrl: '%env(DATABASE_URL)%'
services:
App\Shared\Infrastructure\Tenant\TenantRegistry:
class: App\Shared\Infrastructure\Tenant\InMemoryTenantRegistry
factory: ['@App\Shared\Infrastructure\Tenant\TenantRegistryFactory', 'createFromConfig']
arguments:
$configs: '%tenant.dev_configs%'

View File

@@ -0,0 +1,19 @@
# Configuration des tenants en production
#
# En production, les tenants peuvent être configurés de deux façons :
# 1. Via la variable d'environnement TENANT_CONFIGS (JSON)
# 2. Via une implémentation DatabaseTenantRegistry (à implémenter)
#
# Pour l'instant, on utilise InMemoryTenantRegistry avec configuration env.
# Si aucun tenant n'est configuré, toutes les requêtes retourneront 404.
parameters:
# Format JSON attendu: [{"tenantId":"uuid","subdomain":"ecole","databaseUrl":"postgres://..."}]
tenant.prod_configs_json: '%env(default::TENANT_CONFIGS)%'
services:
App\Shared\Infrastructure\Tenant\TenantRegistry:
class: App\Shared\Infrastructure\Tenant\InMemoryTenantRegistry
factory: ['@App\Shared\Infrastructure\Tenant\TenantRegistryFactory', 'createFromEnv']
arguments:
$configsJson: '%tenant.prod_configs_json%'

View File

@@ -0,0 +1,3 @@
framework:
property_info:
with_constructor_extractor: true

View File

@@ -0,0 +1,10 @@
framework:
router:
# Configure how to generate URLs in non-HTTP contexts, such as CLI commands.
# See https://symfony.com/doc/current/routing.html#generating-urls-in-commands
default_uri: '%env(DEFAULT_URI)%'
when@prod:
framework:
router:
strict_requirements: null

View File

@@ -0,0 +1,9 @@
services:
# Tenant infrastructure event subscribers
App\Shared\Infrastructure\Tenant\TenantMiddleware:
tags:
- { name: kernel.event_subscriber }
App\Shared\Infrastructure\Security\TenantAccessDeniedHandler:
tags:
- { name: kernel.event_subscriber }

View File

@@ -0,0 +1,18 @@
# Tenants pour les tests
# Utilise les mêmes tenants que dev pour les tests d'intégration
parameters:
tenant.test_configs:
- tenantId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
subdomain: 'ecole-alpha'
databaseUrl: '%env(DATABASE_URL)%'
- tenantId: 'b2c3d4e5-f6a7-8901-bcde-f12345678901'
subdomain: 'ecole-beta'
databaseUrl: '%env(DATABASE_URL)%'
services:
App\Shared\Infrastructure\Tenant\TenantRegistry:
class: App\Shared\Infrastructure\Tenant\InMemoryTenantRegistry
factory: ['@App\Shared\Infrastructure\Tenant\TenantRegistryFactory', 'createFromConfig']
arguments:
$configs: '%tenant.test_configs%'

View File

@@ -0,0 +1,13 @@
when@dev:
web_profiler:
toolbar: true
framework:
profiler:
collect_serializer_data: true
when@test:
framework:
profiler:
collect: false
collect_serializer_data: true

View File

@@ -0,0 +1,4 @@
when@dev:
_errors:
resource: '@FrameworkBundle/Resources/config/routing/errors.php'
prefix: /_error

View File

@@ -0,0 +1,3 @@
_security_logout:
resource: security.route_loader.logout
type: service

View File

@@ -0,0 +1,8 @@
when@dev:
web_profiler_wdt:
resource: '@WebProfilerBundle/Resources/config/routing/wdt.php'
prefix: /_wdt
web_profiler_profiler:
resource: '@WebProfilerBundle/Resources/config/routing/profiler.php'
prefix: /_profiler

View File

@@ -4,6 +4,7 @@
# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
parameters:
tenant.base_domain: '%env(TENANT_BASE_DOMAIN)%'
services:
# default configuration for services in this file
@@ -25,3 +26,20 @@ services:
# Domain services need to be registered explicitly to avoid framework coupling
# Example: App\Administration\Application\Command\:
# resource: '../src/Administration/Application/Command/'
# Tenant services
App\Shared\Infrastructure\Tenant\TenantResolver:
arguments:
$baseDomain: '%tenant.base_domain%'
# TenantRegistry est configuré par environnement :
# - dev: config/packages/dev/tenant.yaml (tenants de test)
# - prod: à configurer via admin ou env vars
App\Shared\Infrastructure\Tenant\Command\CreateTenantDatabaseCommand:
arguments:
$masterDatabaseUrl: '%env(DATABASE_URL)%'
App\Shared\Infrastructure\Tenant\Command\TenantMigrateCommand:
arguments:
$projectDir: '%kernel.project_dir%'