części II nauczyliśmy naszą aplikację rozpoznawać instancję na podstawie requestu i konfigurować połączenie do odpowiedniej bazy danych, problem w tym, że dane instancji, bazę, jej użytkownika oraz zawartość musieliśmy stworzyć “ręcznie”, w tym wpisie trochę ten proces zautomatyzujemy.

Przypomnijmy sobie co jest niezbędne do działania aplikacji:

  • rekord w tabeli tenants we wspólnej bazie danych,
  • baza danych dla naszego tenanta,
  • user bazy danych naszego tenanta,
  • schemat dla tej bazy,
  • zazwyczaj jakieś początkowe dane.

ZAPIS tenanta

Zacznijmy od funkcjonalności pozwalającej na zapis naszego tenanta w BD, jak pewnie pamiętasz ostatnio stworzyliśmy repozytorium TenantRepository, zaktualizujmy jego interfejs o nową metodę save(Tenant $tenant).

Naturalnie musimy również ją zaimplementować w DoctrineTenantRepository.

Potrzebujemy miejsca do wykonania naszej nowej metody, proponuję stworzenie komendy konsolowej tenant:create. Komenda “poprosi” nas o podanie nazwy instancji oraz domeny pod jaką będzie uruchomiona, utworzy obiekt klasy Tenant i utrwali go w BD, na koniec wyświetli szczegóły rekordu.

Sam proces rejestracji nowego tenanta zdecydowanie zasługuje na wyniesienie go do osobnego bytu jednak dla zachowania prostoty przykładów pozostawię go tutaj.

Tworzenie bazy danych

Po wykonaniu komendy w tabeli tenants pojawi się nowy rekord zawierający informacje potrzebne do połączenia z bazą danych tenanta, na razie są one jednak bezużyteczne ponieważ nigdzie nie tworzymy ani bazy ani jej usera.

Przygotujemy nową usługę TenantDatabaseService odpowiedzialną za operacje na bazie tenanta, jej publiczna metoda setupForTenant utworzy BD, odpowiedniego usera oraz wypełnieni bazę schematem i danymi.

Mamy zarys naszej klasy, zajmijmy się metodą createDatabase. Doctrine dostarcza nam SchemaManager, dzięki niemu w prosty sposób możemy utworzyć nową bazę:

W celu użycia nowej klasy oczywiście rejestrujemy ją jako usługę, jako argument przekazujemy połączenie do wspólnej bazy:

Wróćmy do komendy CreateTenantCommand i dodajmy wywołanie nowej funkcjonalności po zapisaniu obiektu tenanta w repo:

Po tych zmianach uruchomienie komendy tenant:create utworzy również odpowiednią bazę danych.

Tworzenie użytkownika bazy danych

Ta część jest również bardzo prosta, Doctrine co prawda nie dysponuje gotową metodą ale musimy jedynie wykonać dwa zapytania, w najprostszej postaci wygląda to mniej więcej tak:

Tworzenie schematu i wypełnianie bazy

Jest już rekord tenanta, baza i user, niestety baza świeci pustkami, nie ma w niej tabel, a co za tym idzie również danych. Pora zastanowić się w jaki sposób dostarczymy schemat do nowej bazy i czym ją uzupełnimy. Wypełnienie jest oczywiście opcjonalne ale zazwyczaj coś w niej na starcie musi być, np. konto pierwszego użytkownika.

Aby stworzyć schemat możemy np.:

Do wypełnienia bazy możemy użyć np.:

  • DoctrineFixturesBundle,
  • sklonować dane z bazy “templatki”,
  • zaimportować SQL-kę.

Zacznijmy od stworzenia interfejsu dla naszego “wypełniacza bazy”:

Możemy też na początek stworzyć jego pustą implementację:

Wróćmy do metody fillDatabase z klasy TenantDatabaseService:

Oczywiście trzeba również odpowiednio zmodyfikować konstruktor klasy.

Konfiguracja usług:

Po zdefiniowaniu kontraktu FillerInterface podmiana implementacji nie będzie problemem.

schemat przy pomocy Doctrine

Chyba najprostszy sposób na uruchomienie generowania schematu przez Doctrine to wywołanie komendy doctrine:schema:create.

W tym miejscu możemy również wywołać komendę doctrine:fixtures:load:

Konfiguracja w services.yaml:

Schemat z szablonowej instancji

Te podejście wymaga utrzymywania jednej instancji, z której nikt nie korzysta, służy ona jedynie do skopiowania jej zawartości do nowych baz.

Konfiguracja usługi:

Schemat z pliku SQL

Podobnie jak w pierwszym przykładzie uruchomimy tutaj gotową komendę, tym razem doctrine:database:import:

Listę plików do importu przekażemy w konstruktorze pobierając ją z konfiguracji:

Podsumowanie

Stworzyliśmy dziś narzędzia pozwalające obsłużyć proces od zapisu obiektu Tenant w repozytorium do wypełnienia instancji początkowymi danymi.

Dzięki wprowadzeniu interfejsu App\Service\DatabaseFiller\FillerInterface możemy zmieniać strategię inicjalizacji nowej bazy, przestawiłem tylko trzy, na pewno jest ich jeszcze kilka. Rzecz jasna sposób tworzenia schematu i jego wypełniania może się różnić, przykładowo schemat możemy stworzyć z pliku SQL, a wypełnić go fiksturami.

Zaktualizowany kod projektu jest dostępny na Githubie: https://github.com/rafalkot/multi-tenant-app