{ "index": 55, "slug": "editorial-2026-06-field-multi-runtime", "title": "PHP, JavaScript и D на одной границе: как доказать совместимость", "excerpt": "Практический разбор расхождения между PHP, JavaScript и D: явный контракт значения, ошибки и времени, отрицательные проверки и границы вывода.", "contentHtml": "

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

\n

Цена ошибки — повторная операция, пропущенный отказ или расследование без доказательства связи событий. Клиент может повторить уже принятую команду, worker — принять неполный ответ за успех, а команда — объявить проблему задержкой, хотя сравнивает разные шкалы времени. Статья показывает, как проверить одну узкую границу и получить воспроизводимый результат. Она не объявляет три языка совместимыми и не заменяет тест реального сервиса.

\n

Вопрос, на который отвечает проверка

\n

Нужно ответить не на вопрос «могут ли три языка работать в одной системе», а на более точный: «сохраняет ли каждый участник заранее названный смысл конкретной записи». Для этого у записи должны быть версия схемы, операция, описанное значение, режим ошибки и основание времени. Участники сравниваются с этим контрактом по одинаковым правилам. Сравнение PHP с JavaScript напрямую не заменяет сравнение каждого из них с общей спецификацией.

\n

Такой подход отделяет факт от предположения. Факт — в записи есть schemaVersion: 'boundary-1', значение помечено тегом order-ready, а интервал задан логическими шагами от 100 до 108. Предположение — что этот объект действительно прошёл через PHP, JavaScript и D. Поля model с названиями языков не превращаются в доказательство запуска. Для последнего нужны логи, транспорт, версии сборок и тест конкретного приложения.

\n

Контракт начинается со смысла, а не с формы JSON

\n

JSON описывает синтаксическую форму, но не объясняет значение числа или строки. Число 4200 может быть суммой в минимальных единицах, лимитом, идентификатором или счётчиком. Строка ready может быть состоянием домена, текстом интерфейса или случайным результатом сравнения. Поэтому поле value.tag должно называться явно, а денежное значение — иметь единицу и валюту. Если потребитель угадывает смысл по имени поля, контракт уже неполон.

\n

Операция и версия нужны для защиты от тихой подмены. Контракт для fixed-order-decision нельзя автоматически применять к другому действию только потому, что там тоже есть amountMinor. Версия сообщает, по какому набору правил читать запись. Это не версия PHP, Node.js или компилятора D. Версии runtime и библиотек остаются отдельными атрибутами интеграционного теста.

\n

Ошибку тоже нужно описывать как данные о поведении. В примере error.semantics равен named-envelope, code может быть null, а retry имеет значение not-requested. Последняя строка не означает, что повтор безопасен: она лишь фиксирует, что данная запись не запрашивает повтор. Идемпотентность, эффект повторного вызова и политика клиента требуют отдельного контракта.

\n

Время должно иметь basis. Фиксированные логические шаги подходят для проверки порядка в заранее созданном объекте. Они не являются миллисекундами, latency или SLA. Для наблюдаемой длительности нужны источник часов, единицы измерения, границы интервала и правило обработки рассинхронизации. Подстановка текущего времени в пропущенное поле делает пример менее воспроизводимым и скрывает ошибку.

\n

Воспроизводимая проверка в памяти

\n

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

\n
const contract = {\n  schemaVersion: 'boundary-1',\n  operation: 'fixed-order-decision',\n  value: { tag: 'order-ready', amountMinor: 4200, currency: 'RUB' },\n  error: { semantics: 'named-envelope', code: null, retry: 'not-requested' },\n  time: { basis: 'fixed-logical-ticks', opened: 100, closed: 108 }\n};\n\nconst participants = [\n  { model: 'php', version: 'boundary-1', valueTag: 'order-ready',\n    errorSemantics: 'named-envelope', timeBasis: 'fixed-logical-ticks', mapping: 'exact' },\n  { model: 'javascript', version: 'boundary-1', valueTag: 'order-ready',\n    errorSemantics: 'named-envelope', timeBasis: 'fixed-logical-ticks', mapping: 'exact' },\n  { model: 'd', version: 'boundary-1', valueTag: 'order-ready',\n    errorSemantics: 'named-envelope', timeBasis: 'fixed-logical-ticks', mapping: 'exact' }\n];\n\nfunction review(record, items) {\n  if (record?.schemaVersion !== 'boundary-1' ||\n      record.operation !== 'fixed-order-decision' || items.length !== 3) {\n    return 'stop-incomplete-contract';\n  }\n  if (record.value?.tag !== 'order-ready' ||\n      record.value.currency !== 'RUB' || !Number.isInteger(record.value.amountMinor)) {\n    return 'stop-untagged-value';\n  }\n  if (record.error?.semantics !== 'named-envelope' ||\n      !Object.hasOwn(record.error, 'code') || !Object.hasOwn(record.error, 'retry')) {\n    return 'stop-ambiguous-error';\n  }\n  if (record.time?.basis !== 'fixed-logical-ticks' ||\n      !Number.isInteger(record.time.opened) || !Number.isInteger(record.time.closed) ||\n      record.time.opened > record.time.closed) {\n    return 'stop-undetermined-time-boundary';\n  }\n  const knownModels = new Set(['php', 'javascript', 'd']);\n  const exact = items.every((item) =>\n    knownModels.has(item.model) &&\n    item.version === record.schemaVersion &&\n    item.valueTag === record.value.tag &&\n    item.errorSemantics === record.error.semantics &&\n    item.timeBasis === record.time.basis &&\n    item.mapping === 'exact'\n  );\n  return exact ? 'accepted-fixed-contract' : 'stop-incomparable-adapter';\n}\n\nconsole.log(review(contract, participants));\nconst mixedError = participants.map((item) =>\n  item.model === 'javascript' ? { ...item, errorSemantics: 'thrown-value' } : item\n);\nconsole.log(review(contract, mixedError));
\n

Ожидаемый вывод — сначала accepted-fixed-contract, затем stop-incomparable-adapter. Первый результат означает только то, что все проверенные поля совпали с проектными литералами. Второй показывает, что один участник описывает ошибку иначе. Валидатор не пытается угадать, можно ли преобразовать исключение в envelope. Такое преобразование допустимо только как явно реализованный адаптер с собственными тестами и правилами.

\n

В коде намеренно проверяются наличие полей, целые границы времени и список допустимых участников. Проверка Object.hasOwn отличает явный null от отсутствующего поля. Это важно: отсутствие code может означать, что данные потеряны, тогда как code: null — осознанная часть конкретного формата. Проект может выбрать другую политику, но её нужно записать и одинаково применить к каждому участнику.

\n

Как читать отрицательный результат

\n

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

\n

Проверка времени должна быть особенно строгой. Если closed отсутствует, нельзя вычислять его из текущих часов. Если basis одного участника — epoch seconds, а другого — логические шаги, совпадающие числа не создают общего интервала. Если нужны реальные миллисекунды, пример следует заменить измерением на конкретном пути и отдельно указать часы, нагрузку, версию сборки и способ повторения.

\n

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

\n
Симптомы потери смысла, проверка и безопасное действие
СимптомВероятная причинаМинимальная проверкаДействие
Похожие JSON дают разные решенияНет версии или value tagСверить схему, tag, единицу и валютуСделать поля обязательными; не выводить смысл из shape
Один участник бросает ошибку, другие возвращают объектСмешаны режимы завершенияСравнить semantics, code и retryВвести явный адаптер или остановить передачу
В отчёте появилась latency из двух чиселЛогические шаги приняты за часыПроверить basis, источник и единицыУбрать вывод о скорости или провести измерение
Повтор создаёт вторую операциюRetry назван без идемпотентностиПроверить ключ операции и повторный эффектОстановить retry до отдельного решения
Неполная запись считается успешнойВалидатор подставляет defaultУдалить default и добавить missing-caseВернуть именованную причину отказа
Лог D нельзя связать с запросом PHPНет общего correlation idПроверить идентификатор в каждом событииДобавить его в новый контракт; не связывать по времени
\n

Где проходит граница ответственности

\n

Контракт отвечает за представление и правила передачи. Он не решает, разрешено ли выдавать заказ, имеет ли пользователь право на операцию или безопасно ли повторять вызов. Эти решения принадлежат доменному владельцу и должны иметь собственные условия и тесты. Метка order-ready описывает значение в технической записи, но не выдаёт бизнес-разрешение.

\n

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

\n

Наблюдаемость отвечает за доказательство реального пути. Для связи событий нужны correlation id, идентификатор операции, версия участника, время с известной шкалой и запись результата. Один фиксированный объект в памяти не содержит этих свидетельств. Поэтому его положительный статус нельзя переносить на production, нагрузочное сравнение или гарантию доставки.

\n
Схема проверки контракта между PHP, JavaScript и D с отдельными ветками отказа при неполной записи и точного сопоставления
Схема показывает порядок чтения фиксированной записи: граница, семантика и точное сопоставление. Она не изображает сетевой вызов, запуск трёх runtime или результат production-наблюдения.
\n

Порядок проверки в настоящем проекте

\n
  1. Назовите одну операцию, владельца доменного смысла и версию контракта.
  2. Зафиксируйте value tag, формат числа, единицы, валюту и допустимые пустые значения.
  3. Опишите один режим ошибки. Отдельно укажите code, retry и условие, при котором повтор безопасен.
  4. Выберите time basis. Для latency назовите источник часов и единицы; для порядка используйте отдельные логические метки.
  5. Составьте по одному образцу для PHP, JavaScript и D. Сравнивайте каждый образец с контрактом.
  6. Добавьте отрицательные случаи: пропущенная версия, неизвестный tag, смешанная ошибка, отсутствующая граница времени и скрытое преобразование.
  7. Сохраните для каждого отказа код причины и поле, которое его вызвало. Общий статус adapter error не помогает исправлению.
  8. Проверьте сериализацию и транспорт отдельными тестами. Зафиксируйте версии runtime, библиотеки, сборки и окружения.
  9. Только после этого проверяйте реальный путь по логам и correlation id. Не приписывайте фиксированному примеру эффекты работающей системы.
\n

Ограничения применимости

\n

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

\n

Валидатор также не измеряет производительность. Числа 100 и 108 — проектные целые литералы, показывающие порядок и интервал внутри fixture. Они не означают 8 миллисекунд, 8 секунд или любую другую физическую величину. Нельзя сравнивать такой объект с production latency и делать вывод о быстродействии D, PHP или JavaScript.

\n

Официальная спецификация отдельного языка не является сертификатом межъязыковой совместимости. Документация PHP описывает его исключения, спецификация ECMAScript — семантику JavaScript, спецификация D — собственные исключения и безопасность их обработки. Общий API подтверждается только контрактом приложения и тестом всех границ. Если важное условие нельзя выразить в контракте, область вывода нужно сузить, а не заполнить догадкой.

\n

Критерий готовности

\n

Граница готова к интеграционному тесту, если другой инженер без устного объяснения может назвать операцию, версию, смысл каждого значения, режим ошибки, правило retry и шкалу времени. Для полного и неполного объектов есть разные ожидаемые статусы. Каждый участник имеет идентификатор, версию и exact mapping либо описанное преобразование. Отрицательные случаи не превращаются в успех за счёт default-ов.

\n

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

\n

Проверяемые источники

\n" }