Разработка блога на PHP: варианты реализации

Раздел: Разработка на 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":[...]}

Блоги на PHP - comments

En
Blogs php blog (php)