Перейти к содержимому
DedicatedPHP Контакт

Развитие общих PHP-контрактов без блокировки поставок

Руководство по обеспечению обратной совместимости в PHP при изменении общих компонентов, миграции потребителей и контролируемом выводе API.

Редакционная диаграмма общих PHP-контрактов с адаптерами, потребителями и этапами миграции

Казалось бы, незначительное изменение в общей PHP-библиотеке может остановить независимые поставки. Переименование параметра, изменение значения по умолчанию или замена исключения способны сломать потребителя, который не разворачивается сегодня, находится в другом репозитории или вызывает компонент косвенно. Сбой может проявиться во время выполнения, в асинхронной задаче или при десериализации данных, созданных до изменения.

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

Определить, что входит во внутренний контракт

Определить, что входит во внутренний контракт — guía visual de DedicatedPHP

Внутренний контракт — это любое поведение, от которого зависит другой модуль, даже если оно не опубликовано как внешний API. Зависимости Composer и PHP-интерфейсы — видимая часть, но ими охват не исчерпывается. Перед изменением общего кода проверьте как минимум следующие элементы:

  • Публичные сигнатуры: имена методов, параметры, порядок, типы, допускаемость null, значения по умолчанию и тип возвращаемого значения.
  • Семантика: что означает каждый аргумент, какие поля обязательны и какой результат ожидается при конкретном условии.
  • Ошибки: выбрасываемые исключения, коды ошибок, сообщения, обрабатываемые клиентами, а также результаты null или пустые результаты.
  • Данные: ключи массивов, JSON-структуры, сообщения очереди, доменные события, сериализованные файлы и сохранённые данные.
  • Побочные эффекты: отправка событий, запись в базу данных, инвалидация кэша, HTTP-вызовы и порядок выполнения.
  • Операционное поведение: повторные попытки, идемпотентность, тайм-ауты и обработка временных сбоев.

Например, добавление поля в JSON-ответ обычно является аддитивным изменением, но перестаёт быть таковым, если потребитель проверяет закрытый список свойств. Аналогично, более специфичное исключение может быть технически корректным, но несовместимым, если потребитель перехватывает прежнее исключение, чтобы запустить восстановление.

Классифицировать изменение до написания реализации

Классификация не даёт проектному решению превратиться в инцидент в производственной среде. Её следует документировать в предложении изменения вместе с известными потребителями и стратегией выхода.

Аддитивные изменения

Они добавляют новую возможность, не изменяя существующий путь: новый метод, необязательный параметр с нейтральной семантикой, дополнительное событие или новую версию сообщения. Это предпочтительный вариант, когда потребители разворачиваются отдельно. Новый путь должен сосуществовать с прежним, а сохранение прежнего поведения должно быть проверяемым.

Совместимые изменения с адаптацией

Они позволяют сохранить прежний результат с помощью слоя преобразования. Например, старый интерфейс может делегировать новому сервису, преобразуя аргументы и результаты. Адаптация имеет смысл, если она локализована, имеет дату вывода из эксплуатации и не скрывает бизнес-различие, которое потребитель должен осознанно решить сам.

Несовместимые или неопределённые изменения

Удаление метода, ужесточение типа, изменение смысла статуса или изменение сохраняемого формата обычно несовместимы. Неопределённым также следует считать любое изменение без надёжного перечня потребителей. В обоих случаях недостаточно опубликовать новую версию пакета: нужны переходный период, запланированная миграция или отдельная версия контракта.

Создать проверяемый перечень потребителей

Не основывайте решение только на текстовом поиске. Компонент может быть доступен другому через контейнер зависимостей, конфигурацию, рефлексию, события, очереди или HTTP-интеграцию. Перечень должен сочетать статические подтверждения и репрезентативные проверки выполнения.

  1. Проверьте зависимости, объявленные в Composer, ограничения версий и репозитории, устанавливающие пакет.
  2. Найдите прямое использование классов, интерфейсов, методов, событий, ключей конфигурации и форматов сообщений.
  3. Изучите фабрики, определения контейнера, обработчики событий, команды, задания cron, рабочие процессы и инфраструктурные адаптеры.
  4. Определите критические пути: оплату, аутентификацию, заказы, синхронизацию, уведомления и процессы восстановления.
  5. Зафиксируйте для каждого потребителя владельца, используемую версию, путь миграции и доказательство завершения изменения.

Публикация библиотеки и развёртывание приложения — разные действия. Публикация совместимой версии позволяет каждому потребителю обновиться, когда он будет готов; одновременное развёртывание всех потребителей превращает обычную эволюцию в хрупкую организационную зависимость.

Применять аддитивную эволюцию и адаптеры на правильной границе

Когда новое требование изменяет модель, сначала введите новую возможность и временно сохраните прежнюю. Устаревший интерфейс может делегировать новой реализации, если преобразование однозначно. Так потребители мигрируют без необходимости согласовывать единое окно.

interface LegacyPriceCalculator
{
    public function calculate(int $amount): int;
}

final class LegacyPriceCalculatorAdapter implements LegacyPriceCalculator
{
    public function __construct(private PriceCalculator $calculator) {}

    public function calculate(int $amount): int
    {
        return $this->calculator->calculate(new Money($amount, 'EUR'))->amount();
    }
}

Адаптер обычно находится на границе между контрактами, а не в ядре домена. Домен должен выражать актуальную модель; преобразование старых аргументов, значений-сентинелов или исторических форматов должно оставаться в выделенном слое. Если домен сохраняет условия для каждого поколения клиентов, историческая сложность распространяется на каждое будущее изменение.

Не создавайте адаптер принудительно, когда возникает потеря информации или новое бизнес-решение. Если старый контракт не содержит данных, необходимых для нового поведения, сохраняйте оба контракта в течение переходного периода или явно запросите у потребителя дополнительную информацию.

Превратить депрекацию в управляемое удаление

API, помеченный как устаревший без альтернативы, срока и ответственного, — это не депрекация, а технический долг без отслеживания. Эффективная депрекация должна включать сигнал в коде, инструкции по миграции, условие удаления и наблюдение за использованием, когда это возможно.

  • Пометьте устаревший метод или класс понятной документацией и, если применимо, выдавайте контролируемое предупреждение с trigger_error(..., E_USER_DEPRECATED).
  • Укажите точную альтернативу, включая различия в семантике, ошибках и значениях по умолчанию.
  • Определите проверяемое условие выхода: мигрированы все репозитории из перечня, отсутствуют наблюдаемые вызовы или завершена поддержка конкретной версии.
  • Назначьте ответственного, который будет отслеживать прогресс и удалит слой при выполнении условия.

Не выдавайте предупреждения без разбора на высоконагруженных путях без стратегии агрегации: шум может скрыть значимые сигналы и повысить операционные затраты. Наблюдаемость должна отвечать на конкретный вопрос: какие потребители всё ещё используют прежний контракт и на каком пути.

Тестировать переход и выполнять последовательность поставки

Модульные тесты компонента сами по себе не доказывают, что потребители продолжают работать. Добавьте контрактные тесты для входных данных, выходных данных и ошибок, которые нужны каждому потребителю. Сохраняйте регрессионные сценарии для старого интерфейса, пока он поддерживается, и явно тестируйте отсутствующие значения, прежние сериализованные полезные нагрузки и ожидаемые исключения.

Безопасная последовательность обычно выглядит так:

  1. Опубликовать новый контракт или аддитивную реализацию, сохранив прежний путь.
  2. Обновлять и развёртывать потребителей независимо, используя интеграционные тесты там, где это оправдано риском.
  3. Наблюдать за ошибками, предупреждениями о депрекации и использованием устаревшего интерфейса.
  4. Подтвердить перечень миграции и устранить выявленных косвенных потребителей.
  5. Удалить адаптер или старый контракт в отдельной поставке, с тестами, подтверждающими его отсутствие.

Контрольный список для одобрения изменения

Контрольный список для одобрения изменения — guía visual de DedicatedPHP
  • Определён ли затронутый контракт за пределами PHP-сигнатуры?
  • Классифицировано ли изменение как аддитивное, совместимое с адаптацией, несовместимое или неопределённое?
  • Есть ли перечень потребителей, включая события, данные и косвенные пути?
  • Избегает ли решение требования одновременных развёртываний?
  • Находится ли адаптер, если он есть, вне домена и предусмотрен ли его вывод из эксплуатации?
  • Протестированы ли предыдущее поведение, новая возможность и ожидаемые ошибки?
  • Указывает ли депрекация альтернативу, условие вывода и ответственного?
  • Есть ли сигнал для обнаружения скрытых зависимостей до удаления API?

Правильное решение — не сохранять совместимость бесконечно и не навязывать полную координацию. Оно состоит в проектировании перехода с границами: сохранить необходимое, мигрировать с доказательствами и удалить историческую совместимость, когда она перестаёт обеспечивать безопасность.

Хотите применить эти идеи в своем проекте?Давайте обсудим вашу PHP-платформу.
Посмотреть связанные услуги