Миграция сайта с PHP-фреймворка на React/Next.js
Миграция с Laravel/Symfony/CodeIgniter на Next.js — смена не только технологии, но и подхода к рендерингу, деплою и разделению ответственности. Backend остаётся (или трансформируется в API), фронтенд переписывается.
Стратегии миграции
Strangler Fig — постепенная замена: Next.js забирает маршруты один за другим, пока PHP полностью не вытеснен. Nginx/CDN направляет трафик на нужный сервер по URL-паттернам.
Big Bang — полная замена за один деплой. Быстрее для небольших сайтов, рискованнее для крупных.
Гибрид — Next.js как фронтенд, PHP как API (Laravel API). Сохраняет backend-инвестиции.
Подход Strangler Fig через Nginx
server {
server_name mysite.com;
# Новые маршруты → Next.js
location /blog/ {
proxy_pass http://nextjs:3000;
}
location /about {
proxy_pass http://nextjs:3000;
}
# Старые маршруты → Laravel
location / {
proxy_pass http://laravel:8000;
}
}
Перенос бизнес-логики
PHP/Laravel validation → Zod:
// До (Laravel)
// $request->validate([
// 'email' => 'required|email|unique:users',
// 'name' => 'required|min:2|max:255',
// ]);
// После (Zod + React Hook Form)
const schema = z.object({
email: z.string().email('Некорректный email'),
name: z.string().min(2).max(255),
});
PHP ORM Eloquent → Prisma или API-слой:
// До (Eloquent в Laravel)
// User::where('active', true)->with('posts')->paginate(10);
// После (Prisma в Next.js API Route)
const users = await prisma.user.findMany({
where: { active: true },
include: { posts: true },
take: 10,
skip: (page - 1) * 10,
});
Laravel API + Next.js фронтенд
Вместо полного переписывания backend — превращаем Laravel в API:
// Laravel: превращаем в Sanctum API
Route::middleware('auth:sanctum')->group(function () {
Route::apiResource('posts', PostController::class);
Route::apiResource('products', ProductController::class);
});
// ProductController.php
public function index(Request $request): JsonResponse
{
$products = Product::query()
->when($request->category, fn($q) => $q->whereHas('category', fn($q) => $q->where('slug', $request->category)))
->paginate($request->per_page ?? 12);
return ProductResource::collection($products)->response();
}
// Next.js: клиент к Laravel API
import createClient from 'openapi-fetch';
import type { paths } from '@/types/api'; // автогенерированные из OpenAPI
const client = createClient<paths>({ baseUrl: process.env.LARAVEL_API_URL });
const { data, error } = await client.GET('/api/products', {
params: { query: { category: 'electronics', per_page: 12 } },
});
Миграция шаблонов Blade/Twig → React
{{-- Blade --}}
@foreach($products as $product)
<div class="product-card">
<img src="{{ $product->image_url }}" alt="{{ $product->name }}">
<h3>{{ $product->name }}</h3>
<span>{{ number_format($product->price) }} руб.</span>
<a href="{{ route('products.show', $product->slug) }}">Подробнее</a>
</div>
@endforeach
// React эквивалент
const ProductCard = ({ product }: { product: Product }) => (
<div className="product-card">
<img src={product.imageUrl} alt={product.name} />
<h3>{product.name}</h3>
<span>{product.price.toLocaleString('ru-RU')} руб.</span>
<Link href={`/products/${product.slug}`}>Подробнее</Link>
</div>
);
Аутентификация: PHP sessions → JWT/Cookies
// Next.js: аутентификация через Laravel Sanctum
import NextAuth from 'next-auth';
import Credentials from 'next-auth/providers/credentials';
export const { auth, handlers } = NextAuth({
providers: [
Credentials({
async authorize(credentials) {
const res = await fetch(`${process.env.LARAVEL_URL}/api/auth/login`, {
method: 'POST',
body: JSON.stringify(credentials),
headers: { 'Content-Type': 'application/json' },
});
if (!res.ok) return null;
return res.json();
},
}),
],
});
Сроки миграции
| Тип сайта | Strangler Fig | Big Bang |
|---|---|---|
| Корпоративный (10–20 страниц) | 4–8 недель | 3–6 недель |
| Блог/портал (50–200 страниц) | 8–16 недель | 6–12 недель |
| Интернет-магазин | 3–6 месяцев | 2–4 месяца |
| Сложный портал | 6–12 месяцев | Нецелесообразно |







