Разработка блога на PHP: варианты реализации
Обзор подходов к созданию блога на PHP
Создание блога на PHP может быть реализовано разными способами. В этой статье рассматриваются несколько вариантов, от самого простого до профессионального. Для каждого варианта приведены цели использования, примеры кода, типичные проблемы и их решения.
Основное решение: микрофреймворк Slim с PDO и Twig
Этот подход сочетает простоту настройки, безопасность и расширяемость. Идеально подходит для небольших и средних проектов, где не требуется полная функциональность крупных фреймворков.
Как создать блог с маршрутизацией, базой данных и шаблонами без излишней сложности?
Установка через Composer:
composer require slim/slim slim/psr7 slim/twig-viewБаза данных PostgreSQL используется через PDO. Пример контроллера для отображения статей:
<?php
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Factory\AppFactory;
require __DIR__ . '/../vendor/autoload.php';
$app = AppFactory::create();
// Настройка контейнера с PDO
$container = $app->getContainer();
$container->set('db', function () {
$pdo = new PDO('pgsql:host=localhost;dbname=blog', 'user', 'pass');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
return $pdo;
});
// Маршрут для списка статей
$app->get('/articles', function (Request $request, Response $response) {
$db = $this->get('db');
$stmt = $db->query('SELECT id, title FROM articles ORDER BY created_at DESC');
$articles = $stmt->fetchAll(PDO::FETCH_ASSOC);
$view = \Slim\Views\Twig::fromRequest($request);
return $view->render($response, 'articles.twig', ['articles' => $articles]);
});
$app->run();Шаблон articles.twig с выводом списка:
<ul>
{% for article in articles %}
<li><a href="/article/{{ article.id }}">{{ article.title }}</a></li>
{% endfor %}
</ul>Типичные ошибки и решения
- Ошибка соединения с БД: проверьте настройки DSN, имя пользователя и пароль.
- Отсутствие Autowiring в Twig: убедитесь, что установлен пакет slim/twig-view и зарегистрирован middleware.
- Медленная работа при большом количестве статей: добавьте индексы в БД и используйте пагинацию.
Вариант 1: Файловый блог на markdown
Цель: максимально быстрый старт без базы данных. Подходит для личного блога с малым числом записей или для обучения.
Как сделать блог без использования MySQL? Статьи хранятся в файлах .md, извлекаются через чтение директории.
<?php
function getArticles() {
$files = glob('articles/*.md');
$articles = [];
foreach ($files as $file) {
$content = file_get_contents($file);
// Парсинг заголовка из первой строки (## Заголовок)
preg_match('/^## (.+)$/m', $content, $matches);
$title = $matches[1] ?? basename($file, '.md');
$slug = basename($file, '.md');
$articles[] = ['title' => $title, 'slug' => $slug, 'content' => $content];
}
return $articles;
}
$articles = getArticles();
foreach ($articles as $article): ?>
<h3><?= htmlspecialchars($article['title']) ?></h3>
<div><?= Parsedown::instance()->text($article['content']) ?></div>
<?php endforeach; ?>Проблемы и решения
- Отсутствие поиска: реализуйте grep-подобную фильтрацию через PHP.
- Сложность управления большим числом файлов: введите категории через поддиректории.
- Нет системы пользователей: если требуется админка, придется добавить аутентификацию.
Вариант 2: Чистый PHP с MySQL (без фреймворка)
Цель: полный контроль над кодом, понимание основ. Хорошо для обучения архитектуре приложений.
Как написать блог без сторонних библиотек, используя только PDO и простую маршрутизацию?
<?php
$pdo = new PDO('mysql:host=localhost;dbname=blog', 'root', '');
// Маршрутизация через REQUEST_URI
$request = $_SERVER['REQUEST_URI'];
if ($request === '/articles') {
$stmt = $pdo->query('SELECT * FROM articles');
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
echo '<h3>' . htmlspecialchars($row['title']) . '</h3>';
echo '<p>' . substr($row['content'], 0, 200) . '...</p>';
}
} elseif (preg_match('#^/article/(\d+)$#', $request, $matches)) {
$id = (int)$matches[1];
$stmt = $pdo->prepare('SELECT * FROM articles WHERE id = ?');
$stmt->execute([$id]);
$article = $stmt->fetch(PDO::FETCH_ASSOC);
if ($article) {
echo '<h1>' . htmlspecialchars($article['title']) . '</h1>';
echo '<div>' . nl2br(htmlspecialchars($article['content'])) . '</div>';
} else {
http_response_code(404);
echo 'Статья не найдена';
}
} else {
http_response_code(404);
echo 'Страница не найдена';
}Типичные ошибки
- SQL-инъекции: при привязке параметров обязательно использовать prepare.
- Дублирование кода: вынесите общие части в функции.
- Сложность добавления новых страниц: код маршрутизации становится запутанным при росте проекта.
Вариант 3: Laravel (полноценный фреймворк)
Цель: быстрая разработка масштабируемого блога с готовыми решениями (аутентификация, админка, миграции). Подходит для профессиональных проектов.
Как построить блог на Laravel с использованием Eloquent и Blade?
// Маршрут в web.php
Route::get('/posts', [PostController::class, 'index']);
// Контроллер
public function index() {
$posts = Post::orderBy('created_at', 'desc')->paginate(10);
return view('posts.index', compact('posts'));
}
// Blade-шаблон resources/views/posts/index.blade.php
@foreach ($posts as $post)
<h3>{{ $post->title }}</h3>
<p>{{ Str::limit($post->body, 150) }}</p>
<a href="{{ route('posts.show', $post) }}">Читать далее</a>
@endforeach
{{ $posts->links() }}Типичные сложности
- Перегрузка функциями: Laravel может быть избыточен для микроблога.
- Конфигурация окружения: требуется правильно настроить .env, кеш, composer.
- Производительность: используйте кэширование запросов и очереди для тяжелых операций.
Вариант 4: API-блог на Slim (RESTful)
Цель: отдельный бэкенд для SPA или мобильного приложения. Позволяет полностью разделить фронтенд и бэкенд, использовать любую клиентскую технологию.
Как создать REST API для блога с поддержкой CRUD на Slim?
use Slim\Psr7\Response;
$app->get('/api/posts', function ($request, $response) {
$db = $this->get('db');
$posts = $db->query('SELECT id, title, created_at FROM posts')->fetchAll(PDO::FETCH_ASSOC);
$payload = json_encode($posts);
$response->getBody()->write($payload);
return $response->withHeader('Content-Type', 'application/json');
});
$app->post('/api/posts', function ($request, $response) {
$data = $request->getParsedBody();
$db = $this->get('db');
$stmt = $db->prepare('INSERT INTO posts (title, body) VALUES (?, ?)');
$stmt->execute([$data['title'], $data['body']]);
$response->getBody()->write(json_encode(['id' => $db->lastInsertId()]));
return $response->withStatus(201);
});Проблемы при разработке API
- Отсутствие валидации: добавьте библиотеку respect/validation.
- Управление CORS: настройте middleware для разрешения запросов с других доменов.
- Документирование: используйте Swagger/OpenAPI для описания эндпоинтов.
Дополнительные примеры кода для блога на PHP
Пример 1: Работа с PDO - полный CRUD для статей
Создание, чтение, обновление и удаление статей через PDO с обработкой исключений.
<?php
class ArticleModel {
private $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 id DESC');
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
public function getById(int $id): ?array {
$stmt = $this->pdo->prepare('SELECT * FROM articles WHERE id = ?');
$stmt->execute([$id]);
$article = $stmt->fetch(PDO::FETCH_ASSOC);
return $article ?: null;
}
public function create(string $title, string $body): int {
$stmt = $this->pdo->prepare('INSERT INTO articles (title, body) VALUES (?, ?)');
$stmt->execute([$title, $body]);
return (int)$this->pdo->lastInsertId();
}
public function update(int $id, string $title, string $body): bool {
$stmt = $this->pdo->prepare('UPDATE articles SET title = ?, body = ? WHERE id = ?');
return $stmt->execute([$title, $body, $id]);
}
public function delete(int $id): bool {
$stmt = $this->pdo->prepare('DELETE FROM articles WHERE id = ?');
return $stmt->execute([$id]);
}
}// Пример использования:
$model = new ArticleModel($pdo);
$articles = $model->getAll();
foreach ($articles as $article) {
echo $article['title'] . "\n";
}
// Результат: вывод заголовков всех статей.Пример 2: Маршрутизация и middleware в Slim
Демонстрация обработки ошибок, CORS и JSON-ответов.
<?php
use Slim\Factory\AppFactory;
use Slim\Middleware\ErrorMiddleware;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
require __DIR__ . '/vendor/autoload.php';
$app = AppFactory::create();
// Middleware для CORS
$app->add(function (Request $request, $handler) {
$response = $handler->handle($request);
return $response
->withHeader('Access-Control-Allow-Origin', '*')
->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
});
// Обработка JSON запросов
$app->addBodyParsingMiddleware();
// Маршрут с JSON ответом
$app->get('/api/version', function (Request $request, Response $response) {
$data = ['version' => '1.0', 'status' => 'ok'];
$response->getBody()->write(json_encode($data));
return $response->withHeader('Content-Type', 'application/json');
});
// Группа маршрутов для статей
$app->group('/api/articles', function ($group) {
$group->get('', 'ArticleController:list');
$group->post('', 'ArticleController:create');
$group->put('/{id}', 'ArticleController:update');
$group->delete('/{id}', 'ArticleController:delete');
});
// Кастомный обработчик ошибок
$errorMiddleware = $app->addErrorMiddleware(true, true, true);
$errorMiddleware->setDefaultErrorHandler(function (Request $request, Throwable $e) use ($app) {
$response = $app->getResponseFactory()->createResponse();
$payload = ['error' => $e->getMessage()];
$response->getBody()->write(json_encode($payload));
return $response->withStatus(500)->withHeader('Content-Type', 'application/json');
});
$app->run();curl http://localhost:8080/api/version
// Ответ: {"version":"1.0","status":"ok"}Пример 3: Шаблоны Twig - наследование и фрагменты
Создание базового layout и фрагментов для повторного использования.
{# base.twig #}
<!DOCTYPE html>
<html>
<head>...
</head>
<body>
<header>{% include 'menu.twig' %}</header>
<main>{% block content %}{% endblock %}</main>
<footer>...</footer>
</body>
</html>
{# article.twig #}
{% extends 'base.twig' %}
{% block content %}
<h3>{{ article.title }}</h3>
<p>{{ article.body|nl2br }}</p>
{% endblock %}// При вызове Twig -> render('article.twig', ['article' => $article])
// формируется полная HTML-страница с обёрткой.Пример 4: Использование Eloquent в Slim (через illuminate/database)
Интеграция ORM Laravel в микрофреймворк для удобной работы с отношениями.
<?php
use Illuminate\Database\Capsule\Manager as Capsule;
$capsule = new Capsule;
$capsule->addConnection([
'driver' => 'mysql',
'host' => 'localhost',
'database' => 'blog',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
]);
$capsule->setAsGlobal();
$capsule->bootEloquent();
// Определение модели
class Article extends \Illuminate\Database\Eloquent\Model {
protected $table = 'articles';
protected $fillable = ['title', 'body'];
public function comments() {
return $this->hasMany(Comment::class);
}
}
// Использование в контроллере
$app->get('/api/articles/{id}', function ($request, $response, $args) {
$article = Article::with('comments')->find($args['id']);
if (!$article) {
$response->getBody()->write(json_encode(['error' => 'Not found']));
return $response->withStatus(404);
}
$response->getBody()->write($article->toJson());
return $response->withHeader('Content-Type', 'application/json');
});GET /api/articles/1
Ответ: {"id":1,"title":"...","body":"...","comments":[...]}