phpsoftbox / multi-tenant
Multi-tenant extension for PhpSoftBox
Requires
- php: ^8.4
- phpsoftbox/auth: dev-master
- phpsoftbox/broadcaster: dev-master
- phpsoftbox/cache: dev-master
- phpsoftbox/clock: dev-master
- phpsoftbox/config: dev-master
- phpsoftbox/database: dev-master
- phpsoftbox/orm: dev-master
- phpsoftbox/queue: dev-master
- phpsoftbox/storage: dev-master
- phpsoftbox/telegram: dev-master
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.93
- phpsoftbox/cli-app: dev-master
- phpsoftbox/cs-fixer: ^1.0.4
- phpunit/phpunit: ^11.2
This package is auto-updated.
Last update: 2026-03-03 13:54:52 UTC
README
phpsoftbox/multi-tenant — расширение для multi-tenant сценариев:
- выбор tenant в CLI (
--tenant, по умолчаниюall) - tenant-aware миграции (
tenant:db:migrate,tenant:db:rollback) - provisioning tenant БД из template (
tenant:db:provision) - tenant-aware запуск Pushr (
tenant:pushr:serve) сPushrAppRegistryпо tenant-приложениям - два провайдера tenant-реестра:
ConfigTenantProviderDatabaseTenantProvider(core БД, JSON payloaddataчерез ORM typecaster)
Рекомендуемая production-схема: DatabaseTenantProvider как primary, ConfigTenantProvider использовать только как fallback/parity-check.
Команды
tenant:listtenant:config:check [--primary=database|config] [--connection=default]tenant:db:migrate [--tenant=all] [--path=...] [--fail-fast]tenant:db:rollback [--tenant=all] [--path=...] [--steps=1] [--fail-fast]tenant:db:provision [--tenant=all] [--template=<id>] [--migrations-table=migrations] [--drop-existing] [--fail-fast]tenant:pushr:serve [--tenant=all] [--host=0.0.0.0] [--port=8080] [--max-skew=300]tenant:queue:core:run [--max-jobs=0]tenant:queue:tenant:run [--tenant=all] [--max-jobs=0]
Конфиг проекта (file provider)
// config/app/tenancy.php return [ 'providers' => [ 'primary' => 'database', // config|database 'enforce_parity' => true, 'database' => [ 'connection' => 'default', ], ], 'tenants' => [ 'tenant-1' => [ 'name' => 'Tenant 1', 'database_connection' => 'tenant', 'database_name' => 'tenant_db_1', 'domains' => [ ['domain' => 'tenant1.chegdesklad.local', 'is_primary' => true], ['domain' => 'www.tenant1.chegdesklad.local'], ], 'data' => [ 'pushr' => [ 'app_id' => 'tenant-1', 'secret' => 'secret-tenant-1', ], 'telegram_bots' => [ [ 'code' => 'account', 'token' => 'telegram-token-account', 'username' => 'account_bot', 'is_default' => true, 'enabled' => true, ], ], ], 'enabled' => true, ], ], 'provision' => [ 'template_tenant' => 'tenant-template', ], ];
Database provider
DatabaseTenantProvider читает core-таблицы:
tenantsdomains
tenants.user_id (nullable) можно использовать для связи tenant с владельцем из core DB
(FK при необходимости добавляется проектной миграцией).
JSON поля data в ORM Entity маппятся через #[Column(type: 'json')] и DefaultTypeCasterFactory.
В data хранятся tenant-настройки (pushr, fallback-конфиги и т.д.).
Провайдер использует ORM-entity-классы и поддерживает кастомизацию:
Tenant(по умолчаниюPhpSoftBox\MultiTenant\Entity\Tenant\Tenant)Domain(по умолчаниюPhpSoftBox\MultiTenant\Entity\Tenant\Domain)
Можно передать свои классы (с дополнительными полями/relations), если они реализуют:
TenantEntityInterfaceDomainEntityInterface
Если database_connection не задан, по умолчанию используется alias tenant.
Пример миграции для этих таблиц находится в:
migrations/20260303000100_create_multi_tenant_registry_tables.phpmigrations/20260303000200_create_multi_tenant_telegram_bots_table.php
Проверка совпадения Config и DB
Если нужно одновременно держать file-config и core БД синхронными, используйте ConsistentTenantProvider.
Пример DI:
use PhpSoftBox\Config\Config; use PhpSoftBox\Database\Connection\ConnectionManagerInterface; use PhpSoftBox\MultiTenant\Contracts\TenantProviderInterface; use PhpSoftBox\MultiTenant\Tenant\Provider\ConfigTenantProvider; use PhpSoftBox\MultiTenant\Tenant\Provider\ConsistentTenantProvider; use PhpSoftBox\MultiTenant\Tenant\Provider\DatabaseTenantProvider; use Psr\Container\ContainerInterface; use function DI\factory; return [ TenantProviderInterface::class => factory(static function (ContainerInterface $container): TenantProviderInterface { return new ConsistentTenantProvider( primary: new DatabaseTenantProvider($container->get(ConnectionManagerInterface::class), 'default'), secondary: new ConfigTenantProvider($container->get(Config::class)), enforceParity: true, ); }), ];
Расширяемый Tenant Context (DI-friendly)
Для расширения tenant-настроек без правок ядра:
TenantContextFactoryпринимаетiterable<TenantExtensionLoaderInterface>- каждый loader имеет
key()/priority()/supports(scope)и добавляет extension вTenantContext - данные достаются через
TenantContext::get('key'),TenantContext::get(LoaderClass::class)илиTenantContext::getTyped(SomeConfig::class)
Для runtime-инициализации:
TenantBootstrapPipelineпринимаетiterable<TenantBootstrapperInterface>- bootstrapper имеет
priority()/supports(scope)+bootstrap()/teardown() - teardown выполняется в обратном порядке
Четкая граница:
Loaderтолько читает/собирает данные и кладет их вTenantContext(без side-effects).Bootstrapperприменяет/откатывает runtime side-effects на основеTenantContext.
Базовые bootstrappers в компоненте:
DatabaseTenantConnectionBootstrapperTenantPushrRegistryBootstrapperTenantTelegramRegistryBootstrapperTenantBroadcastChannelBootstrapperTenantCacheNamespaceBootstrapperTenantStorageNamespaceBootstrapper
Базовые loader-ы в компоненте:
TenantPushrCredentialsLoaderDatabaseTelegramBotsLoader(core tabletelegram_bots, ORM, configurable entity class)
Реализации switcher-ов в компоненте:
PushrConfigSwitcher(runtime overridepushr.app_id/pushr.secretвConfig)TelegramBotRegistrySwitcher(runtime замена bot tokens вTelegramBotRegistry)ChannelRegistryPrefixSwitcher(prefix для broadcaster channel patterns)CacheStoreNamespaceSwitcher(runtime namespace дляCacheStore)StoragePathPrefixSwitcher(runtime path/prefix дляStoragedisks)
Для host-based определения tenant/central domains:
CentralDomainPolicyTenantHostResolver+TenantHostResolution
Пример DI:
use PhpSoftBox\MultiTenant\Bootstrap\TenantBootstrapPipeline; use PhpSoftBox\MultiTenant\Context\TenantContextFactory; use Psr\Container\ContainerInterface; use function DI\factory; return [ TenantContextFactory::class => factory(static function (ContainerInterface $container): TenantContextFactory { return new TenantContextFactory([ $container->get(App\Tenancy\Loader\PushrTenantLoader::class), $container->get(App\Tenancy\Loader\TelegramTenantLoader::class), ]); }), TenantBootstrapPipeline::class => factory(static function (ContainerInterface $container): TenantBootstrapPipeline { return new TenantBootstrapPipeline([ $container->get(App\Tenancy\Bootstrap\DatabaseConnectionBootstrapper::class), $container->get(App\Tenancy\Bootstrap\TelegramRegistryBootstrapper::class), ]); }), ];
Важно про создание tenant БД
Для provisioning нового tenant рекомендуется шаблонный подход:
- создать tenant БД как копию schema существующего template tenant без бизнес-данных
- оставить данные только в таблице
migrations - дальше поддерживать схему через
tenant:db:migrate
Команда tenant:db:provision поддерживает этот workflow:
- создаёт target БД (если не существует)
- копирует структуру таблиц из template tenant
- копирует данные только из таблицы миграций (
migrationsили--migrations-table) - при непустой target БД требует явный
--drop-existing