cleverage/archive-process-bundle
CleverAge ArchiveProcessBundle integrates Archive handling into the ProcessBundle project, adding archive processing support to Symfony applications. Built for stable Symfony and LTS releases, it helps manage archived data within process workflows.
Installation Require the package via Composer:
composer require cleverage/archive-process-bundle
Register the bundle in config/bundles.php:
return [
// ...
Cleverage\ArchiveProcessBundle\CleverageArchiveProcessBundle::class => ['all' => true],
];
Where to Look First The day-to-day surface area is the two process tasks:
Cleverage\ArchiveProcessBundle\Task\ZipTask — packs a set of files/directories into an archive.Cleverage\ArchiveProcessBundle\Task\UnzipTask — extracts an archive to a destination directory.Read those two classes before anything else. Most configuration questions ("why is this file not in the archive?", "why did extraction land in the wrong place?", "why was my upload rejected?") resolve to how a task's inputs and options are evaluated.
First Use Case
Add a ZipTask step to a process so that a finished process's output directory is archived:
# config/packages/cleverage_process.yaml
cleverage_process:
processes:
my_process:
tasks:
archive_output:
class: Cleverage\ArchiveProcessBundle\Task\ZipTask
inputs:
- files: ['%kernel.project_dir%/var/output']
files_base_path: '%kernel.project_dir%/var/output'
archive_path: '%kernel.project_dir%/var/archives/my_process.zip'
Then run the process. If the archive cannot be written you will now get an exception rather than a silently returned path (see Gotchas).
Prefer task codes (the
class:key above) over the deprecatedservice:key in your process definitions — the cookbook and reference docs were updated in v2.4 to reflect this.
Archiving as a Process Step
Prefer wiring ZipTask/UnzipTask as regular steps in the process definition rather than calling services from controllers. The tasks are designed to sit in the middle of a pipeline:
generate_report -> zip_output -> upload_archive
The zip step's output (the archive path) is then consumed by the next task.
Multiple Inputs, Per-Input Options Both tasks accept an array of inputs, each input carrying its own options. Since v2.2 the options are resolved on every execution, so each input is handled with its own settings:
inputs:
- files: ['/var/output/reports']
files_base_path: '/var/output'
archive_path: '/var/archives/reports.zip'
- files: ['/var/output/exports']
files_base_path: '/var/output/exports'
archive_path: '/var/archives/exports.zip'
Before v2.2 the first input's options silently leaked into subsequent inputs — audit any multi-input zip/unzip step you already have, since archive contents may legitimately change after the upgrade.
files_base_path Semantics
files_base_path is stripped from each file path to compute the path inside the archive:
files_base_path is empty, files are read at the given path as-is; relative paths now resolve from the current working directory rather than from the filesystem root (v2.2 fix).Practical rule: always set files_base_path explicitly to the directory whose contents you want at the archive root, and use absolute paths when you can.
Round-Tripping Archives
Pair ZipTask with UnzipTask when processes need to hand off data between environments or replay an archived run:
tasks:
unarchive_inputs:
class: Cleverage\ArchiveProcessBundle\Task\UnzipTask
inputs:
- archive_path: '/var/archives/my_process.zip'
extract_path: '/var/working/restored'
Untrusted Archives: Bound the Extraction (v2.4)
UnzipTask gained max_files and max_size options (default null, i.e. no limit). Both are checked before anything is extracted, and the task throws a RuntimeException when a limit is exceeded:
tasks:
unarchive_upload:
class: Cleverage\ArchiveProcessBundle\Task\UnzipTask
inputs:
- archive_path: '%kernel.project_dir%/var/uploads/incoming.zip'
extract_path: '%kernel.project_dir%/var/working/upload'
max_files: 5000
max_size: 104857600 # 100 MiB of *uncompressed* data
max_files counts entries in the archive (files and directories) — budget for the directory entries too, not just the payload.max_size is compared against the total uncompressed size, i.e. what a zip bomb expands to, not the size of the .zip file.extract_path untouched — no partial extraction to clean up.ZipTask can leave them at null.try/catch for \RuntimeException (I/O failure, limit exceeded) and \UnexpectedValueException (malformed input) so a failed archive doesn't take down an entire batch:
try {
$processManager->execute($process);
} catch (\RuntimeException $e) {
$this->logger->error('Archive step failed', ['exception' => $e]);
$process->markAsFailed($e->getMessage());
}
max_size/max_files from the per-run temp-dir quota you already enforce elsewhere, so the archive task and your disk limits tell the same story.UnzipTask can overwrite existing files. If extraction targets a directory that may already contain data, clear it first or extract into a run-specific temp directory — otherwise a partially-overwritten directory is hard to reason about after a failure. The v2.4 limits reduce (but don't remove) the need for that hygiene: they stop abusive archives, not legitimate-but-wrong ones.var/cache
Point archive_path at a dedicated directory (e.g. var/archives), not a cache or log directory that other tooling clears.max_files/max_size limits. Mirror those cases for your custom file layouts, and add a fixture archive that trips each limit.Silent failures are gone (behaviour change in v2.2)
Before v2.2, ZipTask would output the archive path even when the write failed, and UnzipTask did the same on extraction failure. Both now throw \RuntimeException. Any process that "worked" because it ignored a broken archive will now fail — that's correct, but it is a runtime change worth flagging in your upgrade notes.
Non-array input throws
Passing a bare string path instead of an input array used to produce an unrelated PHP warning or a TypeError. It now throws an explicit \UnexpectedValueException. If you build inputs dynamically, validate the shape (array of arrays) before handing it to the task.
Options caching across inputs (fixed in v2.2) The v2.2 option-resolution fix means multi-input steps finally honour every input's options. Conversely, if you had compensating workarounds (duplicating inputs, splitting into several steps), remove them — they now produce redundant archives.
files_base_path stripping (fixed in v2.2)
Only the leading occurrence is removed now. Archives created before v2.2 may have paths like reports/reports/2024/q1.csv; anything downstream that parses those internal paths will need to cope with the corrected layout.
Empty files_base_path and relative paths
With an empty base path, relative file paths are now resolved from the current working directory. If your process runner's cwd isn't what you expect (queue workers, cron), pass absolute paths.
max_files / max_size default to "unlimited" (v2.4)
The new protections are opt-in: leave them null and you have exactly the pre-v2.4 behaviour, zip bomb and all. Nothing in your existing definitions becomes safer automatically — you have to set the limits. Also note they live on UnzipTask only; ZipTask doesn't need them, and a limit configured on a zip step is simply not applied.
The limit exception is the same type as I/O failures
An over-limit archive raises \RuntimeException, sharing the type with unreadable files and unwritable targets. A blanket catch (\RuntimeException) will treat "someone uploaded a bomb" and "disk is full" identically. Either inspect the message, or log enough context (archive path, resolved limits) in the handler that the two are distinguishable after the fact.
max_size is uncompressed bytes, max_files counts entries
Comparing max_size against the archive file size is the classic mistake — it does nothing against a bomb. And a limit tuned only against "number of files I expect" will trip on archives with many directories or macOS-created __MACOSX/.DS_Store entries.
Filesystem permissions
Ensure the archive destination and extraction target are writable by the process user — this is surfaced as a RuntimeException rather than a bad path in the output.
UnzipTask open failure the message carries the underlying ZipArchive error code; look that code up rather than guessing (ZipArchive::ER_NOENT, ER_NOZIP, ER_OPEN, etc.). Limit failures carry the limit and observed values — use them to decide whether the limit or the archive is wrong.unzip -l / ZipArchive::statIndex() and compare entry count and uncompressed totals to what you configured.unzip -l / ZipArchive::open() outside the process to separate packaging problems from transport problems.files_base_path, archive_path, max_files and max_size for each input — that instantly explains "wrong file set" and "why was this rejected" reports.max_files/max_size guard over with it; the protection lives in the task, not in PHP's archive layer.ZipTask/UnzipTask step and diff the produced archives.\RuntimeException / \UnexpectedValueException.files_base_path bugs.max_files / max_size to every UnzipTask fed by data you didn't generate yourself; verify your exception handling can tell a limit breach from an I/O error.service: keys in process definitions with task codes where applicable.How can I help you explore Laravel packages today?