fds/multi-tenancy-bundle

这是一个多租户symfony扩展包,支持与任何symfony项目协同工作的多租户系统。

v1.0.x-dev 2024-08-22 10:10 UTC

This package is auto-updated.

Last update: 2024-09-22 10:18:52 UTC


README

sf-multi-tenancy

Packagist 扩展包

https://packagist.org.cn/packages/fds/multi-tenancy-bundle

Symfony 多租户扩展包提供了一种简单的方法,将多租户数据库无缝集成到您的Symfony应用程序中。通过单个实体管理器管理多个数据库,它简化了Doctrine的使用,并使得在运行时切换数据库成为可能。本包包含多种功能,包括根据事件简单地切换租户数据库。

请买我一杯咖啡🙂 ☕️: https://www.buymeacoffee.com/fouadsalkini

数字

Total Downloads Monthly Downloads Daily Downloads

功能

  • 支持所有类型的数据库。
  • 支持多个子域名(tenant1.example.com, tenant2.example.com,..)。
  • 易于使用和处理
  • 可扩展的包
  • 不影响应用程序性能
  • 能够通过派发单个事件在数据库之间切换
  • 能够使用单个命令自动生成租户数据库
  • 能够为每个租户数据库自动生成迁移
  • 能够使用种子包将数据种子到特定的租户
  • 能够使用messenger在后台运行进程
  • 它使用默认的实体管理器连接。

要求

  • PHP 8.1+
  • Symfony 6+
  • Doctrine 扩展包
  • Doctrine 迁移扩展包
  • Yaml
  • Apache
  • 虚拟主机

安装

composer require fds/multi-tenancy-bundle

使用

1. 环境要求

  • BASE_HOST 添加到您的 .env 文件中。例如:BASE_HOST=yourmaindomain.com

2. 添加 doctrine 连接包装器

  • 打开您的 config/packages/doctrine.yaml 并添加 wrapper_class
    # config/packages/doctrine.yaml
    doctrine:
      dbal:
          wrapper_class: FDS\MultiTenancyBundle\DBAL\MultiDbConnectionWrapper
    

3. 租户实体

  • 创建租户实体或使用您想要配置扩展包的任何实体:
  • 在您的租户实体中使用 TenantConfigTrait 以实现完整的数据库属性要求。
  • // src/App/Entity/Tenant
    namespace App\Entity;
    use Doctrine\ORM\Mapping as ORM;
    use FDS\MultiTenancyBundle\Traits\TenantConfigTrait;
    
    class Tenant
    {
      use TenantConfigTrait;
    
      #[ORM\Id]
      #[ORM\GeneratedValue]
      #[ORM\Column]
      private ?int $id = null;
    

4. 更新 fds_multi_tenancy.yaml

  • 将您的租户实体路径添加到 config/packages/fds_multi_tenancy.yaml 文件。
      # config/packages/fds_multi_tenancy.yaml
      fds_multi_tenancy:
        tenant_entity: App\Entity\Tenant # set your custom path for your Tenant entity created in step 2.
    

5. 租户实体仓库

  • 您的 TenantRepository 应该实现 TenantRepositoryInterface 接口。
    namespace App\Repository;
    
    use App\Entity\Tenant;
    use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
    use Doctrine\Persistence\ManagerRegistry;
    use FDS\MultiTenancyBundle\Model\TenantRepositoryInterface;
    
    /**
     * @extends ServiceEntityRepository<Tenant>
     */
    class TenantRepository extends ServiceEntityRepository implements TenantRepositoryInterface{
    
    // your custom functions here
    
  • 在您的仓库中定义 findBySubdomainfindByIdentifier 函数
    public function findBySubdomain($subdomain){
        // subdomain is required here
        // you can add your specific filters here like "status","isActive",...
        return $this->findOneBy(["subdomain" => $subdomain]);
    }
    
    public function findByIdentifier($identifier){
        // use your identifier (unique value) or whathever you want(email, username,id,...)
        return $this->findOneBy(["yourCustomIdentifier" => $identifier]);
    }
    

6. 创建第一个迁移

  • 使用此命令创建数据库迁移
    bin/console doctrine:migrations:diff // default migrations folder is '%kernel.project_dir%/migrations'
    
  • 使用此命令更新数据库模式
    bin/console doctrine:migrations:migrate
    
  • 您可以通过编辑 doctrine_migrations.yaml 来更新您的迁移文件夹
      # config/packages/doctrine_migrations.yaml
      doctrine_migrations:
        migrations_paths:
            # namespace is arbitrary but should be different from App\Migrations
            # as migrations classes should NOT be autoloaded
            'DoctrineMigrations\Main': '%kernel.project_dir%/migrations/main'
        enable_profiler: '%kernel.debug%'
    

7. 使用 evotodi/seed-bundle(可选)

  • 遵循此文档以创建种子类:https://packagist.org.cn/packages/evotodi/seed-bundle
  • 按照以下方式更新您的种子类,为您的租户数据库创建种子
      namespace App\Seeds;
      /**
      * The load method is called when loading a seed 
      */
      public function load(InputInterface $input, OutputInterface $output): int
      { 
    
          /**
          * Doctrine logging eats a lot of memory, this is a wrapper to disable logging
          */ 
          $this->disableDoctrineLogging();
    
          /** @var MultiDbConnectionWrapper $connection */
          $connection = $this->em->getConnection();
          
          $this->runSeeds(); // run your seeds for your main database; you should define the runSeeds() function before.
    
          $tenants = $this->getTenants(); // add a function to fetch tenants from your main database.
    
          if (!count($tenants)) {
              return 0;
          }
    
          // loading seeds for each tenant
          foreach ($tenants as $tenant) {
              try {
                  $connection->changeDatabase($tenant->getDbName());
                  $this->runSeeds();
              } catch (Exception $e) {
                  // error handling here
              }
          }
    
          /**
          * Must return an exit code.
          * A value other than 0 or Command::SUCCESS is considered a failed seed load/unload.
          */ 
          return 0;
      }
    

8. 创建租户实例和数据库

  • 在您的租户实体中创建一条记录(email, name, subdomain, dbName)
      $tenant = new Tenant();
      $tenant->setEmail("email@example.com");
      $tenant->setName("First tenant");
      $tenant->setSubdomain("first");
      $tenant->setDbName("tenant_1");
      $em->persist($tenant);
      $em->flush();
    
  • 使用此命令为特定租户创建数据库
      php bin/console tenant:database:create
    
  • 您将被提示输入租户标识符(username|id|email|..)

9. 将 RouterSubscriber 类添加到您的项目中(可选)

  • 定义一个实现 EventSubscriberInterface 的类,以根据分配给特定租户的子域自动在数据库之间切换
    // src/EventSubscriber/RouterSubscriber.php
    namespace App\EventSubscriber;
    
    use Symfony\Component\EventDispatcher\EventSubscriberInterface;
    use Symfony\Component\HttpKernel\Event\ControllerEvent;
    
    use FDS\MultiTenancyBundle\Service\TenantService;
    
    
    class RouterSubscriber implements EventSubscriberInterface
    {
        public function __construct(
            // inject the TenantService in your constructor
            private TenantService $tenantService
            )
        {
        }
        public static function getSubscribedEvents()
        {
            return array(
                KernelEvents::CONTROLLER => array(array('onKernelController', 1)),
            );
        }
    
        public function onKernelController(ControllerEvent $event)
        {
          $request = $event->getRequest();
    
          // call the checkCurrentTenant function to detect the domain changes and switch to the tenant's specific database.
          $this->tenantService->checkCurrentTenant($request);
        }
    }
    

10. 手动在数据库之间切换(可选)

  • 您可以通过调用此函数手动在数据库之间切换
    // $em is the main entity manager
    $connection = $em->getConnection();
    $connection->changeDatabase("your database name");
    

其他说明将很快添加。