Коммерческий запрос становится опасным, когда перестаёт быть явным продуктовым решением и воплощается в виде if ($tenantId === ...). Поначалу это решает срочную задачу. Со временем такое условие появляется в контроллерах, шаблонах, процессах очереди, экспортах и API. В результате получается не конфигурация, а неявные варианты продукта, которые трудно тестировать, объяснять и выводить из эксплуатации.
Конфигурация для каждого тенанта в PHP SaaS должна допускать осознанные и управляемые различия, а не сохранять каждое историческое исключение. Полезный вопрос — не «можем ли мы сделать это для данного клиента?», а «представляет ли этот вариант стабильное измерение продукта, которое может понадобиться другим клиентам, с устойчивыми правилами и поддержкой?».
Предупреждающий сигнал: постоянное исключение в коде

Есть разница между адаптацией пользовательского опыта и поддержкой скрытой ветви продукта. Стоит вмешаться до того, как отдельный запрос создаст любой из следующих признаков:
- Идентификатор тенанта, домена или клиента появляется в бизнес-логике.
- Одно и то же правило дублируется в интерфейсе, API и асинхронном воркере.
- Команда не может ответить, у каких клиентов есть исключение и кто его одобрил.
- Изменение тарифного плана меняет функциональное поведение без централизованного определения.
- Удаление адаптации требует искать условия в нескольких репозиториях или сервисах.
Исключение может быть правомерным во время исследования или миграции, но у него должны быть владелец, дата пересмотра и дальнейший путь: превратить его в функциональную возможность продукта, изолировать как специализированную интеграцию или отклонить. Если оставить его без классификации, технический долг превращается в недокументированное коммерческое обещание.
Не путайте конфигурацию, разрешения, функциональные возможности и кастомную разработку
Эти механизмы отвечают на разные вопросы. Их смешивание приводит к непрозрачным проектным решениям и противоречивым правилам.
- Конфигурация: определяет, как существующая функция ведёт себя для тенанта. Например, формат нумерации, язык по умолчанию или необходимость дополнительного согласования в процессе.
- Разрешения: определяют, что может делать конкретная идентичность внутри тенанта. У пользователя может быть разрешение утверждать платежи, даже если утверждение настроено как обязательное.
- Функциональные возможности: указывают, имеет ли тенант доступ к функции или операционному лимиту. Они могут зависеть от договора, плана или контролируемой активации, но не должны содержать всю логику домена.
- Кастомная разработка: охватывает поведение, которое не укладывается в переиспользуемое измерение продукта, например интеграцию с собственной системой клиента или уникальную договорную трансформацию.
Практическое правило помогает принять решение: если меняется кто выполняет действие, используйте разрешения; если меняется существует ли или доступна ли функция, используйте функциональные возможности; если меняется как работает доступная функция, используйте конфигурацию. Если изменение бизнес-модели носит эксклюзивный характер, не маскируйте это под флаг.
Что должно быть настраиваемым, а что должно остаться в ядре
Опция заслуживает включения в каталог конфигурации, когда у неё ясная семантика, конечный набор значений, известные проверки и разумное ожидание повторного использования. Она также требует понятного опыта поддержки: кто-то должен быть способен объяснить эффект её изменения без изучения кода.
Хорошими кандидатами обычно являются параметры представления, политики уведомлений, пороговые значения, последовательности согласования, региональные предпочтения и выбор между уже поддерживаемыми потоками. Напротив, в ядре должны оставаться инварианты безопасности, целостность данных, базовый финансовый расчёт и правила, изменение которых потребовало бы переосмысления существующих сущностей или договоров.
Не превращайте произвольные данные в конфигурацию только ради гибкости. Поле JSON без схемы может скрывать зависимости, которые невозможно обнаружить. Когда опция изменяет критически важное правило, определите типы, допустимые значения, условия использования и последствия для предыдущих данных.
Постройте управляемую модель конфигурации
Изолированного ключа недостаточно. Каждое определение каталога должно включать метаданные, позволяющие безопасно эксплуатировать продукт:
- Ключ и функциональное описание: стабильные имена, ориентированные на домен, а не на детали реализации.
- Владелец: команда или ответственный, принимающий решения о развитии и выводе из эксплуатации.
- Область действия: глобальная, тенант, организационная единица, проект или пользователь. Не разрешайте все области действия по умолчанию.
- Значение по умолчанию: явное поведение, когда переопределение отсутствует.
- Тип и валидация: булево значение, перечисление, число с диапазоном или структура, валидируемая схемой.
- Зависимости: требования к другим опциям, функциональным возможностям или состоянию миграции.
- Чувствительность: классификация данных и правила доступа для чтения и изменения.
- Жизненный цикл: дата введения, пересмотра, устаревания и запланированного удаления, где это применимо.
В PHP централизуйте разрешение значений в доменном сервисе, например TenantSettings, и передавайте типизированные объекты вместо массивов без контракта. Приложение может объединять глобальное значение, значение тенанта и более специфичное значение по документированному приоритету. Отсутствие значения всегда должно разрешаться в значение по умолчанию, а не интерпретироваться по-разному каждым потребителем.
$policy = $tenantSettings->approvalPolicy($tenantId);
if ($policy->requiresSecondApproval()) {
$workflow->requestSecondApproval($order);
}
Хранилище может быть реляционным или документным, но каталог и валидация не должны зависеть от способа хранения. Также ведите неизменяемую историю изменений: предыдущее и новое значение, актор, момент, причина и канал изменения. История не заменяет журнал аудита бизнес-действий, но позволяет восстановить, какая конфигурация была активна.
Оценивайте решение на подходящей границе
Проблема распределённых условий не решается переносом их всех в контроллер. Конфигурация, влияющая на бизнес-правило, должна оцениваться в доменном сервисе или политике, применяющих это правило. Контроллер преобразует запрос; шаблон представляет результат; ни один из них не должен самостоятельно принимать решение о политике тенанта.
Для сложного поведения используйте зарегистрированные стратегии или политики вместо цепочек булевых значений. Политика выставления счетов может выбрать реализацию среди поддерживаемых режимов после проверки, что тенант обладает необходимой функциональной возможностью. Таким образом, интерфейс, API и очередь вызывают одно и то же решение.
Шаблоны могут получать уже подготовленное представление, включая индикаторы функциональных возможностей для отображения или скрытия действий. Скрытие кнопки не является авторизацией. API должен применять разрешения, функциональную возможность и конфигурацию на сервере, даже если интерфейс не предоставляет операцию.
Функциональные возможности и лимиты без излишней жёсткости тарифных планов
Коммерческий план может предоставлять функциональные возможности, но не должен превращаться в набор if ($plan === '...'). Смоделируйте стабильную функциональную возможность, например advanced_approvals или api_access, и определяйте, какие тенанты ею обладают, через договорной или административный источник. Затем функциональная логика проверяет функциональную возможность, а не название плана.
Лимиты требуют ещё более точного определения: что учитывается, в каком временном окне, когда применяется блокировка и как ведут себя повторные попытки и процессы очереди. Лимит должен быть наблюдаемым и согласованным во всех точках входа. Если интеграция создаёт ресурсы вне основного интерфейса, она не может обходить тот же контроль.
Изменяйте настройки безопасно и с возможностью отката
Изменение опции может немедленно повлиять на выполняемые задачи, существующие записи или интеграции. Перед сохранением проверьте тип, административные разрешения, зависимости и совместимость с текущим состоянием. Когда влияние существенно, предоставьте предварительный просмотр изменения: какой поток будет активирован, какие ограничения он нарушает и на какие будущие операции повлияет.
Постепенная активация отличается от предоставления опции во всём интерфейсе. Можно включить функциональную возможность для контролируемой группы тенантов и наблюдать за её поведением, прежде чем сделать её общедоступной. Также определите откат: какое значение восстанавливает предыдущее состояние, связаны ли с ним миграции данных и что происходит с операциями, начатыми при новой конфигурации.
Изменение, обратимое в интерфейсе, может быть необратимым в данных. Рассматривайте оба измерения отдельно перед активацией новой политики.
Поддерживайте согласованность в очередях, API и интеграциях
Асинхронные процессы добавляют ещё одно решение: разрешать конфигурацию при выполнении задачи или сохранять снимок при её создании. Для действий, которые должны соблюдать действующую политику, разрешайте её при выполнении и включайте тенант в контекст задачи. Для документов, расчётов или сообщений, которые должны воспроизводить исходное решение, сохраняйте явную версию или снимок вместе с командой.
Не смешивайте оба варианта, не объявив об этом. Повторная попытка может изменить результат, если она запрашивает обновлённую конфигурацию. Определите идемпотентность, версию конфигурации и ожидаемое поведение при повторных попытках. Внешним интеграциям нужны эквивалентные контракты: предварительная валидация, обработка ошибок, лимиты и отслеживаемость по тенанту без отправки секретов или персональных данных в диагностический журнал.
Аудит и поддержка: объясняйте наблюдаемое поведение

Поддержка должна уметь ответить, почему клиент видит определённый поток, а не только какое значение имеет ключ. Регистрируйте трассу решения с идентификатором тенанта, версией определения, источником эффективного значения — по умолчанию или переопределение — релевантными функциональными возможностями и результатом оценки. Ограничивайте доступ к этой информации и маскируйте чувствительные значения.
Дополните эту трассируемость метриками использования по опции, ошибками валидации, неудачными изменениями и неиспользуемыми опциями. Конфигурация без использования может быть устаревшей; конфигурация, долго используемая только одним тенантом, заслуживает продуктового пересмотра. Цель не в том, чтобы устранить все различия, а в том, чтобы каждое различие было явным, проверяемым, наблюдаемым и удалялось, когда перестаёт приносить ценность.



