Перенос монолитного бэкенда с PHP на современный стек TypeScript (Next.js 15 App Router, Node.js) сопряжен с фундаментальным конфликтом моделей исполнения и систем типов. PHP исторически опирается на модель разделяемого «ничего» (Shared-Nothing Architecture) с пересозданием контекста на каждый HTTP-запрос, слабую динамическую типизацию, неявное приведение типов (Coercion) и ассоциативные массивы, совмещающие свойства хеш-таблиц и векторных списков.
TypeScript, напротив, оперирует структурной системой типов времени компиляции (Compile-time Structural Typing), которая полностью стирается (Type Erasure) при сборке в JavaScript. Если перенос логики осуществляется наивным синтаксическим переводом «один к одному» через LLM, рантайм теряет гарантии безопасности. При динамическом поступлении внешних данных (JSON, query-параметры) отсутствие валидации на границах системы приводит к непредсказуемым ошибкам: различия в приведении строк, делении чисел, обработке null vs undefined и потере точности float нарушают целостность расчетов.
Надежная стратегия миграции с использованием ИИ базируется на Contract-Driven трансляции. На первом этапе агент генерирует инвариантные схемы валидации рантайма (Runtime Schemas) на базе Zod, формализующие DTO (Data Transfer Objects). На втором этапе создаются Characterization Tests (тесты фиксации поведения), покрывающие legacy-систему набором граничных векторов данных (Fuzzing Inputs). Переписанный код на TypeScript считается валидным только тогда, когда матрица выходов полностью идентична эталонным выходам PHP-движка.
Конвейер миграции строится вокруг изоляции контрактов и автоматической верификации функциональной эквивалентности:
[Legacy PHP Code] ──► [AI Contract Generator] ──► [Zod DTO Schemas]
│ │
▼ ▼
[PHP Characterization Suite] [TypeScript Implementation]
(Run against PHP Runtime) (Next.js 15 Server Action)
│ │
▼ ▼
[Golden Outputs] ◄────── [Equivalence Asserter] ◄──── [TS Test Outputs]
│
(100% Match on Edge Cases)
│
▼
[Production Deploy]"10" + 2 дает 12, а в JS "10" + 2 дает "102").
$wp_rest_server эндпоинтов с жестким контролем структуры запросов и ответов.php://input в идемпотентные Node.js микросервисы.$wpdb->prepare в типобезопасные запросы Prisma / Drizzle ORM.Рассмотрим процесс миграции критического фрагмента: функции расчета стоимости доставки с учетом габаритов, страхования и хрупкости груза.
Функция изобилует неявными преобразованиями типов, проверками через empty() и ассоциативными массивами:
<?php
declare(strict_types=1);
/**
* Legacy-расчет тарифа доставки.
* Принимает ассоциативный массив параметров.
*/
function calculate_shipping_cost(array $payload): array {
// В PHP empty("0") возвращает true! Потенциальный источник дефектов.
if (empty($payload['weight']) || empty($payload['distance'])) {
return [
'success' => false,
'error' => 'MISSING_MANDATORY_FIELDS',
'cost' => 0
];
}
$weight = (float)$payload['weight'];
$distance = (int)$payload['distance'];
$is_fragile = !empty($payload['is_fragile']);
$declared_value = isset($payload['declared_value']) ? (float)$payload['declared_value'] : 0.0;
// Базовый тариф: 150 единиц
$rate = 150.0;
// Расчет по весу: до 5 кг базовая ставка, далее 25 единиц за каждый килограмм
if ($weight > 5.0) {
$rate += ceil($weight - 5.0) * 25.0;
}
// Зональный коэффициент по дистанции
if ($distance > 500) {
$rate *= 1.4;
} elseif ($distance > 100) {
$rate *= 1.15;
}
// Наценка за хрупкость
if ($is_fragile) {
$rate += 300.0;
}
// Страховой сбор: 1.5% от объявленной стоимости при ее наличии
if ($declared_value > 0) {
$insurance = round($declared_value * 0.015, 2);
$rate += $insurance;
}
return [
'success' => true,
'cost' => round($rate, 2),
'currency' => 'RUB'
];
}Агент анализирует входные ограничения и генерирует схему с жесткой проверкой границ:
import { z } from 'zod';
// Схема входных параметров расчета доставки
export const ShippingCalculationInputSchema = z.object({
weight: z
.number({ invalid_type_error: 'Weight must be a valid number' })
.positive('Weight must be greater than zero')
.max(1000, 'Weight cannot exceed 1000 kg'),
distance: z
.number({ invalid_type_error: 'Distance must be a valid integer' })
.int('Distance must be an integer')
.positive('Distance must be strictly positive'),
isFragile: z.boolean().default(false),
declaredValue: z
.number()
.nonnegative('Declared value cannot be negative')
.optional()
.default(0),
});
export type ShippingCalculationInput = z.infer<typeof ShippingCalculationInputSchema>;
// Схема ответа (Discriminated Union для исключения некорректных состояний)
export const ShippingResultSchema = z.discriminatedUnion('success', [
z.object({
success: z.literal(true),
cost: z.number().nonnegative(),
currency: z.literal('RUB'),
}),
z.object({
success: z.literal(false),
error: z.string(),
cost: z.literal(0),
}),
]);
export type ShippingResult = z.infer<typeof ShippingResultSchema>;Реализация полностью изолирует бизнес-правила, устраняет уязвимости empty() и гарантирует предсказуемость типов:
'use server';
import {
ShippingCalculationInputSchema,
ShippingResult
} from '@/lib/contracts/shipping.schema';
/**
* Серверное действие Next.js 15 для расчета стоимости доставки.
* Полная функциональная эквивалентность legacy PHP-модулю.
*/
export async function calculateShippingAction(
rawInput: unknown
): Promise<ShippingResult> {
// Валидация рантайма через Zod SafeParse
const parsed = ShippingCalculationInputSchema.safeParse(rawInput);
if (!parsed.success) {
return {
success: false,
error: 'MISSING_MANDATORY_FIELDS',
cost: 0,
};
}
const { weight, distance, isFragile, declaredValue } = parsed.data;
let rate = 150.0;
// Расчет перевеса с шагом в 1 кг
if (weight > 5.0) {
rate += Math.ceil(weight - 5.0) * 25.0;
}
// Дистанционные зоны
if (distance > 500) {
rate *= 1.4;
} else if (distance > 100) {
rate *= 1.15;
}
// Надбавка за хрупкость
if (isFragile) {
rate += 300.0;
}
// Страховой сбор
if (declaredValue > 0) {
const insurance = Number((declaredValue * 0.015).toFixed(2));
rate += insurance;
}
// Округление до 2 знаков в соответствии с финансовыми правилами
const finalCost = Number(rate.toFixed(2));
return {
success: true,
cost: finalCost,
currency: 'RUB',
};
}Тесты фиксируют идентичность результатов PHP и TypeScript для крайних значений:
import { describe, it, expect } from 'vitest';
import { calculateShippingAction } from '@/actions/calculate-shipping';
describe('Shipping Module: Equivalence & Edge Cases', () => {
it('возвращает ошибку при отсутствии обязательных параметров', async () => {
const res = await calculateShippingAction({});
expect(res).toEqual({
success: false,
error: 'MISSING_MANDATORY_FIELDS',
cost: 0,
});
});
it('точно совпадает с PHP при пограничном весе 5.0001 кг (Math.ceil)', async () => {
// В PHP: ceil(5.0001 - 5.0) = 1. Наценка: 25. Итого: 150 + 25 = 175
const res = await calculateShippingAction({
weight: 5.0001,
distance: 50,
isFragile: false,
});
expect(res.success).toBe(true);
if (res.success) {
expect(res.cost).toBe(175.0);
}
});
it('корректно рассчитывает дистанцию и страховку с float-округлением', async () => {
// Вес 2кг (база 150). Дистанция 600 (>500 -> 150 * 1.4 = 210).
// Хрупкость true (+300 -> 510).
// Страховка: 1337.50 * 0.015 = 20.0625 -> round(20.06) -> Итого 530.06
const res = await calculateShippingAction({
weight: 2,
distance: 600,
isFragile: true,
declaredValue: 1337.5,
});
expect(res.success).toBe(true);
if (res.success) {
expect(res.cost).toBe(530.06);
}
});
it('отклоняет отрицательные дистанции и вес без падения рантайма', async () => {
const res = await calculateShippingAction({
weight: -10,
distance: -500,
});
expect(res.success).toBe(false);
});
});В PHP конструкция empty("0") вычисляется как true. В JavaScript Boolean("0") дает true, а !"0" дает false! Прямая конвертация выражений if (!payload.value) вместо явной валидации типов через Zod приводит к критическим багам, когда валидный идентификатор "0" или число 0 отбрасываются как пустые значения.
Применение приведения const data = payload as MyInput полностью отключает проверку типов TypeScript и не выполняет никакой валидации в рантайме. Если вызывающий клиент передаст undefined или число вместо строки, упадет сервер во время обращения к методам строки (.trim(), .toLowerCase()). Все внешние данные обязаны парситься через schema.safeParse().
В PHP оператор деления целых чисел $a / $b при некратном делении возвращает float (например, 7 / 2 = 3.5), но целочисленное деление выполняется через intdiv(7, 2) = 3. В JavaScript оператор / всегда вещественный (7 / 2 = 3.5). Если в legacy-коде использовалось неявное приведение (int)($a / $b), в TypeScript обязательно использовать Math.trunc(a / b).
В PHP пустой массив [] может служить как списком [], так и словарем {}. При сериализации в JSON PHP-функция json_encode([]) выдаст [], а если ключи строковые — {}. В TypeScript смешение массивов и записей недопустимо: для словарей используйте z.record(z.string(), z.unknown()), а для коллекций — z.array().
Сбор исторических запросов и формирование эталонного набора тестов на стороне PHP.
Автоматическая генерация строгих контрактов валидации входных и выходных данных.
Трансляция алгоритмов в чистые функции TypeScript без внешних мутаций.
Запуск сгенерированных Vitest тестов против эталонных результатов PHP-движка.
Подключение функций к Server Actions / Route Handlers с безопасной обработкой ошибок.