Работа с файловой системой: запись и сохранение данных в 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