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,90 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Shared\Infrastructure\Tenant;
use App\Shared\Infrastructure\Tenant\TenantConfig;
use App\Shared\Infrastructure\Tenant\TenantContext;
use App\Shared\Infrastructure\Tenant\TenantId;
use App\Shared\Infrastructure\Tenant\TenantNotSetException;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
#[CoversClass(TenantContext::class)]
final class TenantContextTest extends TestCase
{
#[Test]
public function itCanSetAndRetrieveCurrentTenant(): void
{
$tenantId = TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890');
$config = new TenantConfig(
tenantId: $tenantId,
subdomain: 'ecole-alpha',
databaseUrl: 'postgresql://user:pass@localhost:5432/classeo_alpha',
);
$context = new TenantContext();
$context->setCurrentTenant($config);
self::assertTrue($tenantId->equals($context->getCurrentTenantId()));
self::assertSame($config, $context->getCurrentTenantConfig());
}
#[Test]
public function itThrowsExceptionWhenNoTenantIsSet(): void
{
$context = new TenantContext();
$this->expectException(TenantNotSetException::class);
$context->getCurrentTenantId();
}
#[Test]
public function itThrowsExceptionWhenGettingConfigWithNoTenantSet(): void
{
$context = new TenantContext();
$this->expectException(TenantNotSetException::class);
$context->getCurrentTenantConfig();
}
#[Test]
public function itCanCheckIfTenantIsSet(): void
{
$context = new TenantContext();
self::assertFalse($context->hasTenant());
$config = new TenantConfig(
tenantId: TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890'),
subdomain: 'ecole-alpha',
databaseUrl: 'postgresql://user:pass@localhost:5432/classeo_alpha',
);
$context->setCurrentTenant($config);
self::assertTrue($context->hasTenant());
}
#[Test]
public function itCanClearTenant(): void
{
$config = new TenantConfig(
tenantId: TenantId::fromString('a1b2c3d4-e5f6-7890-abcd-ef1234567890'),
subdomain: 'ecole-alpha',
databaseUrl: 'postgresql://user:pass@localhost:5432/classeo_alpha',
);
$context = new TenantContext();
$context->setCurrentTenant($config);
self::assertTrue($context->hasTenant());
$context->clear();
self::assertFalse($context->hasTenant());
}
}