Создание PHP-модуля для контент-менеджмента: подходы и код

Раздел: Управление контентом -> Разделы PHP

Основные подходы к реализации раздела управления контентом на PHP

Наиболее эффективное решение: связка PDO + шаблонизатор Twig

Данный подход обеспечивает безопасность (защита от SQL-инъекций через параметризованные запросы), удобство поддержки и гибкость. База данных MySQL или PostgreSQL, подключение через PDO, шаблоны отделены от логики.

Как создать защищённый CRUD-контроллер для статей?

Пример структуры: файл config/database.php с настройками, ArticleRepository.php для запросов, ArticleController.php для обработки запросов, шаблоны в папке templates/.


// config/database.php
$host = 'localhost';
$dbname = 'cms_db';
$user = 'root';
$pass = '';

$options = [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false,
];

try {
    $pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8mb4", $user, $pass, $options);
} catch (PDOException $e) {
    // Логирование ошибки, вывод сообщения пользователю
    die('Ошибка подключения к базе данных');
}
  

Section php url (url раздела php)


// ArticleRepository.php
class ArticleRepository {
    private PDO $pdo;

    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }

    public function getAll(): array {
        $stmt = $this->pdo->query('SELECT id, title, created_at FROM articles ORDER BY created_at DESC');
        return $stmt->fetchAll();
    }

    public function getById(int $id): ?array {
        $stmt = $this->pdo->prepare('SELECT * FROM articles WHERE id = ?');
        $stmt->execute([$id]);
        return $stmt->fetch() ?: null;
    }

    public function create(string $title, string $content): bool {
        $stmt = $this->pdo->prepare('INSERT INTO articles (title, content, created_at) VALUES (?, ?, NOW())');
        return $stmt->execute([$title, $content]);
    }
}
  

Indices php section (индексы разделов php)

Типичные ошибки

  • Использование устаревшего mysql_* - небезопасно, заменять на PDO.
  • Неправильная кодировка: в PDO указывать charset=utf8mb4, иначе смайлы и спецсимволы потеряются.
  • Отсутствие проверки прав доступа - любой пользователь сможет редактировать контент.

Решение: всегда применять параметризованные запросы, проверять токены CSRF и авторизацию.

Цель использования

Создание масштабируемой админ-панели с возможностью лёгкого расширения функционала (категории, теги, медиафайлы).

Как хранить контент без базы данных, только файлами?

Использование JSON-файлов для хранения статей. Подходит для небольших проектов, где нет сложных запросов.


// data/articles.json
[
  {
    "id": 1,
    "title": "Первая статья",
    "content": "Текст статьи...",
    "created_at": "2025-04-01"
  }
]
  

Catalog php section id (каталог разделов по id php)


// ArticleFileRepository.php
class ArticleFileRepository {
    private string $filePath;

    public function __construct(string $filePath) {
        $this->filePath = $filePath;
    }

    private function load(): array {
        if (!file_exists($this->filePath)) {
            return [];
        }
        $content = file_get_contents($this->filePath);
        return json_decode($content, true) ?? [];
    }

    private function save(array $data): void {
        file_put_contents($this->filePath, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
    }

    public function getAll(): array {
        return $this->load();
    }

    public function getById(int $id): ?array {
        $articles = $this->load();
        foreach ($articles as $article) {
            if ($article['id'] === $id) {
                return $article;
            }
        }
        return null;
    }

    public function create(string $title, string $content): void {
        $articles = $this->load();
        $newId = count($articles) > 0 ? max(array_column($articles, 'id')) + 1 : 1;
        $articles[] = [
            'id' => $newId,
            'title' => $title,
            'content' => $content,
            'created_at' => date('Y-m-d H:i:s')
        ];
        $this->save($articles);
    }
}
  

Content php section id (контент раздела по id php)

Проблемы

  • Гонка состояний (race condition) при параллельных запросах - данные могут перезаписываться с потерями.
  • Отсутствие поиска и фильтрации без полной загрузки файла.
  • Сложность масштабирования при большом количестве записей.

Решение: использовать файловую блокировку flock() для записи, применять при малых нагрузках.

Цель использования

Быстрый старт без настройки сервера БД, прототипирование, статические сайты с простым редактированием.

Как использовать легковесную базу данных без установки сервера?

SQLite - файловая БД, подходит для проектов с одним пользователем или малым трафиком.


// config/sqlite.php
$dbPath = __DIR__ . '/../data/cms.sqlite';
$pdo = new PDO("sqlite:$dbPath");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$pdo->exec('CREATE TABLE IF NOT EXISTS articles (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    content TEXT,
    created_at TEXT DEFAULT (datetime("now"))
)');
  

Index php section id (маршрут index.php с id раздела php)


// ArticleSqliteRepository.php (аналогично PDO)
class ArticleSqliteRepository {
    private PDO $pdo;
    public function __construct(PDO $pdo) { $this->pdo = $pdo; }
    // методы одинаковы, только SQL может использовать datetime('now') вместо NOW()
}
  

News php section php (новости раздела php)

Ошибки

  • Права на запись в папку с файлом БД (обычно нужны 777 или владелец - веб-сервер).
  • Одновременное изменение из нескольких потоков - SQLite блокирует всю БД при записи.
  • Сложность полнотекстового поиска (требуется FTS5).

Цель использования

Портативные приложения, встроенные панели, окружения без поддержки MySQL (например, shared hosting с PHP 8).

Как упростить работу с БД, не изучая SQL?

ORM-библиотеки вроде RedBeanPHP или Eloquent (через Illuminate/Database).


// RedBeanPHP
require 'rb.php';
R::setup('mysql:host=localhost;dbname=cms', 'root', '');

// Получить все статьи
$articles = R::findAll('articles', 'ORDER BY created_at DESC');

// Создать статью
$article = R::dispense('articles');
$article->title = 'Новая статья';
$article->content = 'Текст';
$id = R::store($article);
  

Типичные проблемы

  • Сложно отлаживать запросы - ORM генерирует SQL, который не очевиден.
  • Избыточное число запросов (N+1) при связях.
  • Зависимость от версии библиотеки, обновления могут ломать код.

Решение: использовать ORM осознанно, профилировать запросы, не прятать сложную логику.

Цель использования

Быстрая разработка прототипов, уменьшение шаблонного кода, автоматизация миграций.

- Php section 1 (раздел php с id=1)
- Section php id 2 (раздел php с id=2)
- Section 3 php id (раздел php с id=3)

Расширенные примеры реализации раздела управления контентом

1. Асинхронное добавление статьи через AJAX

Описание

Отправка формы без перезагрузки страницы, обработка на PHP, возврат JSON с результатом.

Пример

// save_article_ajax.php
require 'config/database.php';
require 'ArticleRepository.php';
$repo = new ArticleRepository($pdo);

$title = $_POST['title'] ?? '';
$content = $_POST['content'] ?? '';

if (!$title || !$content) {
    echo json_encode(['success' => false, 'message' => 'Заполните все поля']);
    exit;
}

$repo->create($title, $content);
echo json_encode(['success' => true, 'message' => 'Статья добавлена']);
Пример

// frontend.js (jQuery для краткости)
$('#articleForm').on('submit', function(e) {
    e.preventDefault();
    $.post('save_article_ajax.php', $(this).serialize(), function(response) {
        if (response.success) {
            alert('Статья сохранена');
            location.reload();
        } else {
            alert(response.message);
        }
    }, 'json');
});
// Ответ сервера
{"success":true,"message":"Статья добавлена"}

2. Загрузка и привязка изображений к статье

Описание

Сохранение загруженного файла, запись пути в БД, вывод в шаблоне.

Пример

// upload.php
if ($_FILES['image']['error'] === UPLOAD_ERR_OK) {
    $uploadDir = __DIR__ . '/uploads/';
    $fileName = uniqid() . '_' . basename($_FILES['image']['name']);
    $filePath = $uploadDir . $fileName;
    if (move_uploaded_file($_FILES['image']['tmp_name'], $filePath)) {
        // сохраняем путь к файлу в БД вместе с остальными данными статьи
        $imagePath = '/uploads/' . $fileName;
        // ... запись в таблицу articles (поле image_path)
    }
}
Пример

// В шаблоне Twig
<img src="{{ article.image_path }}" alt="{{ article.title }}" class="img-fluid">

Возможные проблемы

  • Проверка типа файла - не доверять расширению, использовать mime_content_type или finfo.
  • Безопасность - не допускать загрузки .php файлов, хранить за пределами document_root.
  • Размер файла - настроить upload_max_filesize и post_max_size в php.ini.

3. Генерация человекопонятных URL (ЧПУ) на основе заголовка

Описание

Транслитерация и создание slug, сохранение в БД, маршрутизация через mod_rewrite или PHP-роутер.

Пример

// Функция транслитерации
function translit($text) {
    $ru = ['а','б','в','г','д','е','ё','ж','з','и','й','к','л','м','н','о','п','р','с','т','у','ф','х','ц','ч','ш','щ','ъ','ы','ь','э','ю','я'];
    $en = ['a','b','v','g','d','e','yo','zh','z','i','y','k','l','m','n','o','p','r','s','t','u','f','h','ts','ch','sh','shch','','y','','e','yu','ya'];
    $text = str_replace($ru, $en, mb_strtolower($text, 'UTF-8'));
    $text = preg_replace('/[^a-z0-9]+/', '-', $text);
    $text = trim($text, '-');
    return $text;
}

$slug = translit($title);
// Проверить уникальность, добавить суффикс при совпадении
Пример

// .htaccess (Apache)
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^article/([a-z0-9-]+)$ article.php?slug=$1 [L,QSA]
Пример

// article.php
$slug = $_GET['slug'] ?? '';
$stmt = $pdo->prepare('SELECT * FROM articles WHERE slug = ?');
$stmt->execute([$slug]);
$article = $stmt->fetch();
// отображение

4. Кэширование вывода списка статей

Описание

Сохранение сгенерированного HTML в файл для ускорения загрузки, с очисткой кэша при изменении контента.

Пример

// CacheHelper.php
class CacheHelper {
    private string $cacheDir;

    public function __construct(string $cacheDir) {
        $this->cacheDir = rtrim($cacheDir, '/') . '/';
    }

    public function get(string $key): ?string {
        $file = $this->cacheDir . md5($key) . '.cache';
        if (file_exists($file) && filemtime($file) > time() - 3600) { // 1 час
            return file_get_contents($file);
        }
        return null;
    }

    public function set(string $key, string $content): void {
        file_put_contents($this->cacheDir . md5($key) . '.cache', $content);
    }

    public function clear(string $prefix = ''): void {
        $files = glob($this->cacheDir . md5($prefix) . '*');
        foreach ($files as $file) {
            unlink($file);
        }
    }
}

// Использование
$cache = new CacheHelper(__DIR__ . '/cache');
$html = $cache->get('article_list');
if (!$html) {
    ob_start();
    // вывод списка
    $html = ob_get_clean();
    $cache->set('article_list', $html);
}
echo $html;

Результат

При повторном запросе HTML берётся из кэша, нагрузка на БД снижается.

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

En
Section php lang (php)