Управление контентом на PHP: ключевые детали и примеры
Реализация управления контентом на PHP
Основной подход: база данных MySQL с PDO
Как организовать хранение и вывод контента с защитой от SQL-инъекций?
Создадим таблицу pages:
CREATE TABLE pages (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
PHP-код для получения страницы:
$pdo = new PDO('mysql:host=localhost;dbname=mydb;charset=utf8mb4', 'user', 'pass', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
$stmt = $pdo->prepare('SELECT title, content FROM pages WHERE id = ?');
$stmt->execute([$_GET['id'] ?? 1]);
$page = $stmt->fetch();
if (!$page) { exit('Страница не найдена'); }
$title = htmlspecialchars($page['title']);
$content = nl2br(htmlspecialchars($page['content']));
echo "<h1>$title</h1><div>$content</div>";
Шаг 1: Подключение к БД с указанием кодировки utf8mb4. Шаг 2: Подготовка запроса с плейсхолдером для предотвращения инъекций. Шаг 3: Выполнение с передачей параметра. Шаг 4: Экранирование вывода через htmlspecialchars и nl2br для сохранения переносов строк.
- Использование устаревшего mysql_*. Решение: применять PDO или mysqli.
- Пропуск подготовки запроса при вставке переменных. Решение: всегда использовать подготовленные выражения.
- Отсутствие обработки исключений PDO. Решение: установка PDO::ATTR_ERRMODE в PDO::ERRMODE_EXCEPTION.
- Забыть про экранирование вывода – угроза XSS. Решение: htmlspecialchars с флагами ENT_QUOTES и кодировкой UTF-8.
Вариант: хранение контента в JSON-файлах
Как сделать простую CMS без базы данных?
Файл data.json:
[
{"id":1,"title":"Главная","content":"<p>Добро пожаловать</p>"},
{"id":2,"title":"Контакты","content":"<p>Email: info@example.com</p>"}
]
PHP-код для чтения:
$json = file_get_contents('data.json');
$pages = json_decode($json, true);
$id = $_GET['id'] ?? 1;
$filtered = array_filter($pages, fn($p) => $p['id'] == $id);
$page = reset($filtered);
if ($page) {
$title = htmlspecialchars($page['title']);
$content = $page['content']; // уже HTML, но всё равно экранируем
echo "<h1>$title</h1><div>$content</div>";
} else {
http_response_code(404);
echo 'Страница не найдена';
}
Шаги: чтение файла, декодирование, фильтрация, вывод с экранированием.
- Конкурентный доступ: при одновременной записи нескольких пользователей файл может быть повреждён. Решение: использовать файловую блокировку flock() или перейти на SQLite.
- Отсутствие структуры: при большом объёме данных поиск неэффективен. Решение: перейти на БД.
- Безопасность: если содержимое содержит PHP-код, он не выполнится, но JSON-файл может быть изменён злоумышленником. Решение: хранить файлы вне document root.
Вариант: использование Markdown-файлов
Как сделать управление контентом через текстовые файлы с разметкой?
Файл about.md:
# О компании
Текст с **полужирным** и *курсивом*.
Ссылка: (https://example.com)[Example]
PHP с библиотекой Parsedown:
require 'Parsedown.php';
$parsedown = new Parsedown();
$md = file_get_contents('pages/about.md');
if ($md === false) { http_response_code(404); exit; }
$html = $parsedown->text($md);
echo $html;
Шаги: загрузка файла, парсинг Markdown в HTML, вывод. Для кэширования результата можно сохранять скомпилированный HTML в отдельный файл.
- Обработка ошибок: файл может отсутствовать или быть недоступным для чтения.
- Производительность: при каждом запросе парсинг Markdown. Решение: кэшировать результат в файл или в память (APCu).
- Безопасность: пользовательский ввод в Markdown может содержать XSS, поэтому после парсинга нужно пропускать через HTML Purifier или экранировать теги.
Вариант: шаблонизатор Twig для вывода контента
Как отделить логику от представления и упростить разработку?
Установка через Composer: composer require twig/twig
Шаблон page.html.twig:
{% extends "base.html.twig" %}
{% block content %}
<h1>{{ title }}</h1>
<div>{{ content|raw }}</div>
{% endblock %}
PHP-код:
require_once 'vendor/autoload.php';
$loader = new \Twig\Loader\FilesystemLoader('templates');
$twig = new \Twig\Environment($loader, ['cache' => 'cache/twig', 'autoescape' => true]);
// получение данных из БД или файла
$pageData = ['title' => 'Главная', 'content' => '<p>Привет</p>'];
echo $twig->render('page.html.twig', $pageData);
Шаги: настройка автозагрузки, загрузчика шаблонов, конфигурация окружения, рендеринг с передачей переменных.
- Забыть установить кэш, что замедляет работу. Решение: указать директорию для кэша.
- Использование raw без необходимости может привести к XSS. Решение: если контент уже безопасен (например, сгенерирован через htmlspecialchars), то raw допустимо. Иначе применять фильтр escape.
- Неправильные пути к шаблонам. Решение: проверять права доступа и пути.
Расширенные примеры работы с контентом на PHP
Пример 1: Полный CRUD для страниц через PDO с админ-интерфейсом
Создание страницы (create.php):
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$pdo = new PDO('mysql:host=localhost;dbname=mydb;charset=utf8mb4', 'user', 'pass', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
$stmt = $pdo->prepare('INSERT INTO pages (title, content) VALUES (:title, :content)');
$stmt->execute(['title' => $_POST['title'], 'content' => $_POST['content']]);
header('Location: list.php');
exit;
}
?>
<form method="post">
<input type="text" name="title" required>
<textarea name="content"></textarea>
<button type="submit">Сохранить</button>
</form>
Результат (list.php) выводит список страниц:
<?php
$pdo = new PDO('...');
$stmt = $pdo->query('SELECT id, title FROM pages');
while ($row = $stmt->fetch()) {
echo "<a href='edit.php?id={$row['id']}'>{$row['title']}</a><br>";
}
Главная О нас Контакты
Редактирование (edit.php):
<?php
$id = $_GET['id'];
$pdo = new PDO('...');
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stmt = $pdo->prepare('UPDATE pages SET title=:title, content=:content WHERE id=:id');
$stmt->execute(['id' => $id, 'title' => $_POST['title'], 'content' => $_POST['content']]);
header('Location: list.php');
exit;
}
$stmt = $pdo->prepare('SELECT * FROM pages WHERE id=:id');
$stmt->execute(['id' => $id]);
$page = $stmt->fetch();
?>
<form method="post">
<input type="text" name="title" value="<?= $page['title'] ?>">
<textarea name="content"><?= $page['content'] ?></textarea>
<button>Обновить</button>
</form>
Удаление (delete.php):
<?php
$id = $_GET['id'];
$pdo = new PDO('...');
$stmt = $pdo->prepare('DELETE FROM pages WHERE id=:id');
$stmt->execute(['id' => $id]);
header('Location: list.php');
Все операции следует защищать авторизацией (сессии, проверка прав). В коде не показана обработка CSRF, что является ошибкой. Решение: добавлять токены к формам.
Пример 2: Кэширование скомпилированного Markdown в файле
Чтобы не парсить Markdown каждый раз, можно кэшировать HTML:
$cacheFile = 'cache/' . md5($pageId) . '.html';
$ttl = 3600; // 1 час
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < $ttl)) {
echo file_get_contents($cacheFile);
exit;
}
// парсинг Markdown
$md = file_get_contents('pages/' . $pageId . '.md');
$html = $parsedown->text($md);
file_put_contents($cacheFile, $html);
echo $html;
Результат: при повторном запросе в течение часа файл читается из кэша, что значительно ускоряет работу. Ошибкой является отсутствие проверки на успешность записи в кэш. Также надо обеспечить уникальность имени кэш-файла для разных страниц.
(Время генерации: 0.001 сек вместо 0.05 сек)
Пример 3: Загрузка и вставка изображений в контент через обработку форм
Форма загрузки:
<form method="post" enctype="multipart/form-data">
<input type="file" name="image" accept="image/*">
<button>Загрузить</button>
</form>
PHP-обработчик:
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (in_array($_FILES['image']['type'], $allowedTypes)) {
$ext = pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION);
$filename = uniqid() . '.' . $ext;
move_uploaded_file($_FILES['image']['tmp_name'], 'uploads/' . $filename);
echo '<img src="uploads/' . $filename . '">';
} else {
echo 'Недопустимый тип файла';
}
Результат: изображение сохраняется в папку uploads и выводится на странице. Ошибки: проверка только MIME-типа не гарантирует безопасность (можно подделать). Решение: проверять расширение, содержимое через getimagesize() и ограничивать размер файла.