Download the PHP package phpsoftbox/multi-tenant without Composer

On this page you can find all versions of the php package phpsoftbox/multi-tenant. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package multi-tenant

PhpSoftBox MultiTenant

phpsoftbox/multi-tenant — расширение для multi-tenant сценариев:

Рекомендуемая production-схема: DatabaseTenantProvider как primary, ConfigTenantProvider использовать только как fallback/parity-check.

Команды

У команд provisioning (tenant:db:provision, tenant:provision:*) --tenant обязателен: они перезаписывают tenant БД (при drop_existing/--drop-existing — удаляют таблицы), поэтому запуск без аргументов не должен затрагивать всех арендаторов. all передаётся явно.

Отключённые арендаторы (enabled = false) и CLI:

tenant:queue:core:run выполняет задачу в контексте арендатора из tenant_id payload. tenant_id должен указывать ровно на одного включённого арендатора: all и списки через запятую приводят к ошибке задачи.

tenant:db:migrate и tenant:db:rollback для каждого tenant активируют runtime DSN по database_name/data.database.name/data.database.dsn. В логах команды выводят connection=<alias>, database=<database_name>, чтобы было видно, какая tenant БД обслуживается.

tenant:auth:role:* команды не читают auth-config проекта и не собирают RoleSynchronizer/UserRoleManager самостоятельно. Приложение должно зарегистрировать ScopedAuthRoleOperationsResolverInterface, который возвращает auth-операции для core и tenant runtime:

Для assign/discharge tenant scope требует явный --tenant=<id> или --tenant=all, чтобы массовое изменение ролей не запускалось случайно.

Конфиг проекта (file provider)

Database provider

DatabaseTenantProvider читает core-таблицы:

tenants.user_id (nullable) можно использовать для связи tenant с владельцем из core DB (FK при необходимости добавляется проектной миграцией).

JSON поля data в ORM Entity маппятся через #[Column(type: 'json')] и DefaultTypeCasterFactory. В data хранятся tenant-настройки (pushr, fallback-конфиги и т.д.). Для Telegram поддерживаются оба формата:

Провайдер использует ORM-entity-классы и поддерживает кастомизацию:

Можно передать свои классы (с дополнительными полями/relations), если они реализуют:

Если database_connection не задан, по умолчанию используется alias tenant.

Пример миграции для этих таблиц находится в:

Проверка совпадения Config и DB

Если нужно одновременно держать file-config и core БД синхронными, используйте ConsistentTenantProvider.

Пример DI:

Расширяемый Tenant Context (DI-friendly)

Для расширения tenant-настроек без правок ядра:

Для runtime-инициализации:

Четкая граница:

Базовые bootstrappers в компоненте:

Базовые loader-ы в компоненте:

Реализации switcher-ов в компоненте:

TenantDefinition::data уже используется для fallback-переопределений (при включенных соответствующих loader/bootstrapper):

Для host-based определения tenant/central domains:

Определение tenant в HTTP-запросах

TenantResolveMiddleware определяет tenant, записывает его в request-атрибут _tenant и запускает следующий обработчик внутри TenantRuntimeExecutor с HTTP scope. После завершения обработчика, включая исключения, runtime выполняет teardown и восстанавливает предыдущий контекст.

По умолчанию используется HostTenantRequestResolver: host из URI запроса передаётся в TenantProviderInterface::findByHost(). Существующие вызовы конструктора middleware продолжают работать. Неизвестный или пустой host приводит к TenantNotFoundException.

Отключённый арендатор (TenantDefinition::enabled = false) по HTTP не обслуживается: middleware выбрасывает то же TenantNotFoundException (TenantNotFoundException::forDisabledTenant(), сообщение Tenant is disabled: <id>), что и для неизвестного арендатора, поэтому приложение отвечает так же (обычно 404) и не раскрывает, что арендатор существует. Проверка выполняется в middleware после любого resolver (host, заголовок, цепочка, собственный), обработчик запроса не вызывается, runtime арендатора не активируется. Если приложению нужна отдельная страница «площадка отключена», её нужно обслуживать вне TenantResolveMiddleware (например, на центральном домене через TenantHostResolver: TenantHostResolution::shouldBootstrapTenant() для отключённого арендатора возвращает false).

Отдельный TenantHostResolver с CentralDomainPolicy служит для различения центральных и tenant-доменов; TenantResolveMiddleware предназначен для маршрутов, где tenant обязателен.

Если API обслуживает несколько tenants на одном домене, можно передавать ID в заголовке. Пример подключения к существующим provider и runtime:

Теперь запрос X-Tenant-Id: 42 вызывает findById('42'). Домен может быть общим для всех tenants и не обязан присутствовать в их настройках. Имя заголовка настраивается; регистр имени не имеет значения. Значение — строковый ID tenant, не имя домена, DSN или API-токен. Пробелы по краям ID удаляются.

Для приоритета заголовка с переходом к домену используйте цепочку:

Передайте $resolver в одноимённый аргумент middleware. Порядок элементов задаёт приоритет: первый найденный tenant завершает поиск. В примере заголовок имеет приоритет, даже если домен соответствует другому tenant.

Заголовок выбирает tenant, но не предоставляет доступ к нему. Приложение должно проверить право аутентифицированного пользователя или API-токена работать с выбранным tenant до выполнения бизнес-операции. Resolver не заменяет Auth; отключённый tenant отклоняет middleware (см. выше), resolver может вернуть его как найденного.

Собственный способ выбора (например, по атрибуту запроса) реализуется через TenantRequestResolverInterface::resolve($request, $tenants): ?TenantDefinition. Middleware передаёт общий provider после reload(), если provider поддерживает ReloadableTenantProviderInterface. Поэтому resolver не должен хранить свой provider или повторно обновлять его. null означает, что источник не задан и цепочка может продолжить поиск; заданный некорректный источник должен приводить к исключению. HostTenantRequestResolver сохраняет прежнее строгое поведение: пустой URI host является ошибкой.

Для диагностики middleware сохраняет событие tenant.resolve; вложенное событие поиска теперь называется tenant.request.resolve вместо tenant.provider.find_by_host и содержит класс resolver. Если приложение фильтрует profiler-события по старому имени, фильтр необходимо обновить.

Tenant-aware ORM

Для приложений, где tenant БД активируется в runtime, компонент предоставляет тонкий tenant-aware слой поверх phpsoftbox/orm:

TenantEntityManagerRegistry оборачивает базовый PhpSoftBox\Orm\Contracts\EntityAwareEntityManagerRegistryInterface и добавляет tenant(bool $write = true). Его forEntity() сначала получает имя connection из ORM metadata, а затем использует тот же кеш, что tenant()/forConnection(). По умолчанию tenant connection называется tenant, но имя можно переопределить в конструкторе.

TenantEntityManager реализует обычный ORM EntityManagerInterface, но все операции делегирует в TenantEntityManagerRegistry::tenant(). Поэтому сервисы приложения могут зависеть от TenantEntityManagerInterface и не передавать имя tenant connection вручную в каждом repository/query.

Registry кеширует entity-manager по паре read/write + connection. Метод reset() очищает кеш полностью, а reset('tenant') очищает tenant connection и runtime-подключения с префиксом tenant.. Это полезно в long-running CLI/worker процессах после tenant switch/teardown. Перед удалением manager из кеша registry вызывает UnitOfWorkInterface::clear(): это освобождает identity map, scheduled operations и ownership сущностей в общем EntityRuntimeRegistry.

Для lifecycle-кода используйте resetTenant(): registry сам применит alias, переданный ему как tenantConnectionName. Благодаря этому bootstrapper и приложение не дублируют имя tenant connection.

Если приложение предоставляет собственную реализацию TenantEntityManagerRegistryInterface, после обновления компонента в ней также нужно реализовать resetTenant(). Метод должен удалять только tenant manager-ы, не затрагивая default/core connection.

Время жизни tenant EntityManager

Tenant EntityManager имеет время жизни одного вызова TenantRuntimeExecutor::run(). TenantOrmEntityManagerBootstrapper сбрасывает его дважды:

  1. после активации tenant DSN, перед выполнением callback;
  2. перед деактивацией DSN, в teardown текущего scope.

Это важно для HTTP-запросов и особенно для долгоживущих CLI, queue, scheduler и WebSocket worker. После простоя соединение предыдущего EntityManager могло быть закрыто сервером БД. Новый runtime scope не переиспользует такой manager и получает новое connection через уже активированный tenant DSN.

Порядок задаётся priority bootstrapper-ов:

Pipeline выполняет teardown в обратном порядке, поэтому дополнительная ручная координация не требуется. Для читаемости регистрируйте bootstrappers в таком же порядке:

Bootstrapper поддерживает все TenantRuntimeScope: HTTP, CLI, Queue, Scheduler и Websocket. Во вложенном runtime tenant A → tenant B → tenant A manager tenant A сбрасывается перед входом в B. После teardown B connection manager восстанавливает DSN tenant A, а следующее обращение создаёт для A новый EntityManager.

Долгоживущие сервисы должны зависеть от TenantEntityManagerInterface: это ленивый proxy, который получает актуальный manager при каждой операции. Нельзя сохранять конкретный tenant EntityManagerInterface или загруженные им entity и передавать их в другой runtime scope.

Сброс не затрагивает core/default EntityManager и не повторяет SQL-запросы. Исключения создания connection или выполнения callback не скрываются TenantRuntimeExecutor; неперехваченное прикладным кодом исключение выходит в queue worker и обрабатывается его штатной retry/failure policy.

runtimeRegistry() делегируется базовому ORM registry и остаётся одним и тем же объектом при смене tenant. Его можно безопасно передать в Resource OrmRelationStateProvider: relation-state будет найден по экземпляру entity, а не по текущему tenant или однажды выбранному UnitOfWork.

Долгоживущие процессы и ServicesResetter

Состояние арендатора (контекст, DSN, namespace кеша и storage, переопределения конфига) применяется и откатывается TenantRuntimeExecutor::run(), поэтому отдельный сброс ему не нужен. Сбрасывать между задачами/запросами нужно tenant EntityManager — identity map и несохранённые изменения. Компонент не зависит от phpsoftbox/container; подключите методы в карту хуков ServicesResetter:

reset() очищает UnitOfWork всех закешированных manager-ов и забывает их; следующее обращение создаёт новый manager для активного DSN, поэтому хук безопасен и внутри tenant:queue:tenant:run, где сброс воркера выполняется внутри tenant runtime. Не добавляйте в хуки TenantContextStoreInterface::clear() и переключатели: в режиме tenant:queue:tenant:run это сбросило бы активного арендатора посреди обработки очереди.

Router binding должен указывать на tenant-aware registry, иначе route entity будет загружена отдельным manager базового registry:

Tenant changelog

Для tenant-aware аудита ORM изменений доступны:

TenantMongoEntityChangeLogger пишет changelog в MongoDB текущего tenant. Он ожидает provider с методом collection(string): object, а collection object должен иметь метод insertOne(array).

Update-записи, где изменились только технические поля из technicalFields, пропускаются. Ошибки записи в MongoDB не прерывают основной ORM flow: logger пишет warning в PSR logger, если он передан.

Подключение к ORM выполняется через стандартный changelog handler сущности:

TenantEntityChangeContextResolver добавляет в changelog context:

AuthManagerEntityChangeInitiatorResolver читает пользователя из одного guard. Если нужен fallback между guards, соберите его явно через ChainEntityChangeInitiatorResolver.

По умолчанию RequestAttributeEntityChangeInitiatorResolver читает scalar-id из request attribute user_id, а пользователя из request attribute user. Если приложение использует другие request attributes, укажите их явно:

В CLI/worker можно передать initiator явно:

Если initiator не передан, resolver вернет system-context и, если tenant context активен, добавит только tenant_id.

Profiler

Компонент поддерживает phpsoftbox/profiler через:

В trace появляется section multi_tenant с событиями tenant lifecycle:

Runtime tenant-подключения, которые создает TenantAwareConnectionManager, должны получать тот же ProfilerInterface и DatabaseProfilerCollector, что и основная DatabaseFactory. Тогда SQL из tenant БД попадает в общий database section, а не теряется как отдельный runtime connection.

В событиях не сохраняется DSN. Разрешены только безопасные теги: tenant_id, tenant_name, host, scope, connection, connection_alias, bootstrapper.

События записываются только при включённом профайлере и активной трассе: вне трассы collector никто не прочитает и не очистит. Список событий ограничен 5 000 записями (new MultiTenantProfilerCollector(maxItems: ...)), после предела растёт только счётчик, а в summary появляются truncated и dropped. Подробнее — в README phpsoftbox/profiler, раздел «Память в долгих процессах».

Пример DI:

Важно про создание tenant БД

Для provisioning нового tenant рекомендуется шаблонный подход:

  1. создать tenant БД как копию schema существующего template tenant без бизнес-данных
  2. оставить данные только в таблице migrations
  3. дальше поддерживать схему через tenant:db:migrate

Команда tenant:db:provision поддерживает этот workflow:

Команды после provisioning (CliCommandListProvisionStep)

tenant:provision:* после клонирования БД выполняет команды из tenancy.provision.commands (или extra.commands payload). Каждая команда запускается отдельным процессом tenancy.provision.command_runner.binary (по умолчанию php psb) без shell:

Собственный запускатель реализует TenantProvisionCommandRunnerInterface::run(list<string> $arguments) — получает аргументы команды без бинарника.


All versions of multi-tenant with dependencies

PHP Build Version
Package Version
Requires php Version ^8.5
ext-pdo Version *
phpsoftbox/auth Version ^1.0
phpsoftbox/broadcaster Version ^1.1
phpsoftbox/cache Version ^1.0
phpsoftbox/cli-app Version ^1.0
phpsoftbox/clock Version ^1.0
phpsoftbox/config Version ^1.0
phpsoftbox/database Version ^1.1
phpsoftbox/inflector Version ^1.0
phpsoftbox/orm Version ^1.0
phpsoftbox/profiler Version ^1.0
phpsoftbox/queue Version ^1.0
phpsoftbox/storage Version ^1.0
phpsoftbox/telegram Version ^1.0
psr/container Version ^2.0
psr/http-client Version ^1.0
psr/http-factory Version ^1.0
psr/http-message Version ^2.0
psr/http-server-handler Version ^1.0
psr/http-server-middleware Version ^1.0
psr/log Version ^3.0
ramsey/uuid Version ^4.7
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package phpsoftbox/multi-tenant contains the following files

Loading the files please wait ...