Работа с файлами и JSON в PHP: безопасное чтение и запись

PHP / API

Работа с файлами и JSON в PHP: безопасное чтение и запись

Как читать и записывать локальные файлы, обрабатывать JSON с исключениями, блокировать конкурентную запись и защищаться от обхода путей.

Опубликовано 04.09.2026 6 минут чтения
Содержание статьи

Файлы подходят для конфигурации, кеша, экспорта и небольших наборов данных. Но файловая система не заменяет базу автоматически: конкурентная запись, права, резервные копии и поиск быстро усложняют решение.

В этой статье разберём локальный JSON-файл вне web-root. Пользователь не управляет абсолютным путём, чтение ограничено по размеру, JSON разбирается с исключениями, а запись защищается блокировкой.

Где хранить изменяемые файлы

Код
project/├── public/│   └── index.php├── src/└── storage/    └── settings.json​

Веб-сервер смотрит только в public/. storage/ должен быть доступен процессу PHP на чтение и, если нужно, запись, но не по HTTP.

Не храните изменяемые runtime-файлы рядом с исходным кодом и не выдавайте каталогу более широкие права, чем требуется.

Построение пути

PHP
<?php​declare(strict_types=1);​const STORAGE_ROOT = __DIR__ . '/../storage';​$settingsPath = STORAGE_ROOT . DIRECTORY_SEPARATOR . 'settings.json';​

Путь строится из серверной константы и фиксированного имени. Опасно:

PHP
// Не делайте так.$contents = file_get_contents(STORAGE_ROOT . '/' . $_GET['file']);​

Строка ../config/secret.php может выйти из разрешённого каталога. Лучше сопоставлять внешний идентификатор с фиксированным allowlist:

PHP
$allowedFiles = [    'settings' => STORAGE_ROOT . '/settings.json',    'navigation' => STORAGE_ROOT . '/navigation.json',];​$key = is_string($_GET['file'] ?? null) ? $_GET['file'] : '';$path = $allowedFiles[$key] ?? null;​if ($path === null) {    http_response_code(404);    exit('Файл не найден.');}​

Если продукту действительно нужны динамические имена, проверяйте их жёстким шаблоном, нормализуйте путь и убеждайтесь, что realpath() остаётся внутри разрешённого корня. Для нового файла realpath() ещё не существует, поэтому такой сценарий требует отдельной аккуратной логики.

Чтение файла

PHP
function readFileContents(string $path, int $maximumBytes = 1_048_576): string{    if (!is_file($path) || !is_readable($path)) {        throw new RuntimeException('Файл недоступен для чтения.');    }​    $size = filesize($path);​    if ($size === false || $size > $maximumBytes) {        throw new RuntimeException('Недопустимый размер файла.');    }​    $contents = file_get_contents($path);​    if ($contents === false) {        throw new RuntimeException('Не удалось прочитать файл.');    }​    return $contents;}​

file_get_contents() возвращает строку или false, поэтому строгое сравнение обязательно. Пустая строка является допустимым результатом и не равна ошибке.

Проверка размера защищает память только частично: между filesize() и чтением файл может измениться. Для недоверенных больших данных используйте потоковое чтение с лимитом, а не загрузку целиком.

Декодирование JSON

PHP
/** * @return array<string, mixed> */function readJsonObject(string $path): array{    $contents = readFileContents($path);​    try {        $data = json_decode(            $contents,            true,            512,            JSON_THROW_ON_ERROR,        );    } catch (JsonException $error) {        throw new RuntimeException('JSON-файл повреждён.', 0, $error);    }​    if (!is_array($data) || array_is_list($data)) {        throw new RuntimeException('Ожидался JSON-объект.');    }​    return $data;}​

Флаг JSON_THROW_ON_ERROR превращает ошибку синтаксиса в JsonException. Без него json_decode() может вернуть null, который невозможно отличить от корректного JSON null без дополнительной проверки.

Декодированный массив остаётся недоверенным. Проверьте каждое поле:

PHP
$data = readJsonObject($settingsPath);​$siteName = is_string($data['site_name'] ?? null)    ? trim($data['site_name'])    : '';​if ($siteName === '' || mb_strlen($siteName) > 100) {    throw new RuntimeException('Некорректное поле site_name.');}​

Кодирование JSON

PHP
function encodeJson(array $data): string{    try {        return json_encode(            $data,            JSON_PRETTY_PRINT                | JSON_UNESCAPED_UNICODE                | JSON_UNESCAPED_SLASHES                | JSON_THROW_ON_ERROR,        ) . PHP_EOL;    } catch (JsonException $error) {        throw new RuntimeException('Не удалось сформировать JSON.', 0, $error);    }}​

JSON_UNESCAPED_UNICODE делает русские строки читаемыми. JSON_THROW_ON_ERROR особенно полезен при недопустимых UTF-8 данных и неподдерживаемых значениях.

Не сериализуйте секреты в файл, который затем попадает в лог, архив или публичный экспорт.

Запись с эксклюзивной блокировкой

Простая запись:

PHP
$bytes = file_put_contents($settingsPath, encodeJson($data), LOCK_EX);​if ($bytes === false) {    throw new RuntimeException('Не удалось записать настройки.');}​

LOCK_EX просит эксклюзивную advisory-блокировку во время записи. Другие процессы тоже должны уважать flock(). Для чтения и записи согласованного файла можно сделать явные функции.

PHP
function writeLocked(string $path, string $contents): void{    $handle = fopen($path, 'c+b');​    if ($handle === false) {        throw new RuntimeException('Не удалось открыть файл для записи.');    }​    try {        if (!flock($handle, LOCK_EX)) {            throw new RuntimeException('Не удалось заблокировать файл.');        }​        if (!ftruncate($handle, 0) || !rewind($handle)) {            throw new RuntimeException('Не удалось подготовить файл.');        }​        $remaining = $contents;​        while ($remaining !== '') {            $written = fwrite($handle, $remaining);​            if ($written === false || $written === 0) {                throw new RuntimeException('Не удалось полностью записать файл.');            }​            $remaining = substr($remaining, $written);        }​        if (!fflush($handle)) {            throw new RuntimeException('Не удалось сбросить буфер файла.');        }    } finally {        flock($handle, LOCK_UN);        fclose($handle);    }}​

Блокировка уменьшает риск одновременной записи, но не превращает файл в транзакционную базу. Для критичных данных нужны резервные копии, проверка восстановления и подходящее хранилище.

Чтение с общей блокировкой

PHP
function readLocked(string $path): string{    $handle = fopen($path, 'rb');​    if ($handle === false) {        throw new RuntimeException('Не удалось открыть файл.');    }​    try {        if (!flock($handle, LOCK_SH)) {            throw new RuntimeException('Не удалось заблокировать файл для чтения.');        }​        $contents = stream_get_contents($handle);​        if ($contents === false) {            throw new RuntimeException('Не удалось прочитать файл.');        }​        return $contents;    } finally {        flock($handle, LOCK_UN);        fclose($handle);    }}​

Если все участники используют совместимые блокировки, читатель не увидит промежуточную запись.

Удаление и переименование

Перед unlink() или rename() разрешите точный абсолютный путь и проверьте, что операция относится к ожидаемому объекту. Не передавайте пользовательскую строку и glob напрямую в удаление.

Удаление важного файла должно быть авторизовано, журналироваться и по возможности оставаться восстановимым. Ошибка is_file() между проверкой и операцией возможна: файловая система меняется конкурентно, поэтому обрабатывайте результат самой операции.

Когда выбрать базу данных

Переходите к СУБД, если нужны:

  • несколько одновременных редакторов;
  • выборка и сортировка по полям;
  • связи и ограничения;
  • атомарное изменение нескольких записей;
  • история миграций;
  • надёжная конкурентная запись.

JSON удобен для небольшого цельного документа, который редко меняется. Большой массив сущностей в одном файле обычно становится хрупким.

Что запомнить

Храните runtime-файлы вне web-root, не принимайте путь от пользователя и ограничивайте размер. Разбирайте JSON с JSON_THROW_ON_ERROR, затем валидируйте структуру. При записи учитывайте конкурентный доступ, права и восстановление, а при росте требований переходите к базе данных.

Сообщить об ошибке в статье

Рекомендации редакции

Другие публикации из этого раздела.

Новые материалы

Получайте практические разборы без информационного шума

Одна общая рассылка о новых статьях и инструментах. Адрес будет активироваться только после подтверждения.

Подписка появится после подключения сервиса рассылок и Yandex SmartCaptcha.