Работа с файловой системой: запись и сохранение данных в PHP

Раздел: Ввод-вывод в PHP -> Чтение и запись файлов

Основные подходы к сохранению файлов в PHP

Эффективный способ: file_put_contents

Функция file_put_contents объединяет открытие, запись и закрытие файла в одном вызове. Она принимает путь к файлу, данные (строку или массив) и необязательные флаги. Этот метод подходит для большинства задач, когда требуется сохранить небольшой объём текстовой информации (логи, конфигурации, кэш).


<?php
$file = '/var/www/data/settings.txt';
$content = 'version=1.0™' . PHP_EOL;
if (file_put_contents($file, $content) === false) {
    // Обработка ошибки записи
}
?>
  

Флаг FILE_USE_INCLUDE_PATH позволяет искать файл в include_path, а LOCK_EX устанавливает эксклюзивную блокировку. Для дописывания применяется флаг FILE_APPEND.

Проблемы:

  • Права доступа: если каталог недоступен для записи, функция вернёт false. Решение – проверить права командой is_writable() или использовать исключения.
  • Перезапись: по умолчанию содержимое файла заменяется. Для предотвращения случайной потери данных используйте проверку существования файла или флаг FILE_APPEND.
  • Блокировка: при одновременной записи из нескольких процессов данные могут перемешаться. Флаг LOCK_EX решает проблему на уровне файла.

Как записать строку в файл с ручным управлением потоком?

Функции fopen, fwrite, fclose дают полный контроль над процессом. Это необходимо, когда требуется выполнить несколько операций с одним дескриптором (например, запись в разные позиции) или настроить таймауты.


<?php
$handle = fopen('/tmp/example.log', 'w');
if ($handle) {
    fwrite($handle, '[' . date('Y-m-d H:i:s') . '] Старт приложения' . PHP_EOL);
    fwrite($handle, '[' . date('Y-m-d H:i:s') . '] Завершение' . PHP_EOL);
    fclose($handle);
}
?>
  

Типичные ошибки:

  • Забытый fclose – дескриптор остаётся открытым, возможна утечка ресурсов. Решение – использовать блок try/finally или обёртку.
  • Неправильный режим: 'w' перезаписывает файл, 'a' дописывает. Выбор режима влияет на сохранность старых данных.

Как дописать данные в конец файла без перезаписи?

Флаг FILE_APPEND в file_put_contents или режим 'a' в fopen решают эту задачу. Пример с дописыванием строки в лог-файл:


<?php
file_put_contents('/var/log/app.log', 'Новая запись' . PHP_EOL, FILE_APPEND | LOCK_EX);
?>
  

Режим 'a' всегда помещает указатель в конец файла, даже если файл не существует – создаёт новый.

Проблемы:

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

Как сохранить массив в файл для последующего восстановления?

Сериализация (serialize) и JSON (json_encode) – два популярных способа. Сериализация сохраняет типы данных PHP, но не читаема для других языков. JSON универсален, но теряет информацию о классе объекта.


<?php
$data = ['user' => 'Иван', 'age' => 30];
// JSON
file_put_contents('user.json', json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
// Сериализация
file_put_contents('user.ser', serialize($data));
?>
  

Ошибки:

  • Кодировка: json_encode без флага JSON_UNESCAPED_UNICODE преобразует кириллицу в escape-последовательности.
  • Попытка сериализовать ресурсы (например, соединение с БД) приведёт к ошибке.
  • При чтении сериализованных данных из непроверенного файла возможна удалённая атака через unserialize – используйте allowed_classes или переходите на JSON.

Как записать структурированные данные в CSV?

Функция fputcsv форматирует массив как строку CSV и записывает её в файл. Это удобно для экспорта таблиц и обмена с электронными таблицами.


<?php
$handle = fopen('data.csv', 'w');
$headers = ['Имя', 'Возраст', 'Город'];
fputcsv($handle, $headers, ';');
$rows = [
    ['Анна', 25, 'Москва'],
    ['Пётр', 30, 'Санкт-Петербург']
];
foreach ($rows as $row) {
    fputcsv($handle, $row, ';');
}
fclose($handle);
?>
  

Проблемы:

  • Разделитель по умолчанию – запятая. Если данные содержат запятые, используйте другой разделитель (например, точку с запятой) и экранирование.
  • Кодировка CSV должна совпадать с ожидаемой программой-импортёром. Часто требуется UTF-8 с BOM.
  • Функция fputcsv автоматически экранирует кавычки, но не добавляет BOM. Для BOM используйте fwrite($handle, "\xEF\xBB\xBF"); перед записью заголовков.

Как обеспечить атомарность записи при многопользовательском доступе?

Блокировка с помощью flock предотвращает конфликты. Однако лучшее решение – запись во временный файл с последующим переименованием (rename), что является атомарной операцией в файловых системах Linux.


<?php
$tmp = '/tmp/data_' . uniqid() . '.tmp';
$final = '/var/www/data/config.json';
file_put_contents($tmp, json_encode($data));
rename($tmp, $final); // Атомарно заменяет файл
?>
  

Ошибки:

  • Функция rename не работает между разными файловыми системами (mount). В таких случаях следует использовать copy с удалением и блокировкой.
  • При ошибке записи во временный файл основной файл остаётся нетронутым – это плюс атомарного подхода.

Как записать большой файл (более 100 МБ) без перегрузки памяти?

Данные следует записывать порциями, используя fwrite в цикле. Это может быть потоковое чтение из другого источника (база данных, сокет) или генерация по шагам.


<?php
$src = fopen('source.txt', 'r');
$dst = fopen('destination.txt', 'w');
while (!feof($src)) {
    $chunk = fread($src, 8192);
    fwrite($dst, $chunk, 8192);
}
fclose($src);
fclose($dst);
?>
  

Проблемы:

  • Отсутствие контроля памяти – если чанки слишком большие (килобайты), накладные расходы на вызовы fwrite растут. Оптимальный размер 4-8 КБ.
  • Таймауты выполнения скрипта – для длительных операций требуется выставить set_time_limit(0) или использовать CLI.

Выбор конкретного метода зависит от объёма данных, требований к атомарности, удобства чтения другими системами и ожидаемой нагрузки. Все приведённые варианты работоспособны при соблюдении рекомендаций по обработке ошибок.

Расширенные примеры записи файлов

1. Запись лога с ротацией и временем

Пример

<?php
$logDir = '/var/log/myapp';
$date = date('Y-m-d');
$file = "$logDir/app_$date.log";
if (!is_dir($logDir)) {
    mkdir($logDir, 0755, true);
}
$message = '[' . date('H:i:s') . '] INFO: Пользователь авторизован' . PHP_EOL;
file_put_contents($file, $message, FILE_APPEND | LOCK_EX);
?>

Результат: файл /var/log/myapp/app_2025-03-25.log содержит строку с временем и уровнем.

[14:23:45] INFO: Пользователь авторизован

2. Запись многомерного массива в JSON с отступами и без экранирования Unicode

Пример

<?php
$config = [
    'database' => [
        'host' => 'localhost',
        'port' => 3306,
        'credentials' => ['user' => 'admin', 'pass' => 'secret']
    ],
    'cache' => ['type' => 'redis', 'ttl' => 3600]
];
$json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
if (file_put_contents('/etc/myapp/config.json', $json, LOCK_EX) === false) {
    throw new RuntimeException('Не удалось записать конфиг');
}
?>
{
    "database": {
        "host": "localhost",
        "port": 3306,
        "credentials": {
            "user": "admin",
            "pass": "secret"
        }
    },
    "cache": {
        "type": "redis",
        "ttl": 3600
    }
}

3. Запись CSV с BOM и настраиваемым разделителем

Пример

<?php
$handle = fopen('/tmp/export.csv', 'w');
fwrite($handle, "\xEF\xBB\xBF"); // UTF-8 BOM
fputcsv($handle, ['Товар', 'Цена', 'Количество'], ',', '"', "\\");
$items = [
    ['Кофе', 250.50, 10],
    ['Чай', 180.00, 25],
    ['"Сок" апельсиновый', 120.00, 15]
];
foreach ($items as $item) {
    fputcsv($handle, $item, ',', '"', "\\");
}
fclose($handle);
?>
Товар,Цена,Количество
Кофе,250.5,10
Чай,180,25
"""Сок"" апельсиновый",120,15

4. Атомарная запись через временный файл с использованием исключений

Пример

<?php
function atomicWrite(string $finalPath, string $data): void
{
    $dir = dirname($finalPath);
    $tmp = tempnam($dir, 'tmp_');
    if (file_put_contents($tmp, $data, LOCK_EX) === false) {
        unlink($tmp);
        throw new RuntimeException('Ошибка записи временного файла');
    }
    if (!rename($tmp, $finalPath)) {
        unlink($tmp);
        throw new RuntimeException('Не удалось переименовать');
    }
}
atomicWrite('/var/www/data/state.json', json_encode(['status' => 'active']));
?>

Результат: файл state.json обновлён атомарно, при сбое остаётся предыдущая версия.

5. Запись потока из сокета в файл с таймаутом

Пример

<?php
$src = fsockopen('tcp://example.com', 80, $errno, $errstr, 30);
if (!$src) {
    throw new RuntimeException("Не удалось соединиться: $errstr");
}
fwrite($src, "GET /data HTTP/1.0\r\nHost: example.com\r\n\r\n");
stream_set_timeout($src, 10);
$dst = fopen('/tmp/response.bin', 'w');
while (!feof($src)) {
    $chunk = fread($src, 8192);
    if ($chunk === false) break;
    fwrite($dst, $chunk);
}
fclose($dst);
fclose($src);
?>

Результат: содержимое ответа сервера сохраняется в файл response.bin.

6. Использование SplFileObject для записи с итерацией

Пример

<?php
$file = new SplFileObject('/tmp/data.txt', 'w');
$file->fwrite("Первая строка\n");
$file->fwrite("Вторая строка\n");
// Получение размера
$size = $file->getSize(); // 0, так как файл только что создан (после flush)
$file->fflush();
$size = filesize('/tmp/data.txt');
echo $size; // 30
?>
30

Сохраненные файлы в PHP - comments

En
Saved files php (php)