Организация языковых версий контентных разделов с помощью PHP идентификаторов

Раздел: Управление контентом -> Разделы 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));
}

Проблемы: инвалидация кэша при изменении данных; увеличение потребления памяти. Решение: очищать кэш при обновлении раздела.

- Section 3 php id (раздел php с id=3)
- Section php id 4 (раздел php с id=4)
- Section php lang (раздел с языком php)

Примеры программной реализации

Пример 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(); // Новый заголовок

Разделы с языком и ID PHP - comments

En
Sections php lang id (php)