Пока каталог существует только в переменной. Перезапустите PHP — и список исчезнет. Добавим сохранение в JSON-файл: формат читаем человеком и подходит для маленького личного каталога. Для сайта с несколькими пользователями и параллельными изменениями позже выберем базу данных.
Храним файл вне публичной папки
Создайте папку storage в корне проекта рядом с public, а не внутри неё. Тогда посетитель не сможет запросить файл напрямую через браузер. У PHP-процесса должны быть права на чтение и запись в этот каталог.
Преобразуем объекты в простой формат
JSON умеет хранить строки, числа, логические значения, массивы и null. Переведём объект книги в массив скаляров:
<?php
declare(strict_types=1);
use App\Book;
function bookToArray(Book $book): array
{
return [
'title' => $book->title(),
'author' => $book->author(),
'year' => $book->year(),
'status' => $book->status()->value,
];
}
Статус enum сохраняется как его строковое значение. Файл не содержит PHP-объектов или исполняемого кода, только данные.
Сохраняем с проверкой ошибки
function saveBooks(array $books, string $path): void
{
$rows = array_map('bookToArray', $books);
$json = json_encode(
$rows,
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR
);
if (file_put_contents($path, $json, LOCK_EX) === false) {
throw new RuntimeException('Не удалось сохранить каталог.');
}
}
JSON_UNESCAPED_UNICODE сохраняет кириллицу читаемой, а JSON_THROW_ON_ERROR превращает ошибку кодирования в исключение. LOCK_EX просит PHP взять исключительную блокировку на время записи, чтобы две записи не перезаписывали файл одновременно.
Это всё ещё простое файловое хранилище, а не транзакционная база данных. Блокировка защищает только конкретную операцию записи; файл не заменяет SQLite или серверную СУБД, если несколько запросов одновременно меняют сложные данные.
Читаем и восстанавливаем книги
use App\Book;
use App\ReadingStatus;
function loadBooks(string $path): array
{
if (!is_file($path)) {
return [];
}
$json = file_get_contents($path);
if ($json === false) {
throw new RuntimeException('Не удалось прочитать каталог.');
}
$rows = json_decode($json, true, flags: JSON_THROW_ON_ERROR);
if (!is_array($rows)) {
throw new UnexpectedValueException('Файл каталога имеет неверный формат.');
}
return array_map(
static fn (array $row): Book => new Book(
title: $row['title'],
author: $row['author'],
year: $row['year'],
status: ReadingStatus::from($row['status']),
),
$rows
);
}
json_decode() с флагом исключения сообщает о повреждённом JSON. При этом синтаксически корректный файл всё ещё может содержать неподходящие поля: для данных, которым нельзя доверять, проверьте наличие и тип каждого ключа, прежде чем создавать объект.
Используем постоянный путь
Не собирайте путь из данных запроса. Для учебного проекта задайте его в конфигурации приложения:
$storagePath = dirname(__DIR__) . '/storage/books.json';
Проверьте, что каталог существует, доступен на запись и остаётся вне public. Если файла ещё нет, loadBooks() возвращает пустой каталог — это нормальное состояние нового проекта.
В следующей части перестанем запускать код только из терминала и разберём, что веб-запрос приносит PHP и какой ответ получает браузер.
Навигация по курсу
Часть 10 из 16.
← Часть 9: Валидация и исключения PHP: отклоняем неверные данные · Часть 11: HTTP на PHP: разбираем запрос и формируем ответ →
0
8