Класс-репозиторий
Запросы SQL, разбросанные по действиям, трудно находить и менять. Их собирают в один класс — репозиторий: у него методы вроде «все статьи» и «статья по номеру», а остальной код не знает, что за ними стоит SQL. Этот класс заменяет модель из больших фреймворков.
Где лежит
Репозитории кладут в src/Repository/. Класс PostRepository там уже есть — с массивом вместо базы. Перепишите его.
Чтение списка
// src/Repository/PostRepository.php
namespace App\Repository;
use PDO;
final class PostRepository
{
public function __construct(private PDO $pdo) {}
public function all(): array
{
return $this->pdo
->query('SELECT id, title, created_at FROM posts ORDER BY id DESC')
->fetchAll();
}
}Объект PDO приходит через конструктор: контейнер создаёт его по описанию из прошлого урока. Действие ListPostsAction менять не нужно — оно по-прежнему вызывает all(). Откройте /posts: список теперь приходит из базы.
Одна запись
Значения от пользователя никогда не вклеивают в текст запроса. Вместо них ставят знак вопроса, а значение передают отдельно — это подготовленный запрос:
// src/Repository/PostRepository.php
public function find(int $id): ?array
{
$statement = $this->pdo->prepare('SELECT * FROM posts WHERE id = ?');
$statement->execute([$id]);
return $statement->fetch() ?: null;
}Метод возвращает массив с полями статьи или null, если строки с таким номером нет.
Использование в действии
// src/Action/ShowPostAction.php
namespace App\Action;
use App\Repository\PostRepository;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Slim\Exception\HttpNotFoundException;
use Slim\Views\Twig;
final class ShowPostAction
{
public function __construct(private PostRepository $posts, private Twig $twig) {}
public function __invoke(Request $request, Response $response, array $args): Response
{
$post = $this->posts->find((int) $args['id']);
if ($post === null) {
throw new HttpNotFoundException($request);
}
return $this->twig->render($response, 'posts/show.twig', ['post' => $post]);
}
}{# templates/posts/show.twig #}
{% extends 'layout.twig' %}
{% block content %}
<h1>{{ post.title }}</h1>
<p>{{ post.body }}</p>
{% endblock %}Адрес /posts/1 покажет статью, а /posts/999 — страницу 404: исключение HttpNotFoundException перехватывает посредник ошибок.
Добавление записи
// src/Repository/PostRepository.php
public function create(string $title, string $body): int
{
$statement = $this->pdo->prepare('INSERT INTO posts (title, body) VALUES (?, ?)');
$statement->execute([$title, $body]);
return (int) $this->pdo->lastInsertId();
}Метод возвращает номер новой строки. Он понадобится в уроке о формах.
Готовые библиотеки
Когда запросов становится много, вместо ручного SQL подключают библиотеку. Eloquent, ORM из Laravel, ставится пакетом illuminate/database и работает отдельно от Laravel. Doctrine DBAL (doctrine/dbal) даёт построитель запросов без моделей. Обе подключают так же, как PDO: описывают в контейнере и получают через конструктор.
Частая ошибка
Запрос вида "SELECT * FROM posts WHERE id = " . $args['id'] открывает путь SQL-инъекции — посетитель может дописать к запросу свой SQL. Ограничение маршрута [0-9]+ не спасает: завтра его могут убрать. Любое значение извне передавайте через prepare() и execute().