{ "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Нужно ответить не на вопрос «могут ли три языка работать в одной системе», а на более точный: «сохраняет ли каждый участник заранее названный смысл конкретной записи». Для этого у записи должны быть версия схемы, операция, описанное значение, режим ошибки и основание времени. Участники сравниваются с этим контрактом по одинаковым правилам. Сравнение PHP с JavaScript напрямую не заменяет сравнение каждого из них с общей спецификацией.
\nТакой подход отделяет факт от предположения. Факт — в записи есть schemaVersion: 'boundary-1', значение помечено тегом order-ready, а интервал задан логическими шагами от 100 до 108. Предположение — что этот объект действительно прошёл через PHP, JavaScript и D. Поля model с названиями языков не превращаются в доказательство запуска. Для последнего нужны логи, транспорт, версии сборок и тест конкретного приложения.
JSON описывает синтаксическую форму, но не объясняет значение числа или строки. Число 4200 может быть суммой в минимальных единицах, лимитом, идентификатором или счётчиком. Строка ready может быть состоянием домена, текстом интерфейса или случайным результатом сравнения. Поэтому поле value.tag должно называться явно, а денежное значение — иметь единицу и валюту. Если потребитель угадывает смысл по имени поля, контракт уже неполон.
Операция и версия нужны для защиты от тихой подмены. Контракт для fixed-order-decision нельзя автоматически применять к другому действию только потому, что там тоже есть amountMinor. Версия сообщает, по какому набору правил читать запись. Это не версия PHP, Node.js или компилятора D. Версии runtime и библиотек остаются отдельными атрибутами интеграционного теста.
Ошибку тоже нужно описывать как данные о поведении. В примере error.semantics равен named-envelope, code может быть null, а retry имеет значение not-requested. Последняя строка не означает, что повтор безопасен: она лишь фиксирует, что данная запись не запрашивает повтор. Идемпотентность, эффект повторного вызова и политика клиента требуют отдельного контракта.
Время должно иметь basis. Фиксированные логические шаги подходят для проверки порядка в заранее созданном объекте. Они не являются миллисекундами, latency или SLA. Для наблюдаемой длительности нужны источник часов, единицы измерения, границы интервала и правило обработки рассинхронизации. Подстановка текущего времени в пропущенное поле делает пример менее воспроизводимым и скрывает ошибку.
Ниже — самостоятельный пример на JavaScript. Его можно сохранить в файл и запустить в среде с поддержкой современного синтаксиса JavaScript. Он проверяет только два заранее заданных объекта в памяти: полный набор участников и набор с изменённой семантикой ошибки. Пример не запускает PHP и D, не вызывает сеть, не читает системные часы, не измеряет скорость и не проверяет сериализацию.
\nconst 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. Такое преобразование допустимо только как явно реализованный адаптер с собственными тестами и правилами.
В коде намеренно проверяются наличие полей, целые границы времени и список допустимых участников. Проверка Object.hasOwn отличает явный null от отсутствующего поля. Это важно: отсутствие code может означать, что данные потеряны, тогда как code: null — осознанная часть конкретного формата. Проект может выбрать другую политику, но её нужно записать и одинаково применить к каждому участнику.
Отказ не доказывает, что реализация плохая. Он доказывает более узкое утверждение: текущая запись не позволяет честно объявить соответствие выбранному контракту. Причина должна вести к следующему действию. Для неполной версии нужно найти владельца схемы; для неразмеченного значения — определить единицу и тег; для смешанной ошибки — выбрать границу преобразования или сохранить разницу.
\nПроверка времени должна быть особенно строгой. Если closed отсутствует, нельзя вычислять его из текущих часов. Если basis одного участника — epoch seconds, а другого — логические шаги, совпадающие числа не создают общего интервала. Если нужны реальные миллисекунды, пример следует заменить измерением на конкретном пути и отдельно указать часы, нагрузку, версию сборки и способ повторения.
Нельзя исправлять отказ неявной coercion. Превращение строки в число, округление суммы или замена пустого значения default-ом может быть осмысленной операцией. Но она меняет границу данных. Её следует назвать, покрыть тестом на исходное и полученное значение и включить в версию адаптера. Пока правило не названо, статус exact будет ложным.
| Симптом | Вероятная причина | Минимальная проверка | Действие |
|---|---|---|---|
| Похожие JSON дают разные решения | Нет версии или value tag | Сверить схему, tag, единицу и валюту | Сделать поля обязательными; не выводить смысл из shape |
| Один участник бросает ошибку, другие возвращают объект | Смешаны режимы завершения | Сравнить semantics, code и retry | Ввести явный адаптер или остановить передачу |
| В отчёте появилась latency из двух чисел | Логические шаги приняты за часы | Проверить basis, источник и единицы | Убрать вывод о скорости или провести измерение |
| Повтор создаёт вторую операцию | Retry назван без идемпотентности | Проверить ключ операции и повторный эффект | Остановить retry до отдельного решения |
| Неполная запись считается успешной | Валидатор подставляет default | Удалить default и добавить missing-case | Вернуть именованную причину отказа |
| Лог D нельзя связать с запросом PHP | Нет общего correlation id | Проверить идентификатор в каждом событии | Добавить его в новый контракт; не связывать по времени |
Контракт отвечает за представление и правила передачи. Он не решает, разрешено ли выдавать заказ, имеет ли пользователь право на операцию или безопасно ли повторять вызов. Эти решения принадлежат доменному владельцу и должны иметь собственные условия и тесты. Метка order-ready описывает значение в технической записи, но не выдаёт бизнес-разрешение.
Адаптер отвечает за явное преобразование между своим представлением и контрактом. Он не должен скрывать потерю поля, менять валюту без правила или превращать исключение в успех. Если адаптер не может выразить состояние, правильный результат — отказ с причиной. Клиент отвечает за реакцию на эту причину, а не за угадывание пропущенного значения.
\nНаблюдаемость отвечает за доказательство реального пути. Для связи событий нужны correlation id, идентификатор операции, версия участника, время с известной шкалой и запись результата. Один фиксированный объект в памяти не содержит этих свидетельств. Поэтому его положительный статус нельзя переносить на production, нагрузочное сравнение или гарантию доставки.
\nadapter error не помогает исправлению.Модель не описывает ABI, правила приведения типов, сериализатор, сетевые таймауты, порядок доставки, транзакции, сборку мусора, планировщик и раскладку памяти. Эти свойства могут менять результат реальной интеграции. Их нельзя считать проверенными по совпавшему JSON. Для каждого свойства нужен отдельный источник, тест или наблюдение в заявленной среде.
\nВалидатор также не измеряет производительность. Числа 100 и 108 — проектные целые литералы, показывающие порядок и интервал внутри fixture. Они не означают 8 миллисекунд, 8 секунд или любую другую физическую величину. Нельзя сравнивать такой объект с production latency и делать вывод о быстродействии D, PHP или JavaScript.
\nОфициальная спецификация отдельного языка не является сертификатом межъязыковой совместимости. Документация PHP описывает его исключения, спецификация ECMAScript — семантику JavaScript, спецификация D — собственные исключения и безопасность их обработки. Общий API подтверждается только контрактом приложения и тестом всех границ. Если важное условие нельзя выразить в контракте, область вывода нужно сузить, а не заполнить догадкой.
\nГраница готова к интеграционному тесту, если другой инженер без устного объяснения может назвать операцию, версию, смысл каждого значения, режим ошибки, правило retry и шкалу времени. Для полного и неполного объектов есть разные ожидаемые статусы. Каждый участник имеет идентификатор, версию и exact mapping либо описанное преобразование. Отрицательные случаи не превращаются в успех за счёт default-ов.
\nИтоговая формулировка должна оставаться узкой: «этот набор полей соответствует контракту на уровне структуры и названных семантик». Формулировки «интеграция подтверждена», «латентность известна» и «три языка совместимы» требуют дополнительных доказательств. Если их нет, именованный отказ — полезный результат: он показывает, какое именно наблюдение нужно получить дальше.
\nthrow, catch, распространения исключения по стеку и требований к выбрасываемому объекту. Используется для ограничения утверждений о PHP, а не для доказательства общего API.