Организация языковых версий контентных разделов с помощью PHP идентификаторов
Управление контентными разделами сайта, поддерживающими несколько языков, является распространенной задачей. Каждый раздел имеет уникальный идентификатор (ID), а его содержимое может отличаться в зависимости от выбранного языка. В этой статье рассматриваются различные подходы к реализации хранения и получения таких разделов с помощью PHP.
Основные подходы к хранению многоязычных разделов
Рекомендуемый способ: реляционная модель с отдельной таблицей для переводов
Этот подход предполагает создание двух основных таблиц: sections для общей структуры и sections_lang для локализованных данных. Таблица sections содержит поля id (первичный ключ), parent_id (для вложенности), sort_order, status. Таблица sections_lang включает id, section_id (внешний ключ), lang_id (идентификатор языка, например, 'en', 'ru'), title, content, meta_description.
Пример создания таблиц SQL:
CREATE TABLE sections (
id INT AUTO_INCREMENT PRIMARY KEY,
parent_id INT DEFAULT NULL,
sort_order INT DEFAULT 0,
status TINYINT DEFAULT 1,
FOREIGN KEY (parent_id) REFERENCES sections(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE sections_lang (
id INT AUTO_INCREMENT PRIMARY KEY,
section_id INT NOT NULL,
lang_id VARCHAR(5) NOT NULL,
title VARCHAR(255),
content TEXT,
meta_description VARCHAR(255),
UNIQUE KEY unique_section_lang (section_id, lang_id),
FOREIGN KEY (section_id) REFERENCES sections(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;Section php url (url раздела php)
Запрос на получение раздела на конкретном языке выполняется через JOIN:
$query = "SELECT s.id, s.parent_id, sl.title, sl.content
FROM sections s
LEFT JOIN sections_lang sl ON s.id = sl.section_id AND sl.lang_id = ?
WHERE s.id = ?";
$stmt = $pdo->prepare($query);
$stmt->execute([$lang, $sectionId]);
$section = $stmt->fetch(PDO::FETCH_ASSOC);Indices php section (индексы разделов php)
Типичные проблемы и их решения:
- Проблема: Отсутствие индекса по (section_id, lang_id) приводит к медленным запросам. Решение: создать уникальный композитный индекс (см. SQL).
- Проблема: Кодировка содержимого не соответствует UTF-8, что вызывает искажение символов. Решение: задать CHARSET=utf8mb4 и выполнить
SET NAMES utf8mb4. - Проблема: SQL-инъекции при прямом подставлении переменных. Решение: использовать подготовленные запросы (PDO).
- Проблема: Большое количество JOIN при получении списка разделов с переводом. Решение: кэшировать результат в Redis или денормализовать данные для чтения.
Цели использования:
- Гибкость: можно добавлять новые языки без изменения схемы таблицы sections.
- Масштабируемость: индексы позволяют быстро фильтровать по ID и языку.
- Поддержка вложенности через parent_id.
Как упростить структуру БД для небольшого числа языков?
Вариант с хранением всех переводов в одном поле JSON.
В таблице sections добавляется поле translations JSON, где хранится объект вида {"en": {"title":"...", "content":"..."}, "ru":{...}}. Пример:
CREATE TABLE sections (
id INT AUTO_INCREMENT PRIMARY KEY,
parent_id INT,
translations JSON,
...
) ENGINE=InnoDB;
Catalog php section id (каталог разделов по id php)
Запрос для получения заголовка на русском языке:
$query = "SELECT JSON_UNQUOTE(JSON_EXTRACT(translations, '$.ru.title')) AS title FROM sections WHERE id = ?";Content php section id (контент раздела по id php)
Проблемы: невозможно индексировать содержимое отдельных языков; при редактировании одного языка нужно перезаписывать все поле; JSON функции могут быть медленными. Решение: использовать для статичного контента, где редактирование редко.
Как обойтись без базы данных для статичных разделов?
Использование PHP-файлов с массивами.
Создается директория lang/, например, lang/en.php и lang/ru.php. Каждый файл возвращает массив, где ключи - ID разделов, а значения - данные.
// lang/ru.php
return [
1 => ['title' => 'О нас', 'content' => 'Текст о компании
'],
2 => ['title' => 'Контакты', 'content' => 'Адрес
'],
];Index php section id (маршрут index.php с id раздела php)
Получение раздела:
$sections = include __DIR__ . '/lang/'.$lang.'.php';
$section = $sections[$id] ?? null;News php section php (новости раздела php)
Проблемы: неудобно управлять вложенностью; сложно сортировать; невозможно динамически добавлять разделы без изменения файлов. Решение: подходит для небольших сайтов с редкими изменениями.
Как абстрагироваться от SQL и использовать объектный подход?
Применение Doctrine ORM.
Создаются сущности Section и SectionLang. Пример:
/** @Entity */
class Section {
/** @Id @Column(type="integer") @GeneratedValue */
public $id;
/** @Column(type="integer", nullable=true) */
public $parentId;
/** @OneToMany(targetEntity="SectionLang", mappedBy="section") */
public $translations;
}View php section (просмотр раздела php)
// Получение раздела на нужном языке
$section = $entityManager->find(Section::class, $id);
$translation = $section->getTranslation($lang); // допустим, метод реализован
echo $translation->getTitle();Section php id 0 (раздел php с id=0)
Проблемы: накладные расходы ORM, сложность настройки для новичков. Решение: использовать только для крупных проектов с множеством связей.
Как ускорить получение данных при многих языках?
Кэширование результатов запросов с помощью Redis.
После выполнения запроса к БД результат сохраняется в Redis на время, а при повторном запросе отдается из кэша.
$cacheKey = "section_{$sectionId}_{$lang}";
$cached = $redis->get($cacheKey);
if ($cached) {
$section = json_decode($cached, true);
} else {
// запрос к БД
$section = ...;
$redis->setex($cacheKey, 3600, json_encode($section));
}Проблемы: инвалидация кэша при изменении данных; увеличение потребления памяти. Решение: очищать кэш при обновлении раздела.
Примеры программной реализации
Пример 1: CRUD для таблиц sections и sections_lang (PDO)
// Создание раздела с переводом
function createSection(PDO $pdo, $parentId, $lang, $title, $content) {
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare("INSERT INTO sections (parent_id) VALUES (?)");
$stmt->execute([$parentId]);
$sectionId = $pdo->lastInsertId();
$stmt = $pdo->prepare("INSERT INTO sections_lang (section_id, lang_id, title, content) VALUES (?, ?, ?, ?)");
$stmt->execute([$sectionId, $lang, $title, $content]);
$pdo->commit();
return $sectionId;
} catch (Exception $e) {
$pdo->rollBack();
throw $e;
}
}
// Чтение раздела
function getSection(PDO $pdo, $sectionId, $lang) {
$query = "SELECT s.id, s.parent_id, sl.title, sl.content
FROM sections s
LEFT JOIN sections_lang sl ON s.id = sl.section_id AND sl.lang_id = ?
WHERE s.id = ?";
$stmt = $pdo->prepare($query);
$stmt->execute([$lang, $sectionId]);
return $stmt->fetch(PDO::FETCH_ASSOC);
}
// Обновление перевода
function updateSectionTranslation(PDO $pdo, $sectionId, $lang, $title, $content) {
$query = "INSERT INTO sections_lang (section_id, lang_id, title, content)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE title = VALUES(title), content = VALUES(content)";
$stmt = $pdo->prepare($query);
return $stmt->execute([$sectionId, $lang, $title, $content]);
}
// Удаление раздела
function deleteSection(PDO $pdo, $sectionId) {
$pdo->prepare("DELETE FROM sections WHERE id = ?")->execute([$sectionId]); // каскадно удалит переводы
}$section = getSection($pdo, 1, 'ru');
var_dump($section);
// array(4) { ["id"]=> string(1) "1" ["parent_id"]=> NULL ["title"]=> string(12) "Главная" ["content"]=> string(16) "Контент" }Пример 2: Рекурсивное построение дерева разделов с переводом.
function getSectionTree(PDO $pdo, $lang, $parentId = null) {
$query = "SELECT s.id, s.parent_id, sl.title
FROM sections s
LEFT JOIN sections_lang sl ON s.id = sl.section_id AND sl.lang_id = ?
WHERE s.parent_id " . ($parentId === null ? "IS NULL" : "= ?") . "
ORDER BY s.sort_order";
$stmt = $pdo->prepare($query);
if ($parentId === null) {
$stmt->execute([$lang]);
} else {
$stmt->execute([$lang, $parentId]);
}
$children = [];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$row['children'] = getSectionTree($pdo, $lang, $row['id']);
$children[] = $row;
}
return $children;
}$tree = getSectionTree($pdo, 'ru');
echo json_encode($tree, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
// Пример вывода:
[
{
"id": 1,
"parent_id": null,
"title": "Главная",
"children": [
{
"id": 2,
"parent_id": 1,
"title": "О нас",
"children": []
}
]
}
]Пример 3: Использование JSON поля с функциями MySQL.
// Создание таблицы
$pdo->exec("CREATE TABLE sections_json (
id INT AUTO_INCREMENT PRIMARY KEY,
translations JSON
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");
// Вставка
$stmt = $pdo->prepare("INSERT INTO sections_json (translations) VALUES (?)");
$translations = json_encode(['ru' => ['title' => 'Привет', 'content' => 'Контент'], 'en' => ['title' => 'Hello', 'content' => 'Content']], JSON_UNESCAPED_UNICODE);
$stmt->execute([$translations]);
// Получение заголовка на русском
$stmt = $pdo->query("SELECT JSON_UNQUOTE(JSON_EXTRACT(translations, '$.ru.title')) AS title FROM sections_json WHERE id = 1");
$row = $stmt->fetch(PDO::FETCH_ASSOC);
echo $row['title']; // Привет
// Обновление одного языка (перезапись всего поля)
$newTranslations = json_encode(['ru' => ['title' => 'Новый заголовок', 'content' => 'Новый контент'], 'en' => ['title' => 'Hello', 'content' => 'Content']], JSON_UNESCAPED_UNICODE);
$pdo->prepare("UPDATE sections_json SET translations = ? WHERE id = 1")->execute([$newTranslations]);// После обновления заголовок на русском изменился.
$stmt = $pdo->query("SELECT JSON_UNQUOTE(JSON_EXTRACT(translations, '$.ru.title')) AS title FROM sections_json WHERE id = 1");
echo $stmt->fetchColumn(); // Новый заголовок