Как создать блог на PHP и MongoDB
Блог на PHP и MongoDB можно собрать без фреймворка. В результате вы получите локальный проект с постами, комментариями, пагинацией и админкой с паролем. Статья ведет от установки MongoDB и расширения PHP до запуска готовых файлов. Все учетные данные хранятся в переменных окружения, а пользовательский ввод проходит проверку и экранирование.
- Что понадобится и как устроен проект
- Документы и коллекции MongoDB на примере блога
- Установка MongoDB
- Установка расширения mongodb для PHP и библиотеки
- Подключение к базе из PHP
- CRUD-операции на коллекции posts
- Класс для работы с постами
- Главная страница и пагинация
- Страница поста и комментарии
- Админка: создание, редактирование и удаление поста
- Запуск и проверка проекта
- Безопасность блога на PHP и MongoDB
- Что дальше
Что понадобится и как устроен проект
Установите PHP 8.1 или новее, Composer и MongoDB Community Server. Для базы подойдет локальная служба или Docker. PHP будет подключаться к MongoDB через расширение ext-mongodb и библиотеку mongodb/mongodb.
Библиотека mongodb/mongodb 2.5 требует PHP 8.1 или новее и ext-mongodb 2.5. В примере используется PHP 8.1–8.5, а сервер MongoDB работает локально на порту 27017.
Создайте каталог blog. Веб-сервер увидит только каталог public, поэтому конфигурация и зависимости останутся за пределами корня сайта.
blog/
├── bootstrap.php
├── Posts.php
├── composer.json
├── composer.lock
├── vendor/
└── public/
├── index.php
├── single.php
└── admin.phpВыполняйте команды из каталога blog. Команды php -v и composer --version должны вывести версии программ.
php -v
composer --versionЕсли оболочка не находит команду, добавьте каталог PHP или Composer в PATH. После изменения PATH откройте новое окно терминала.
Документы и коллекции MongoDB на примере блога
MongoDB хранит документы внутри коллекций. База blog будет содержать коллекцию posts, а каждый документ поста – заголовок, текст, даты и массив комментариев.
BSON расширяет возможности JSON типами ObjectId и датой. Поле _id идентифицирует документ. Если приложение не передает собственный идентификатор, MongoDB создает ObjectId при вставке.
Документ поста может выглядеть так:
{
_id: ObjectId("507f1f77bcf86cd799439011"),
title: "Первый пост",
content: "Текст с **Markdown**",
saved_at: ISODate(),
updated_at: ISODate(),
comments: [
{
name: "Читатель",
text: "Полезный пример",
posted_at: ISODate()
}
]
}Вложенные комментарии удобно получать вместе с постом. В учебном проекте массив ограничен 100 элементами. Для большого блога вынесите комментарии в отдельную коллекцию с полем post_id.
Подробности модели документов приведены в документации MongoDB о документах.
Установка MongoDB
Выберите один способ запуска базы. Docker быстрее подготовить для учебного проекта, Ubuntu подходит для обычной службы, а Windows – для работы с MSI-установщиком. Не запускайте два экземпляра на порту 27017.
| Способ | Когда выбрать | Проверка |
|---|---|---|
| Docker | Нужна изоляция проекта | mongosh внутри контейнера |
| Ubuntu | Нужна системная служба | systemctl status mongod |
| Windows | Нужен установщик MSI | подключение через PowerShell |
Запуск MongoDB в Docker
Установите Docker и запустите его движок. Команда создаст контейнер с именованным томом. Привязка к 127.0.0.1 оставит базу доступной только на вашем компьютере.
docker run --name blog-mongo -d -p 127.0.0.1:27017:27017 -v blog-mongo-data:/data/db mongo
docker exec blog-mongo mongosh --quiet --eval 'db.runCommand({ping: 1})'Ответ с ok: 1 означает, что сервер выполняет команды. Если контейнер завершился, посмотрите журнал командой docker logs blog-mongo.
Остановите контейнер командой docker stop blog-mongo. Для следующего запуска используйте docker start blog-mongo. Именованный том сохранит данные после остановки.
Контейнер запускается без аутентификации и подходит только для локальной разработки. Не публикуйте порт базы во внешнюю сеть.
Установка MongoDB в Ubuntu
Официальный репозиторий MongoDB 9.0 поддерживает Ubuntu 22.04 и 24.04. Для начала установите GnuPG и curl, затем сохраните ключ репозитория в отдельном keyring-файле.
sudo apt-get install -y gnupg curl
curl -fsSL https://pgp.mongodb.com/server-9.asc | sudo gpg -o /usr/share/keyrings/mongodb-server-9.gpg --dearmorДля Ubuntu 24.04 используйте ветку noble. В Ubuntu 22.04 замените ее на jammy.
echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-9.gpg ] https://repo.mongodb.org/apt/ubuntu noble/mongodb-org/9.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-9.0.list
sudo apt-get update
sudo apt-get install -y mongodb-orgЗапустите службу и включите ее запуск после перезагрузки:
sudo systemctl start mongod
sudo systemctl status mongod
sudo systemctl enable mongodСтатус службы должен показывать активное состояние. Проверьте запросом ping:
mongosh --quiet --eval 'db.runCommand({ping: 1})'Ответ ok: 1 подтверждает подключение. Если оболочка mongosh не установлена, скачайте ее с официальной страницы загрузки mongosh.
Установка MongoDB в Windows
Скачайте MSI-установщик MongoDB Community Server с официальной страницы MongoDB. В установщике включите запуск MongoDB как службы и задайте каталог данных, например C:\data\db.
Установите mongosh, откройте новое окно PowerShell и выполните команду:
mongosh "mongodb://127.0.0.1:27017" --quiet --eval 'db.runCommand({ping: 1})'Ответ ok: 1 подтверждает подключение. При ошибке соединения откройте список служб Windows и проверьте службу MongoDB. Docker остается запасным способом запуска с тем же URI.
Установка расширения mongodb для PHP и библиотеки
Сначала подключите ext-mongodb, затем добавьте библиотеку Composer. Такой порядок сразу показывает, готова ли среда PHP.
Расширение в Linux
Установите расширение через PECL. Для сборки нужны компилятор и заголовки установленного PHP.
sudo pecl install mongodb
php --iniВ загружаемый PHP-файл конфигурации добавьте строку, если установщик не сделал это сам:
extension=mongodbКоманда php --ini показывает конфигурацию CLI. Веб-сервер может использовать другой php.ini, поэтому его конфигурацию проверяют отдельно.
Расширение в Windows
Скачайте ZIP-архив подходящей сборки с официальной страницы релизов драйвера MongoDB для PHP. Выберите архив под версию PHP, архитектуру и режим Thread Safety. Из архива достаньте файл php_mongodb.dll.
Положите DLL в каталог extension_dir, который показывает команда php -i. В подключаемый php.ini добавьте:
extension=php_mongodb.dllПосле изменения конфигурации перезапустите PHP-процесс. Команда php -m должна вывести строку mongodb.
Зависимости Composer и проверка загрузки
Из каталога проекта установите библиотеку MongoDB и Markdown-парсер league/commonmark:
php -m
php --ri mongodb
composer require mongodb/mongodb league/commonmark
composer check-platform-reqsКоманда php --ri mongodb выведет сведения о расширении. Проверка Composer должна завершиться без ошибок требований платформы. Не используйте --ignore-platform-reqs, потому что этот флаг скрывает несовместимость.
Ошибка Class MongoDB\Client not found обычно связана с отсутствием автозагрузчика или библиотеки. Если CLI видит расширение, а веб-сервер нет, сравните их файлы конфигурации.
Подключение к базе из PHP
Класс MongoDB\Client принимает URI и параметры подключения. Пароли передавайте через переменные окружения, а не храните в PHP-файлах.
Создайте полный файл bootstrap.php в корне проекта:
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
require __DIR__ . '/Posts.php';
use League\CommonMark\CommonMarkConverter;
use MongoDB\Client;
function envValue(string $name, string $default = ''): string
{
$value = getenv($name);
return $value === false ? $default : $value;
}
function input(array $source, string $key): string
{
$value = $source[$key] ?? '';
if (!is_string($value)) {
http_response_code(400);
exit('Ожидалась строка.');
}
return trim($value);
}
function e(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function fail(int $status, string $message): void
{
http_response_code($status);
exit($message);
}
function redirectTo(string $path): void
{
header('Location: ' . $path, true, 303);
exit;
}
function csrfField(): string
{
return '<input type="hidden" name="csrf" value="'
. e($_SESSION['csrf']) . '">';
}
function checkCsrf(): void
{
if (!hash_equals($_SESSION['csrf'], input($_POST, 'csrf'))) {
fail(403, 'Неверный CSRF-токен. Обновите страницу формы.');
}
}
function dateText(MongoDB\BSON\UTCDateTime $date): string
{
return $date->toDateTime()->format('d.m.Y H:i') . ' UTC';
}
if (PHP_SAPI !== 'cli') {
ini_set('session.use_strict_mode', '1');
session_set_cookie_params([
'httponly' => true,
'samesite' => 'Lax',
'secure' => !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off',
]);
session_start();
if (!isset($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
header('Content-Type: text/html; charset=UTF-8');
}
$options = [];
if (envValue('MONGODB_USER') !== '') {
$options = [
'username' => envValue('MONGODB_USER'),
'password' => envValue('MONGODB_PASSWORD'),
'authSource' => envValue('MONGODB_AUTH_SOURCE', 'admin'),
];
}
$client = new Client(
envValue('MONGODB_URI', 'mongodb://127.0.0.1:27017'),
$options,
['typeMap' => [
'root' => 'array', 'document' => 'array', 'array' => 'array',
]]
);
$db = $client->selectDatabase(envValue('MONGODB_DATABASE', 'blog'));
$posts = new Posts($db->selectCollection('posts'));
$markdown = new CommonMarkConverter([
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);Локальный сервер без авторизации использует URI по умолчанию. Для защищенной базы задайте MONGODB_USER, MONGODB_PASSWORD и MONGODB_AUTH_SOURCE в окружении PHP.
Проверьте соединение после создания Posts.php:
php -r 'require "bootstrap.php"; var_export($db->command(["ping" => 1])->toArray());'В результате появится значение ok со значением 1.
CRUD-операции на коллекции posts
CRUD MongoDB PHP включает создание, чтение, обновление и удаление документов. Библиотека использует методы insertOne, find, findOne, updateOne и deleteOne.
Небольшой скрипт показывает полный цикл. Сохраните его как crud-demo.php в корне проекта:
<?php
declare(strict_types=1);
require __DIR__ . '/bootstrap.php';
use MongoDB\BSON\ObjectId;
use MongoDB\BSON\UTCDateTime;
$collection = $db->selectCollection('posts');
$now = new UTCDateTime();
$result = $collection->insertOne([
'title' => 'Проверка CRUD',
'content' => 'Тестовый текст',
'saved_at' => $now,
'updated_at' => $now,
'comments' => [],
]);
$id = new ObjectId((string) $result->getInsertedId());
$post = $collection->findOne(['_id' => $id]);
echo $post['title'], PHP_EOL;
$updated = $collection->updateOne(
['_id' => $id],
['$set' => ['title' => 'Обновлено', 'updated_at' => new UTCDateTime()]]
);
echo 'Найдено: ', $updated->getMatchedCount(), PHP_EOL;
$deleted = $collection->deleteOne(['_id' => $id]);
echo 'Удалено: ', $deleted->getDeletedCount(), PHP_EOL;Запустите php crud-demo.php. Терминал покажет заголовок, число найденных и число удаленных документов.
Оператор $set меняет выбранные поля и сохраняет остальные. Для дат используйте UTCDateTime. Строковый идентификатор сначала проверьте по формату, затем передайте в ObjectId.
Класс для работы с постами
Создайте файл Posts.php рядом с bootstrap.php. Класс содержит методы списка, поиска, создания, обновления, удаления и добавления комментария.
<?php
declare(strict_types=1);
use MongoDB\BSON\ObjectId;
use MongoDB\BSON\UTCDateTime;
use MongoDB\Collection;
final class Posts
{
public function __construct(private Collection $collection) {}
private function objectId(string $id): ?ObjectId
{
return preg_match('/\A[0-9a-f]{24}\z/i', $id)
? new ObjectId($id) : null;
}
public function page(int $page, int $limit = 5): array
{
$limit = max(1, min(50, $limit));
$total = $this->collection->countDocuments([]);
$pages = max(1, (int) ceil($total / $limit));
$page = max(1, min($page, $pages));
$cursor = $this->collection->find([], [
'sort' => ['saved_at' => -1, '_id' => -1],
'skip' => ($page - 1) * $limit,
'limit' => $limit,
]);
return [
'items' => $cursor->toArray(),
'page' => $page,
'pages' => $pages,
'total' => $total,
];
}
public function byId(string $id): ?array
{
$objectId = $this->objectId($id);
return $objectId === null ? null : $this->collection->findOne(['_id' => $objectId]);
}
public function create(string $title, string $content): string
{
$now = new UTCDateTime();
$result = $this->collection->insertOne([
'title' => $title, 'content' => $content,
'saved_at' => $now, 'updated_at' => $now, 'comments' => [],
]);
return (string) $result->getInsertedId();
}
public function update(string $id, string $title, string $content): bool
{
$objectId = $this->objectId($id);
if ($objectId === null) return false;
return $this->collection->updateOne(
['_id' => $objectId],
['$set' => [
'title' => $title, 'content' => $content,
'updated_at' => new UTCDateTime(),
]]
)->getMatchedCount() === 1;
}
public function delete(string $id): bool
{
$objectId = $this->objectId($id);
return $objectId !== null
&& $this->collection->deleteOne(['_id' => $objectId])->getDeletedCount() === 1;
}
public function addComment(string $id, string $name, string $text): bool
{
$objectId = $this->objectId($id);
if ($objectId === null) return false;
return $this->collection->updateOne(
['_id' => $objectId, 'comments.99' => ['$exists' => false]],
['$push' => ['comments' => [
'name' => $name, 'text' => $text,
'posted_at' => new UTCDateTime(),
]]]
)->getModifiedCount() === 1;
}
}Метод page считает документы через countDocuments, а курсор преобразует в массив один раз. Условие comments.99 не позволяет добавить 101-й комментарий.
Главная страница и пагинация
Создайте файл public/index.php. Список использует сортировку, пропуск документов и лимит страницы.
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/bootstrap.php';
$data = $posts->page(max(1, (int) input($_GET, 'page')));
?>
<!doctype html>
<html lang="ru">
<meta charset="UTF-8">
<title>Блог на PHP и MongoDB</title>
<body>
<header><h1>Блог</h1><a href="/admin.php">Админка</a></header>
<main>
<p>Постов: <?= $data['total'] ?></p>
<?php if (!$data['items']): ?>
<p>Пока нет постов. Создайте первый в админке.</p>
<?php endif; ?>
<?php foreach ($data['items'] as $post): ?>
<article>
<h2><a href="/single.php?id=<?= e((string) $post['_id']) ?>"><?= e($post['title']) ?></a></h2>
<p><?= e(dateText($post['saved_at'])) ?></p>
</article>
<?php endforeach; ?>
<nav aria-label="Страницы постов">
<?php if ($data['page'] > 1): ?><a href="/?page=<?= $data['page'] - 1 ?>">Назад</a><?php endif; ?>
<span><?= $data['page'] ?> из <?= $data['pages'] ?></span>
<?php if ($data['page'] < $data['pages']): ?><a href="/?page=<?= $data['page'] + 1 ?>">Вперед</a><?php endif; ?>
</nav>
</main>
</body>
</html>Метод countDocuments считает общее число записей отдельно от курсора. Поэтому курсор не приходится перебирать дважды. Второе поле сортировки _id задает стабильный порядок при одинаковом времени.
Страница поста и комментарии
Создайте public/single.php. Файл показывает Markdown-текст, комментарии и форму. После успешного сохранения применяется редирект.
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/bootstrap.php';
$id = input($_GET, 'id');
$post = $posts->byId($id);
if ($post === null) fail(404, 'Пост не найден или идентификатор имеет неверный формат.');
$error = '';
$name = '';
$text = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
checkCsrf();
$name = input($_POST, 'name');
$text = input($_POST, 'text');
if ($name === '' || $text === '') {
$error = 'Заполните имя и комментарий.';
} elseif (strlen($name) > 200 || strlen($text) > 4000) {
$error = 'Имя ограничено 200 байтами, комментарий – 4000.';
} elseif (!$posts->addComment($id, $name, $text)) {
$error = 'Пост удален или достигнут лимит в 100 комментариев.';
} else {
redirectTo('/single.php?id=' . rawurlencode($id));
}
}
?>
<!doctype html>
<html lang="ru">
<meta charset="UTF-8">
<title><?= e($post['title']) ?></title>
<body>
<p><a href="/">Все посты</a></p>
<main>
<article>
<h1><?= e($post['title']) ?></h1>
<p><?= e(dateText($post['saved_at'])) ?></p>
<div><?= $markdown->convert($post['content']) ?></div>
</article>
<section>
<h2>Комментарии</h2>
<?php foreach ($post['comments'] as $comment): ?>
<article><h3><?= e($comment['name']) ?></h3><p><?= e(dateText($comment['posted_at'])) ?></p><p><?= nl2br(e($comment['text'])) ?></p></article>
<?php endforeach; ?>
<p role="alert"><?= e($error) ?></p>
<form method="post">
<?= csrfField() ?>
<p><label>Имя <input name="name" value="<?= e($name) ?>" required></label></p>
<p><label>Комментарий <textarea name="text" required><?= e($text) ?></textarea></label></p>
<button type="submit">Добавить комментарий</button>
</form>
</section>
</main>
</body>
</html>Неверный или несуществующий ObjectId возвращает HTTP-статус 404. После отправки формы браузер получает адрес страницы заново, поэтому обновление не дублирует комментарий.
Админка: создание, редактирование и удаление поста
Админка использует пароль, хеш, сессию и CSRF-токены. Сначала создайте хеш в терминале:
php -r 'echo password_hash(rtrim(fgets(STDIN), "\r\n"), PASSWORD_DEFAULT), PHP_EOL;'Передайте хеш PHP-процессу. В Bash используйте одинарные кавычки:
export ADMIN_PASSWORD_HASH='ВСТАВЬТЕ_ПОЛУЧЕННЫЙ_ХЕШ'
export MONGODB_URI='mongodb://127.0.0.1:27017'
export MONGODB_DATABASE='blog'В PowerShell задайте переменные так:
$env:ADMIN_PASSWORD_HASH = 'ВСТАВЬТЕ_ПОЛУЧЕННЫЙ_ХЕШ'
$env:MONGODB_URI = 'mongodb://127.0.0.1:27017'
$env:MONGODB_DATABASE = 'blog'Запускайте сервер в том же терминале. Исходный пароль в проекте не хранится.
Создайте файл public/admin.php:
<?php
declare(strict_types=1);
require dirname(__DIR__) . '/bootstrap.php';
$hash = envValue('ADMIN_PASSWORD_HASH');
if ($hash === '') fail(503, 'Задайте ADMIN_PASSWORD_HASH перед запуском PHP.');
$error = '';
$isPost = $_SERVER['REQUEST_METHOD'] === 'POST';
$action = $isPost ? input($_POST, 'action') : '';
if ($isPost) checkCsrf();
if ($isPost && $action === 'login') {
if (password_verify(input($_POST, 'password'), $hash)) {
session_regenerate_id(true);
$_SESSION['admin'] = true;
$_SESSION['csrf'] = bin2hex(random_bytes(32));
redirectTo('/admin.php');
}
$error = 'Неверный пароль.';
}
if (($_SESSION['admin'] ?? false) !== true):
?>
<!doctype html>
<html lang="ru"><meta charset="UTF-8"><title>Вход в админку</title><body>
<h1>Вход</h1><p role="alert"><?= e($error) ?></p>
<form method="post"><?= csrfField() ?><input type="hidden" name="action" value="login">
<label>Пароль <input type="password" name="password" autocomplete="current-password" required></label>
<button type="submit">Войти</button></form>
<p><a href="/">На главную</a></p>
</body></html>
<?php exit; endif;
if ($isPost && $action === 'logout') {
$_SESSION = [];
session_regenerate_id(true);
redirectTo('/admin.php');
}
$id = input($_GET, 'id');
$title = '';
$content = '';
if ($id !== '') {
$post = $posts->byId($id);
if ($post === null) fail(404, 'Пост не найден.');
$title = $post['title'];
$content = $post['content'];
}
if ($isPost && $action === 'delete') {
if (!$posts->delete(input($_POST, 'id'))) fail(404, 'Пост не найден.');
redirectTo('/admin.php');
}
if ($isPost && $action === 'save') {
$title = input($_POST, 'title');
$content = input($_POST, 'content');
if ($title === '' || $content === '') {
$error = 'Заполните заголовок и текст.';
} elseif (strlen($title) > 500 || strlen($content) > 100000) {
$error = 'Заголовок ограничен 500 байтами, текст – 100000 байтами.';
} elseif ($id === '') {
$id = $posts->create($title, $content);
redirectTo('/single.php?id=' . rawurlencode($id));
} elseif (!$posts->update($id, $title, $content)) {
fail(404, 'Пост не найден.');
} else {
redirectTo('/single.php?id=' . rawurlencode($id));
}
}
$data = $posts->page(max(1, (int) input($_GET, 'page')));
?>
<!doctype html>
<html lang="ru"><meta charset="UTF-8"><title>Админка блога</title><body>
<h1>Админка</h1>
<p><a href="/">Блог</a> · <a href="/admin.php">Новый пост</a></p>
<form method="post" action="/admin.php"><?= csrfField() ?><button name="action" value="logout">Выйти</button></form>
<h2><?= $id === '' ? 'Создать пост' : 'Редактировать пост' ?></h2>
<p role="alert"><?= e($error) ?></p>
<form method="post"><?= csrfField() ?><input type="hidden" name="action" value="save">
<p><label>Заголовок <input name="title" value="<?= e($title) ?>" required></label></p>
<p><label>Текст Markdown <textarea name="content" rows="12" required><?= e($content) ?></textarea></label></p>
<button type="submit">Сохранить</button></form>
<h2>Посты</h2>
<?php foreach ($data['items'] as $item): ?>
<article><h3><?= e($item['title']) ?></h3>
<p><a href="/admin.php?id=<?= e((string) $item['_id']) ?>">Редактировать</a></p>
<form method="post" action="/admin.php"><?= csrfField() ?><input type="hidden" name="id" value="<?= e((string) $item['_id']) ?>"><button name="action" value="delete">Удалить без восстановления</button></form></article>
<?php endforeach; ?>
<nav aria-label="Страницы админки"><?php if ($data['page'] > 1): ?><a href="/admin.php?page=<?= $data['page'] - 1 ?>">Назад</a><?php endif; ?> <span><?= $data['page'] ?> из <?= $data['pages'] ?></span> <?php if ($data['page'] < $data['pages']): ?><a href="/admin.php?page=<?= $data['page'] + 1 ?>">Вперед</a><?php endif; ?></nav>
</body></html>Форма входа вызывает password_verify. После успешного входа PHP меняет идентификатор сессии. Все формы изменения данных содержат CSRF-токен.
Запуск и проверка проекта
Запустите встроенный сервер из корня проекта в том же терминале, где заданы переменные окружения:
php -S 127.0.0.1:8000 -t publicОткройте в браузере http://127.0.0.1:8000. В админке войдите с выбранным паролем и создайте пост с текстом Привет, **MongoDB**!. На странице поста слово MongoDB выделится жирным.
- Измените заголовок поста и сохраните форму.
- Добавьте комментарий на странице поста.
- Создайте еще 5 постов и откройте вторую страницу списка.
- Удалите тестовый пост через админку.
- Нажмите «Выйти» и снова откройте админку.
Удаление поста удаляет его вложенные комментарии. Для остановки встроенного сервера нажмите Ctrl+C.
Безопасность блога на PHP и MongoDB
Экранируйте пользовательский текст функцией htmlspecialchars. Комментарии проходят экранирование до добавления переносов строк. Заголовки и значения форм не могут превратиться в HTML-разметку.
Markdown-парсер получает настройки html_input=strip и allow_unsafe_links=false. Эти параметры удаляют встроенный HTML и запрещают опасные ссылки. Выводите без экранирования только результат безопасного преобразования Markdown.
Приводите входные параметры к ожидаемому типу. Функция input отклоняет массивы, поэтому запрос id[$ne]=x получает HTTP-статус 400. Не передавайте $_GET или $_POST целиком в фильтры MongoDB.
- CSRF-токен защищает формы входа, создания, редактирования, удаления и комментариев.
password_hashхранит пароль в виде хеша.HttpOnlyзакрывает сессионную cookie для JavaScript.- Смена идентификатора сессии после входа снижает риск фиксации сессии.
- Публичный сервер должен использовать HTTPS и отдельного пользователя MongoDB.
Перед публикацией добавьте ограничение частоты запросов и модерацию комментариев. Не открывайте порт MongoDB во внешнюю сеть и не обслуживайте публичный сайт встроенным сервером PHP.
Что дальше
Добавьте составной индекс под сортировку списка:
use blog
db.posts.createIndex({saved_at: -1, _id: -1})
db.posts.getIndexes()Для поиска по заголовку и тексту создайте текстовый индекс:
db.posts.createIndex({title: "text", content: "text"}, {default_language: "russian"})
db.posts.find({$text: {$search: "база данных"}})Глубокий skip на больших списках можно заменить пагинацией по последнему ключу сортировки. При росте проекта вынесите комментарии в отдельную коллекцию, а маршруты и шаблоны перенесите во фреймворк.
Совет от нашей редакции: сначала пройдите полный цикл на тестовом посте – создайте запись, измените ее, добавьте комментарий и удалите документ. Такой сценарий быстро показывает ошибки подключения и обработки форм.
Популярное за неделю
- Как создать сайт самому в 2026: 4 способа + пошаговая инструкция 34 мин чтения
- Лучшие конструкторы для создания сайтов — плюсы, минусы и сравнения 22 мин чтения
- Регулярные выражения PHP: синтаксис и примеры 10 мин чтения
- Как создать формы регистрации и авторизации на PHP 5 мин чтения
- PHP переход на другую страницу: серверный и клиентский редирект 5 мин чтения
- Адаптивный помесячный календарь на AJAX и PHP 2 мин чтения
Комментарии
Статья полезная, но за ссылку добавляемую в буфер обмена при каждом копировании, гореть Вам в аду