Vai al contenuto

Creare applicazioni Symfony multitenant con Ecotone

La multitenancy (multiarendamento) è un principio architetturale che permette a un’unica applicazione software di servire più clienti indipendenti, chiamati tenant. Ogni tenant può avere dati, configurazioni e logiche isolate, pur condividendo la stessa infrastruttura applicativa.

Tipi principali di multitenancy

  • Database condiviso, schemi separati: tutti i tenant usano lo stesso database fisico, ma ogni tenant ha le proprie tabelle o schemi separati. È economico ma richiede attenzione alla sicurezza dei dati.
  • Database separati per ogni tenant: ogni tenant ha un database dedicato. Offre isolamento completo e sicurezza elevata, ma può diventare complesso da scalare con centinaia di tenant.
  • Database condiviso, righe separate: tutti i tenant condividono le stesse tabelle, ma ogni riga ha un identificatore del tenant. È semplice da gestire, ma meno sicuro e può impattare le performance con grandi numeri di tenant.

In pratica, la scelta della strategia dipende da:

  • Numero di tenant previsti
  • Requisiti di isolamento dei dati
  • Performance attese
  • Budget per infrastruttura e manutenzione

Esempio concettuale

Immagina un sistema di gestione di ordini:

  • Tenant A: database separato → ordini, clienti e prodotti isolati
  • Tenant B: database separato → ordini, clienti e prodotti isolati
  • Applicazione unica → interfaccia web condivisa, logica comune

Quando l’utente di Tenant A crea un ordine, l’applicazione sa automaticamente quale database usare grazie al tenant header o al contesto configurato da Ecotone.

Ecotone

Ecotone è una libreria PHP che facilita lo sviluppo di applicazioni basate su CQRS (Command Query Responsibility Segregation), Event Sourcing e messaggistica asincrona. È particolarmente utile per sistemi complessi e multitenant perché:

  • Gestisce automaticamente il routing dei comandi, eventi e query
  • Permette di separare logica di lettura e scrittura
  • Supporta middleware per inserire logiche cross-cutting come autenticazione, logging e multitenancy
  • Facilita la gestione di processi distribuiti, saghe e orchestrazioni complesse

Funzionamento base di Ecotone

Ecotone si basa su tre concetti principali:

  1. Command Handler: gestisce comandi, cioè richieste di modifica dello stato del sistema.
  2. Event Handler: reagisce agli eventi prodotti da comandi o altri eventi.
  3. Query Handler: gestisce richieste di lettura dei dati, separando la logica di lettura da quella di scrittura.

In un sistema multitenant, Ecotone permette di:

  • Associare automaticamente ogni comando o query al tenant corretto
  • Gestire transazioni indipendenti per ogni tenant
  • Orchestrare processi complessi tra più tenant senza contaminare i dati

Esempio pratico: registrazione cliente multitenant con Ecotone

<?php

final readonly class RegistrazioneClienteHandler
{
    #[CommandHandler]
    public function handle(RegistraClienteComando $comando): void
    {
        $tenant = $comando->tenant;

        // Seleziona automaticamente l'Entity Manager corretto per il tenant
        $em = $this->tenantManager->getEntityManager($tenant);

        $cliente = new Cliente(nome: $comando->nome, email: $comando->email);

        $em->persist($cliente);
        $em->flush();

        // Pubblica evento solo per il tenant corretto
        $this->messaggistica->pubblicaEvento(new EventoClienteRegistrato(
            tenant: $tenant,
            nomeCliente: $comando->nome,
            emailCliente: $comando->email
        ));
    }
}

Con pochi passaggi, Ecotone gestisce:

  • Routing del comando al tenant giusto
  • Persistenza sicura dei dati separati
  • Pubblicazione di eventi tenant-aware

Perché usare Ecotone per sistemi multitenant?

  • Riduce il rischio di errori di routing tra tenant
  • Semplifica l’adozione di CQRS e Event Sourcing
  • Consente di integrare facilmente meccanismi asincroni, code di messaggi e orchestrazioni complesse
  • Migliora la scalabilità e la manutenibilità delle applicazioni

In sintesi, combinando Symfony con Ecotone, possiamo costruire sistemi multitenant robusti, scalabili e sicuri, con codice chiaro e facilmente manutenibile.

Il modo in cui implementiamo la multitenancy (multiarendamento) dipende dal tipo di applicazione su cui stiamo lavorando. In alcuni casi, può essere sufficiente avere un unico database condiviso, mentre in altri può essere necessario avere database separati per garantire un isolamento completo. Possiamo avere solo un paio di tenant o centinaia, e la performance potrebbe dover essere ottimizzata per tenant specifici. Tutto ciò rende la multitenancy non solo una questione tecnica, ma anche di logica di business.

In questo articolo vedremo come creare sistemi multitenant in Symfony. Per ogni scenario, forniremo anche link a demo reali, così potremo non solo discutere del concetto, ma vedere esempi pratici funzionanti.

Invio di messaggi a database separati per ogni tenant

Supponiamo di gestire un negozio online con due tenant, ognuno con il proprio database (strategia “DB per Tenant”). Il primo passo in un sistema di e-commerce è la registrazione di un nuovo cliente. Vediamo come farlo in un contesto multitenant.

Il processo avviene inviando un messaggio-comando di registrazione del cliente al nostro gestore dei comandi. Questo comando salverà il nuovo cliente nel database corretto, legato al tenant specifico.

Prima di tutto, installiamo Ecotone per Symfony:

composer require ecotone/symfony-starter

Questo comando integra Ecotone con Symfony e fornisce gli strumenti necessari per gestire più database.

Configurare le connessioni per ciascun tenant

Nel nostro esempio useremo Doctrine ORM. Ogni tenant avrà una connessione separata al database, quindi dobbiamo configurare Doctrine di conseguenza (file doctrine.yaml):

doctrine:
  dbal:
    connections:
      tenant_a_connessione:
        url: '%env(resolve:TENANT_A_DATABASE_URL)%'
        charset: UTF8
      tenant_b_connessione:
        url: '%env(resolve:TENANT_B_DATABASE_URL)%'
        charset: UTF8
  orm:
    entity_managers:
      tenant_a_connessione:
        connection: tenant_a_connessione
        mappings:
          # configurazioni mapping specifiche
      tenant_b_connessione:
        connection: tenant_b_connessione
        mappings:
          # configurazioni mapping specifiche

Ora dobbiamo mappare i nomi dei tenant ai loro Entity Manager usando la configurazione Ecotone tramite l’attributo ServiceContext:

<?php

final readonly class ConfigurazioneEcotone
{
    #[ServiceContext]
    public function configurazioneMultiTenant(): MultiTenantConfiguration
    {
        return MultiTenantConfiguration::create(
            tenantHeaderName: 'tenant',
            tenantToConnectionMapping: [
                'tenant_a' => SymfonyConnectionReference::createForManagerRegistry('tenant_a_connessione'),
                'tenant_b' => SymfonyConnectionReference::createForManagerRegistry('tenant_b_connessione')
            ],
        );
    }
}

Ora Ecotone sa quale connessione usare per ciascun tenant. Ogni volta che invieremo un messaggio (comando, evento o query), il sistema saprà quale database utilizzare.

Gestore dei comandi per il sistema multitenant

Per il nostro sistema multitenant useremo il pattern CQRS di Ecotone, che offre molte funzionalità integrate per creare sistemi multitenant.

Definiamo il gestore del comando Registrazione Cliente:

<?php

final readonly class ServizioClienti
{
    #[CommandHandler]
    public function registraCliente(RegistraClienteComando $comando): void
    {
        $cliente = new Cliente(
            nome: $comando->nome,
            email: $comando->email
        );

        // Determina il tenant corrente
        $entityManager = $this->tenantManager->getEntityManager($comando->tenant);

        $entityManager->persist($cliente);
        $entityManager->flush();
    }
}

In questo esempio, il comando RegistraClienteComando contiene il nome del tenant e i dati del cliente. Il servizio seleziona l’Entity Manager corretto e salva il cliente nel database giusto.

Approfondimento: gestione delle transazioni multitenant

In sistemi con molti tenant, è importante gestire correttamente le transazioni. Ogni Entity Manager opera su un database separato, quindi le transazioni devono essere isolate per ogni tenant. Ecotone gestisce questa separazione in automatico se le configurazioni sono corrette.

È anche possibile implementare meccanismi di retry o fallback per tenant specifici, utile quando alcuni database sono temporaneamente non disponibili.

Pubblicazione di eventi per tenant specifici

Oltre ai comandi, in un sistema multitenant è fondamentale gestire anche gli eventi. Ad esempio, dopo aver registrato un cliente, vogliamo inviare un evento che notifichi ad altri sistemi il nuovo utente, sempre legato al tenant corretto.

Definiamo un evento ClienteRegistrato:

<?php

final readonly class EventoClienteRegistrato
{
    public function __construct(
        public string $tenant,
        public string $nomeCliente,
        public string $emailCliente
    ) {}
}

Per pubblicare l’evento subito dopo la registrazione:

<?php

final readonly class ServizioClienti
{
    #[CommandHandler]
    public function registraCliente(RegistraClienteComando $comando): void
    {
        $cliente = new Cliente(
            nome: $comando->nome,
            email: $comando->email
        );

        $entityManager = $this->tenantManager->getEntityManager($comando->tenant);

        $entityManager->persist($cliente);
        $entityManager->flush();

        // Pubblica evento per tenant specifico
        $this->messaggistica->pubblicaEvento(new EventoClienteRegistrato(
            tenant: $comando->tenant,
            nomeCliente: $comando->nome,
            emailCliente: $comando->email
        ));
    }
}

Questo approccio garantisce che ogni tenant riceva solo eventi pertinenti al proprio database e alle proprie logiche di business.

Gestione delle query multitenant

Per leggere dati da database diversi, possiamo utilizzare un pattern simile a quello dei comandi. Creiamo un servizio dedicato per le query:

<?php

final readonly class ServizioQueryClienti
{
    public function trovaClientePerEmail(string $tenant, string $email): ?Cliente
    {
        $entityManager = $this->tenantManager->getEntityManager($tenant);

        return $entityManager->getRepository(Cliente::class)
                             ->findOneBy(['email' => $email]);
    }
}

In questo modo, tutte le query rispettano la separazione dei dati tra i tenant senza rischiare “incroci” tra database.

Consiglio pratico: caching per tenant

Nei sistemi con molti tenant, le query possono diventare pesanti. È consigliabile usare un cache per tenant, per esempio con Redis o Memcached. Ecotone supporta la pubblicazione e la sottoscrizione di eventi anche da cache, riducendo il carico sui database.

Gestione avanzata della multitenancy con Ecotone

Ecotone offre strumenti avanzati per semplificare lo sviluppo multitenant:

  • Middleware multitenant: intercetta ogni comando, evento o query e assegna automaticamente il tenant corretto.
  • Context aware handlers: i gestori dei comandi e degli eventi possono essere consapevoli del tenant corrente senza dover passare esplicitamente l’informazione.
  • Orchestrazione dei processi: con saghe e process manager, possiamo coordinare operazioni su più tenant mantenendo l’isolamento dei dati.

Esempio di middleware multitenant:

<?php

final readonly class MiddlewareTenant
{
    public function handleComando(ComandoInterface $comando, callable $next)
    {
        $tenant = $comando->tenant ?? throw new \InvalidArgumentException('Tenant mancante');

        $this->tenantManager->setTenantCorrente($tenant);

        return $next($comando);
    }
}

Questo middleware garantisce che tutti i comandi vengano eseguiti con il tenant corretto, evitando errori dovuti all’uso del database sbagliato.

Conclusioni e best practice

Creare un sistema multitenant in Symfony con Ecotone richiede attenzione sia alla logica dei dati sia alla separazione dei tenant. Ecco alcune best practice:

  • Usare connessioni separate per tenant critici o isolati, e un database condiviso solo per tenant meno sensibili.
  • Italianizzare nomi di classi e variabili per chiarezza e manutenzione del codice.
  • Gestire eventi e query in maniera multitenant-aware, evitando contaminazioni tra dati di tenant diversi.
  • Implementare caching e strategie di retry per migliorare performance e resilienza.
  • Usare middleware o saghe per centralizzare la gestione del tenant corrente.

Con queste strategie, possiamo creare applicazioni Symfony scalabili, sicure e manutenibili, pronte a gestire qualsiasi numero di tenant.

Se vuoi approfondire ulteriormente, ti consiglio di leggere la documentazione ufficiale di Ecotone e provare gli esempi di demo live disponibili sul sito.