Обработка большого объёма данных не должна зависеть от того, оставляет ли пользователь открытым HTTP-запрос, и не должна заставлять его гадать, продолжает ли система работу. Хорошо спроектированный API превращает процесс в идентифицируемую и наблюдаемую операцию: принимает файл, проверяет его структуру, обрабатывает данные и сообщает результат, включая ошибки, требующие исправления.
Отслеживание прогресса импорта в PHP — часть контракта API, а не технология очередей. Стоит разделить три взаимосвязанных вопроса: какой статус запрашивает пользователь, как выполняется фоновая задача и как восстановить задачу, завершившуюся с ошибкой.
Почему не следует обрабатывать весь импорт в одном HTTP-запросе

При синхронном запросе клиент ждёт ответа. При длительном импорте обработка может превысить ограничения веб-сервера, прокси или самого клиента. Если соединение прервётся, кроме того, может остаться неясным, успели ли измениться какие-либо записи.
В качестве альтернативы можно принять запрос, сохранить файл в хранилище, доступном обработчику, зарегистрировать операцию и вернуть её идентификатор. Задача выполняется позже, вне исходного запроса. Очередь может координировать её выполнение, но она не заменяет сохранённое состояние, которое API должен предоставлять клиенту.
Ответ о принятии подтверждает, что запрос получен, а не то, что все данные импортированы. Поэтому в нём должны быть идентификатор, по которому можно запросить состояние, и начальный статус. Проверка размера и формата может отклонить неподходящие файлы, но не гарантирует корректность каждой строки.
Определение статусов, переходов и результатов
Согласуйте ограниченный набор статусов и проверяемых переходов между ними. Возможный цикл включает received, validating и processing, а завершающими результатами могут быть completed, completed_with_errors, failed и cancelled. Названия могут различаться, но их значения должны быть однозначными.
completed_with_errors может означать, что корректные строки применены, а остальные доступны для исправления. Используйте failed для ситуации, которая не позволяет продолжить работу или определить надёжный результат. Не сообщайте об успехе только потому, что технический процесс завершился, если часть данных не была применена.
- Определите, какие статусы являются промежуточными, а какие — завершающими.
- Задокументируйте, кто и при каких условиях может инициировать каждый переход.
- Отделяйте общие ошибки от ошибок, связанных с конкретной строкой.
- Указывайте даты создания, начала и завершения, если они помогают интерпретировать результат.
- Не раскрывайте в публичных ответах трассировки, внутренние сообщения или конфиденциальные данные.
Защитите переходы от повторного выполнения и обновлений не по порядку. Завершённая операция не должна возвращаться в состояние processing из-за запоздавшего уведомления. Для предотвращения конкурентной перезаписи обычно используют обновления с проверкой статуса или версии.
Разделение приёма файла и проверки строк
При приёме проверьте права доступа, размер, тип файла и обязательные метаданные. Первичная проверка может подтвердить наличие обязательных столбцов и корректность структуры. Если файл невозможно интерпретировать, отклоните его или завершите операцию до применения частичных изменений, если соответствующая политика не определена.
Проверка отдельных строк выполняется во время обработки. Она может выявить отсутствующие ссылки, недопустимые значения или конфликты с текущими данными. Решите, следует ли применять корректные строки, даже если другие завершились ошибкой, или операция должна быть атомарной и откатываться целиком. Решение зависит от предметной области и побочных эффектов и должно быть понятно пользователю.
Идентифицируйте каждую строку стабильным образом — с помощью исходного ключа или, если его нет, ссылки, привязанной к пакету. Сохраняйте код ошибки и понятное сообщение. Для больших файлов возвращайте ошибки постранично или в виде защищённого файла для скачивания, вместо того чтобы повторять их при каждом запросе статуса.
Проектирование процесса исправления и повторной отправки ошибок
Продукт может позволять исправить весь файл и загрузить его как новый импорт либо повторно отправить только строки, завершившиеся ошибкой. Первый вариант проще объяснить, но при повторной обработке файла необходимо распознавать уже применённые записи. Второй сокращает объём повторной работы, но требует сохранять исходные ключи и связывать каждую новую попытку с первоначальным импортом.
Для частичной повторной отправки предоставьте отдельное действие, принимающее строки, завершившиеся ошибкой, либо строки, не подтверждённые как обработанные, вместе с исправленными данными. Не полагайтесь только на номер строки: пользователь может изменить порядок или структуру файла. Сохраняйте результат предыдущей попытки для аудита и повторно проверяйте полученные данные.
Для предотвращения дубликатов необходим стабильный идентификатор записи. Это может быть внешний идентификатор или сгенерированный и сохранённый ключ строки. Добавьте подходящее ограничение уникальности и определите, должна ли повторная отправка обновлять существующую запись, отклоняться или игнорироваться. В любом случае уже подтверждённые строки не должны повторно вызывать побочные эффекты. Идемпотентные операции и транзакции для каждой строки или пакета помогают обеспечить эту гарантию.
Публикация прогресса и управление запросами статуса
Сообщайте число обработанных единиц и общее количество, если оно известно. Указывайте текущий этап — например, проверку или применение, — если это помогает объяснить ожидание. Если общее количество ещё неизвестно, сообщайте этап, не выдумывая процент выполнения. Процент может вводить в заблуждение, если этапы занимают очень разное время.
Необязательно сохранять обновление после обработки каждой строки. Группируйте обновления с учётом затрат и полезности информации и не позволяйте клиенту отправлять запросы без ограничений. Интерфейс может увеличивать интервалы между запросами и прекращать их после получения завершающего статуса. Можно также использовать уведомления, но после перезагрузки страницы или потери соединения клиент всё равно должен иметь возможность получить текущее состояние.
Авторизуйте каждый запрос и проверяйте, есть ли у запрашивающего доступ именно к этой операции. Трудно угадываемый идентификатор не заменяет проверку прав. Применяйте те же проверки к ошибкам и скачиваемым файлам и определите срок их хранения.
Различие между повторным запуском, возобновлением и отменой
Повторный запуск — это повторное выполнение задачи, завершившейся с ошибкой; возобновление — продолжение с сохранённой точки. При повторном запуске могут повториться обработка строк или побочные эффекты, если процесс не идемпотентен. Прежде чем предлагать такой вариант, определите, что было подтверждено и как обнаруживаются дубликаты.
Для возобновления нужно сохранять достаточно информации, чтобы определить последнюю подтверждённую задачу и не применять повторно уже обработанные данные. Если исходные данные изменились или невозможно гарантировать согласованность продолжения, безопаснее создать новый импорт, чем утверждать, что прежний возобновлён.
Отмена также не отменяет автоматически уже применённые изменения. Определите, принимается ли она только до начала обработки, проверяется ли между пакетами или предусматривает компенсирующие действия. Сообщайте, остановлена ли операция до обработки, остались ли частичные изменения или прерывать её было уже слишком поздно.
Модель данных и процесс работы API в PHP
Начальная модель может включать таблицу imports с полями id, owner_id, status, stage, total_rows, processed_rows, success_rows, error_rows, file_key, created_at, started_at, finished_at и version. В таблице import_rows сохраняйте import_id, source_key, row_number, status и attempt_count. Ограничение уникальности по import_id и source_key идентифицирует строку внутри пакета. В таблице import_errors можно хранить ключ, код и безопасное для пользователя сообщение.
В качестве примера REST-процесса можно принимать файл через POST /api/imports и возвращать 202 Accepted с ответом {"id":"imp_8f2a","status":"received","status_url":"/api/imports/imp_8f2a"}. Клиент запрашивает GET /api/imports/imp_8f2a; пример ответа во время обработки: {"status":"processing","stage":"applying","total_rows":1200,"processed_rows":450,"success_rows":438,"error_rows":12}. Для получения ошибок используйте постраничный маршрут, например GET /api/imports/imp_8f2a/errors?limit=50&cursor=....
Если разрешена повторная отправка только строк с ошибками, маршрут POST /api/imports/imp_8f2a/retry-errors может принимать исходные ключи и исправленные данные. Он должен отклонять ключи, для которых уже зафиксировано применение, либо обрабатывать их в соответствии с явно заданной идемпотентной политикой и возвращать идентификатор новой попытки. Маршрут не должен скрывать, что создаётся связанная попытка, а не удаляется предыдущая история.
В PHP контроллер регистрирует операцию и отвечает, не обрабатывая весь файл в рамках запроса. Этот фрагмент — псевдокод в стиле PHP 7: он не использует именованные аргументы, а setJsonBody() обозначает собственный адаптер, а не стандартный метод PHP или метод, переносимый между фреймворками. В реальном приложении его следует заменить ответом, предусмотренным используемой реализацией или фреймворком:
$import = $imports->create(
$user->id,
$storedFile->key,
'received'
);
$dispatcher->dispatch(new ProcessImport($import->id));
$response->setStatusCode(202);
$response->setJsonBody(array(
'id' => $import->id,
'status' => 'received',
'status_url' => '/api/imports/' . $import->id
));
return $response;
Пример показывает разделение приёма запроса и выполнения задачи, но сам по себе не гарантирует, что задача будет надёжно сохранена. Согласуйте сохранение операции и отправку задачи так, чтобы сбой между этими шагами не оставил импорт необработанным. В зависимости от инфраструктуры можно использовать транзакцию с паттерном outbox или другой надёжный механизм.
Тестирование и контрольный список

Проверьте допустимые и недопустимые переходы, повреждённые файлы, частичные ошибки, повторные запросы и прерывания. Убедитесь, что для завершённой операции не публикуется дальнейший прогресс, счётчики согласованы, а постраничная выдача ошибок учитывает права доступа. Включите случай, когда уже применённая строка появляется в повторной отправке: в итоге должна существовать только одна запись, а побочные эффекты не должны повторяться.
- Различает ли пользователь приём файла, обработку и итоговый результат?
- Определено ли, нужно ли исправлять и загружать файл заново или повторно отправлять только строки с ошибками?
- Предотвращают ли стабильные ключи дублирование уже импортированных записей?
- Отражают ли счётчики объём подтверждённо выполненной работы и обновляются ли с разумной частотой?
- Требуют ли запрос статуса и скачивание ошибок соответствующих прав доступа?
- Определены и проверяемы ли последствия повторного запуска, возобновления и отмены?
- Проверены ли частичные сбои, конкурентный доступ и сохранность данных?
Наблюдаемость импорта — это не только индикатор прогресса. Нужны понятные статусы, доступные для запроса результаты и политика исправления, не допускающая повторного применения подтверждённых записей. Разделение этого контракта и механизма выполнения позволяет развивать обработку, не оставляя пользователя без информации.



