В модульном PHP-приложении граница — это не просто разделение папок, классов или namespaces. Она также определяет, какие данные модуль может принимать, какие операции выполнять и кто отвечает за проверку каждого условия. Если эти решения остаются неявными, модули могут чрезмерно доверять друг другу, дублировать проверки или предоставлять доступ к данным, которыми не должны управлять.
Проектировать границы доверия между PHP-модулями — значит явно распределять эти обязанности и сохранять возможность проверять их по мере изменения системы. Это не означает, что каждый внутренний вызов нужно считать враждебным трафиком или повторять все правила на каждом уровне. Нужно определить, где меняются полномочия, задать ясные контракты и проверять каждую операцию в модуле, компетентном её выполнять.
Почему граница между модулями — это также граница доверия

Модуль может получать данные другого модуля через внутренний интерфейс, событие, API или общий запрос. В каждом случае стоит выяснить, какие гарантии предоставляет отправитель и что должен проверить получатель. По расположению кода на этот вопрос не ответить: напрямую вызванный PHP-метод может получить данные, сформированные из внешнего запроса, фоновой задачи или другого модуля, развивающегося независимо.
Различайте проверку формата, бизнес-инварианты и авторизацию. Проверка формата подтверждает, что идентификатор или дата представлены допустимым образом. Инвариант гарантирует, например, что операция не содержит отрицательное количество. Авторизация определяет, вправе ли субъект выполнить эту операцию над этим ресурсом. Это связанные, но не взаимозаменяемые механизмы контроля.
Граница доверия особенно важна, когда меняется субъект, владелец данных или канал ввода. Например, модуль биллинга может получить заказ от другого модуля, но не должен заключать из этого, что пользователь, инициировавший запрос, вправе изменять любой счёт. Идентичность и контекст должны передаваться явно и поддаваться проверке.
Составьте перечень данных и операций, пересекающих каждую границу
Начните с карты значимых взаимодействий, а не с перепроектирования всего приложения. Для каждой границы задокументируйте, кто вызывает операцию, кому она принадлежит, какие данные передаются и является ли вызов синхронным или асинхронным. Учтите и менее заметные пути: команды консоли, задачи по расписанию, обработчики очередей и административные задачи.
- Данные: определите передаваемые поля, их источник и то, содержат ли они персональную, чувствительную информацию или данные другого домена.
- Операции: зафиксируйте, какие изменения запрашиваются и какой модуль должен их применять.
- Контекст: определите, какие субъект, организация, tenant или цель должны сопровождать запрос.
- Результаты: укажите, какие сведения модуль может вернуть и об ошибках какого типа сообщить.
Такой перечень помогает выявить тревожные признаки: модуль обращается к чужим таблицам, чтобы обойти интерфейс; получает идентификатор ресурса, не проверяя его принадлежность; или принимает изменяемый объект домена, созданный другим модулем. Он также обнаруживает дублирование: несколько потребителей по-своему реализуют правило, за которое в действительности отвечает только один домен.
Задайте явные контракты: формат, инварианты и совместимость
Контракт описывает, что необходимо получателю для выполнения операции и какие гарантии он предоставляет в ответ. В PHP его можно выразить с помощью неизменяемых DTO, интерфейсов, чётко определённых типов или схем сообщений — в зависимости от архитектуры. Конкретная форма менее важна, чем ясность: не передавайте целые сущности, если потребителю нужно всего несколько значений.
В контракте следует точно указать обязательные поля, ограничения и их смысл. Например, customerId сам по себе не сообщает, обозначает ли он глобального клиента, клиента определённой организации или клиента, доступного конкретному пользователю. Если операция зависит от контекста, включите его в контракт или определите, как его получает ответственный модуль.
Также договоритесь о различимых типах ошибок. Неправильно сформированный ввод — не то же самое, что неавторизованная операция, отсутствующий ресурс или конфликт с текущим состоянием. Сведения, передаваемые вызывающей стороне, должны быть достаточными для обработки результата, но не раскрывать данные, помогающие перебирать ресурсы или узнавать информацию о третьих лицах.
Контракты должны эволюционировать с осознанным сохранением совместимости. Добавление необязательного поля может нарушить работу системы меньше, чем изменение смысла существующего, но всё равно требует проверки производителей и потребителей. Для асинхронных сообщений учитывайте, что старая версия может продолжать передаваться и после развёртывания нового кода. Не путайте развёртывание кода с включением нового поведения: если производители и потребители не обновляются одновременно, используйте контролируемый переход.
Проверяйте авторизацию и принадлежность там, где выполняется операция
Синтаксическую проверку можно проводить у точки входа, но авторизацию нужно проверять в контексте, которому известен ресурс и который управляет действием. Например, у модуля-владельца счёта есть информация, чтобы решить, можно ли его выставить или изменить. Не следует передавать это решение вызывающему модулю, который знает только идентичность пользователя.
Полезная проверка связывает субъекта, действие, ресурс и контекст: кто запрашивает какую операцию, над каким ресурсом и в рамках какой организации или tenant. Не считайте, что полученный идентификатор сам по себе даёт разрешение. Проверяйте принадлежность на границе, выполняющей операцию, даже если запрос исходит из внутреннего кода, которому доверяют. Это не позволит альтернативному маршруту или последующим архитектурным изменениям обойти авторизацию, применяемую только в веб-интерфейсе.
Однако не дублируйте политику целиком во всех модулях. Централизуйте решение в компетентном модуле и предоставьте операцию или сервис с ясным контрактом. Модули-потребители могут выполнять дополнительные проверки для удобства и улучшения пользовательского опыта, но они не должны становиться единственным барьером безопасности.
Не допускайте, чтобы общие сущности создавали привилегированный доступ
Совместное использование ORM-сущностей между модулями кажется удобным, но может создавать скрытые зависимости. Если несколько модулей загружают и изменяют одну и ту же сущность, любой из них может обойти операции, которые должны защищать инварианты. Кроме того, изменения схемы или поведения затрагивают потребителей, не контролирующих модель.
Предпочтительнее передавать необходимые данные с помощью команд, запросов или DTO. Модуль-владелец сохраняет ответственность за запись данных и проверку своих инвариантов; потребителю не нужен общий доступ к его репозиториям или таблицам. Если требуется чтение, спроектируйте запрос, возвращающий минимально необходимый набор данных и учитывающий установленные разрешения и область доступа.
Общая база данных не требует общего владения полномочиями. Ещё до физического разделения данных можно установить правило, согласно которому только один модуль записывает определённые таблицы, а остальные используют документированные интерфейсы. Это правило снижает стоимость будущего разделения и упрощает обнаружение обращений за пределами контракта.
Проверяйте, что изоляция сохраняется при изменении системы
Тесты должны покрывать как корректный сценарий, так и попытки пересечь границу без разрешения. Модульные тесты проверяют правила модуля-владельца; интеграционные тесты — контракты, сохранение данных и конфигурацию авторизации между компонентами. Добавьте сценарии, в которых действительный субъект пытается выполнить операцию над ресурсом другого tenant, обращается к несуществующему ресурсу или передаёт неполный контекст.
- Убедитесь, что действительный идентификатор сам по себе не предоставляет доступ.
- Проверьте, что ошибки авторизации не раскрывают данные защищённого ресурса.
- Протестируйте, что потребители не могут изменять инварианты через общие сущности или прямой доступ к репозиториям.
- Запускайте контрактные тесты, если производители и потребители развёртываются независимо.
Тесты должны отражать реальную политику, а не только проверять, что выбрасывается исключение. Убедитесь, что изменение не сохраняется, нежелательное событие не публикуется и повторный запуск операции не приводит к частичным эффектам. Для многоэтапных процессов определите, что происходит при сбое одного из этапов и как согласуется состояние.
Пересматривайте границы при выделении сервиса или смене владельца

Выделение отдельного сервиса делает явными предположения, которые раньше были скрыты в памяти: идентичность, разрешения, задержки, повторные попытки, дубликаты и доступность. Контракт должен передавать достаточный контекст и учитывать сетевые сбои. Не превращайте удалённый вызов в копию локального, не пересмотрев тайм-ауты, идемпотентность и поведение при ошибках.
Пересматривайте границы и при смене ответственного за данные, добавлении tenant, появлении нового канала или новых потребителей. Выясните, кто может записывать и читать данные, какой модуль принимает решение об авторизации и какие гарантии сохраняются во время миграции. Храните ответы рядом с документацией контракта и соответствующими тестами.
Полезная граница доверия не зависит от того, остаются ли все модули в одном процессе и помнит ли команда неформальные соглашения. Её признаки — чётко назначенные полномочия, ограниченный состав передаваемых данных, проверка разрешений там, где выполняется операция, и тесты, обнаруживающие несанкционированный доступ. Так можно сохранить изоляцию, не превращая каждый модуль в копию правил остальных.



