phatnt93 / php-file-helper
A high-performance, expressive, and secure PHP file and directory manipulation library supporting create, delete, rename, copy, move, atomic writes, streaming, path traversal protection, finder, zip archiving, and remote FTP management.
Requires
- php: ^8.1
- ext-dom: *
- ext-fileinfo: *
- ext-json: *
- ext-libxml: *
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- phpseclib/phpseclib: ^3.0.43
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^10.5.38 || ^11.4
Suggests
- ext-ftp: Required for FTP upload and remote file management features
- ext-phar: Required for TAR and GZIP archiving and extraction features
- ext-zip: Required for ZIP archiving and extraction features
- phpseclib/phpseclib: Required for pure-PHP SFTP support without ext-ssh2
Provides
None
Conflicts
None
Replaces
None
README
English | Tiếng Việt
A secure, dependency-free PHP 8.1+ library for working with files and directories: atomic writes, streaming reads, path-traversal protection, ZIP/TAR with Zip Slip protection, checksums, filesystem transactions with rollback, a fluent Finder, FTP/SFTP clients and 48 short global f_* helpers.
- Every failure throws a typed exception — no silent
falsereturns. - Writes are atomic by default and create missing parent directories.
- Works the same on Windows, Linux and macOS.
- Statically analysed with PHPStan level 8, tested on PHP 8.1–8.4 with ≥ 90 % line coverage.
Installation
composer require phatnt93/php-file-helper
Requirements: PHP >= 8.1 with ext-dom, ext-fileinfo, ext-json and ext-libxml (all enabled in standard PHP builds).
Optional:
ext-zipfor ZIP archivesext-phar(andext-zlibfor.tar.gz) for TAR archivesext-ftpfor FTP/FTPSphpseclib/phpseclib^3 (recommended) orext-ssh2for SFTP
Quick start
use PhpFileHelper\File; File::write('/var/app/storage/report.txt', 'Hello'); // atomic, creates /var/app/storage if needed echo File::read('/var/app/storage/report.txt'); // Or with the global helpers f_write(f_path($dir, 'notes.txt'), 'Hello'); echo f_read(f_path($dir, 'notes.txt'));
Runnable scripts live in examples/:
| Script | Shows |
|---|---|
01_quick_start.php |
f_* helpers, atomic writes, JSON, .env, head/tail, replace |
02_folders_and_finder.php |
scoped temp workspaces, mirror with ignore patterns, Finder |
03_advanced_features.php |
transactions, locks, chunk stitching, stream copy progress, snapshots, shredding |
04_archives_and_security.php |
ZIP/TAR, traversal guard, filename and SVG sanitising, checksum manifests |
How to write files atomically?
File::write() writes to a temporary file in the same directory and renames it over the target, so readers never see a half-written file. Parent directories are created automatically.
use PhpFileHelper\File; File::write('/path/to/file.txt', 'Hello World!'); // atomic + LOCK_EX (default) File::write('/path/to/file.txt', 'fast', atomic: false); // plain file_put_contents File::append('/path/to/file.txt', "\nNew line"); File::prepend('/path/to/file.txt', "Header line\n"); File::writeJson('/path/to/data.json', ['user' => 'John', 'age' => 30]); $data = File::readJson('/path/to/data.json'); File::replace('/path/to/app.env', 'APP_ENV=local', 'APP_ENV=production'); File::replace('/path/to/template.html', ['{{title}}' => 'Home', '{{year}}' => '2026']); File::replaceRegex('/path/to/config.php', '/debug\s*=\s*true/', 'debug = false');
How to read large files without running out of memory?
// Generator: one line at a time foreach (File::readLines('/var/log/app.log', trim: true, skipEmpty: true) as $line) { // ... } $first10 = File::head('/var/log/app.log', lines: 10); $last20 = File::tail('/var/log/app.log', lines: 20); // seeks backwards from the end File::writeCsv('/path/to/users.csv', [['Name', 'Role'], ['Alice', 'Admin']]); foreach (File::readCsv('/path/to/users.csv') as $row) { // ['Alice', 'Admin'] } // Copy a large file with progress: fn(int $copiedBytes, int $totalBytes, float $percentage) File::streamCopy('/source/backup.dump', '/dest/backup.dump', function (int $copied, int $total, float $pct) { printf("\r%.1f%%", $pct); });
How to prevent path traversal?
use PhpFileHelper\Path; use PhpFileHelper\Exceptions\PathTraversalException; try { $safePath = Path::assertWithinPath($_GET['file'], '/var/www/uploads'); // $safePath is normalised and guaranteed to be inside /var/www/uploads } catch (PathTraversalException $e) { // "../../etc/passwd" and similar attempts end up here } Path::normalize('C:\app/storage//logs/../cache/app.log'); // "C:/app/storage/cache/app.log" Path::join('/base', 'uploads', 'avatars', 'user.png'); // "/base/uploads/avatars/user.png" Path::sanitizeFilename('user:avatar/test*?.png'); // "user_avatar_test_.png"
Paths containing null bytes throw InvalidPathException.
How to zip or tar a folder?
use PhpFileHelper\Tar; use PhpFileHelper\Zip; Zip::archive('/var/app/source', '/backup/source.zip'); Zip::extract('/backup/source.zip', '/var/app/restored'); Tar::archive('/var/app/source', '/backup/source.tar.gz', gzip: true); Tar::extract('/backup/source.tar.gz', '/var/app/restored', entries: ['source/config.php']);
Before anything is extracted, every entry is checked: absolute paths, drive letters (C:/…), .. segments and null bytes are rejected with PathTraversalException (Zip Slip / Tar Slip).
How to copy, mirror or clean up directories?
use PhpFileHelper\Folder; Folder::create('/path/to/nested/directory'); Folder::copy('/src', '/dist', ignore: ['.git', 'node_modules', '*.log']); // ignoring a directory skips its contents Folder::move('/old', '/new'); Folder::delete('/path/to/dir'); // recursive by default // One-way sync: copies changed files, optionally deletes files missing from the source Folder::mirror('/var/source', '/var/backup', [ 'delete' => true, 'ignore' => ['.git', 'node_modules', '*.tmp'], ]); // Temporary workspace that is always deleted, even when the callback throws $result = Folder::scopedTemp(function (string $tempDir) { File::write($tempDir . '/temp.txt', 'processing...'); return 'Done'; }); Folder::purge('/var/log/app', olderThan: '30 days'); // retention policy Folder::hasEnoughSpace('/var/www', requiredBytes: 500 * 1024 * 1024);
How to find files by name, size or date?
use PhpFileHelper\Search\Finder; $logs = Finder::create() ->in('/var/log') ->files() ->name('*.log') // glob, or a regex such as '/^app-\d+\.log$/' ->notName('*error*') ->size('> 10MB') ->size('<= 100MB') ->modified('< 7 days ago') ->depth('<= 2') ->get(); // FileInfo[] (SplFileInfo with helpers such as read(), getFormattedSize())
How to roll back several file changes when something fails?
use PhpFileHelper\Transaction\FilesystemTransaction; FilesystemTransaction::run(function (FilesystemTransaction $tx) { $tx->createDir('/var/app/v2'); $tx->copy('/var/app/v1/config.json', '/var/app/v2/config.json'); $tx->write('/var/app/v2/.env', 'APP_ENV=production'); $tx->delete('/var/app/v1/old_cache.txt'); // Any exception thrown here restores every file and directory touched above. });
How to lock a file between processes?
$next = File::withLock('/var/run/counter.lock', function () { $count = (int) File::read('/var/run/counter.txt') + 1; File::write('/var/run/counter.txt', (string) $count); return $count; }, timeoutSeconds: 5); // PermissionDeniedException after 5 s File::withSharedLock('/var/run/config.lock', fn() => File::readJson('/var/run/config.json'));
How to verify file integrity?
use PhpFileHelper\Folder; use PhpFileHelper\Metadata; Metadata::hash('/path/to/video.mp4', algo: 'sha256'); // streamed Metadata::isSame('/a.bin', '/b.bin'); // size check, then chunked comparison Metadata::generateChecksumFile('/var/app/release', 'SHA256SUMS'); $report = Metadata::verifyChecksumFile('/var/app/release/SHA256SUMS'); // ['matched' => [...], 'failed' => [...], 'missing' => [...]] $snapshot = Folder::snapshot('/var/app/config'); // ... later $diff = Folder::detectDiff('/var/app/config', $snapshot); // ['added' => [], 'modified' => [], 'deleted' => []]
How to securely delete sensitive files?
File::shred('/path/to/private.key', passes: 3); // overwrite with random bytes, then delete Folder::shred('/path/to/sensitive_workspace', passes: 3);
How to edit .env, INI and other config files?
File::setEnvValue('/var/app/.env', 'APP_ENV', 'production'); // keeps comments and other keys File::getEnvValue('/var/app/.env', 'APP_ENV'); File::readEnv('/var/app/.env'); // ['APP_ENV' => 'production', ...] File::writeIni('/etc/app.ini', ['database' => ['host' => 'localhost', 'port' => 3306]]); File::readIni('/etc/app.ini'); File::imageDimensions('/path/to/photo.png'); // ['width' => 800, 'height' => 600, 'mime' => 'image/png'] File::sanitizeSvg('/path/to/upload.svg'); // DOM allow-list: drops scripts, handlers, external links
How to upload and download over FTP or SFTP?
use PhpFileHelper\Ftp; use PhpFileHelper\Sftp; $ftp = Ftp::connect([ 'host' => 'ftp.example.com', 'username' => 'deployer', 'password' => 'secret', 'ssl' => true, // FTPS only; never downgrades to plain FTP 'passive' => true, ]); $ftp->uploadDirectory('/var/app/dist', '/public_html/app', ignore: ['.git', '*.tmp']); $ftp->downloadFile('/public_html/backup.zip', '/var/app/backup.zip'); $items = $ftp->scanDir('/public_html'); // FtpFileItem[] $ftp->close(); $sftp = Sftp::connect([ 'host' => 'ssh.example.com', 'username' => 'ubuntu', 'privateKey' => '/home/user/.ssh/id_rsa', // or 'password' => 'secret' ]); $sftp->uploadFile('/local/build.tar.gz', '/remote/releases/build.tar.gz'); // remote folders are created $sftp->close();
FtpClient and SftpClient both implement PhpFileHelper\Contracts\RemoteFilesystemInterface.
Error handling
Every exception extends PhpFileHelper\Exceptions\FileSystemException (a \RuntimeException with getPath()), so one catch covers the whole library:
FileSystemException
├── FileNotFoundException ├── PermissionDeniedException (also lock timeouts)
├── FileAlreadyExistsException ├── ReadFailedException
├── DirectoryNotFoundException ├── WriteFailedException
├── DirectoryAlreadyExistsException├── DiskFullException
├── DirectoryNotEmptyException ├── InvalidPathException
├── PathTraversalException ├── FtpException → FtpConnectionException, FtpTransferException
└── SftpException → SftpConnectionException
use PhpFileHelper\Exceptions\FileNotFoundException; use PhpFileHelper\Exceptions\FileSystemException; try { $config = File::readJson('/etc/app/config.json'); } catch (FileNotFoundException $e) { $config = []; } catch (FileSystemException $e) { error_log(sprintf('%s (%s)', $e->getMessage(), $e->getPath())); throw $e; }
Invalid arguments (unknown hash algorithm, zero buffer size, invalid date expression) throw \InvalidArgumentException; JSON helpers throw \JsonException.
Comparison
| Capability | PHP native | symfony/filesystem |
league/flysystem |
php-file-helper |
|---|---|---|---|---|
| Atomic write (temp file + rename) | manual | ✅ dumpFile() |
❌ local adapter writes in place | ✅ default |
| Path traversal guard | manual | ⚠️ Path::isBasePath() helper |
✅ inside the configured root | ✅ Path::assertWithinPath() |
| Line-by-line / CSV streaming | SplFileObject |
❌ | ⚠️ raw streams | ✅ generators |
| ZIP / TAR with slip protection | ❌ | ❌ | ⚠️ ZIP via extra adapter | ✅ |
| FTP / SFTP | ext functions | ❌ | ✅ via adapters | ✅ |
| Cloud storage (S3, GCS, …) | ❌ | ❌ | ✅ | ❌ |
| Directory mirror | manual | ✅ mirror() |
❌ | ✅ with ignore + delete |
| Transactions with rollback | ❌ | ❌ | ❌ | ✅ |
| Checksum manifests / snapshots | manual | ❌ | ⚠️ checksum() per file |
✅ |
.env / INI / CSV helpers |
partial | ❌ | ❌ | ✅ |
| Runtime Composer dependencies | — | polyfills | league/mime-type-detection, … |
none |
Pick league/flysystem when you need one API over many storage backends (S3, GCS, Azure…). Pick this library for local-disk work where safety (atomic writes, traversal and slip protection, rollback) and a short API matter.
Global helpers (f_*)
Autoloaded through Composer. Each helper forwards to the facade method shown.
| Helper | Maps to |
|---|---|
f_path(...$parts) |
Path::join |
f_normalize($path) |
Path::normalize |
f_read($path) |
File::read |
f_read_lines($path, $trim, $skipEmpty) |
File::readLines |
f_read_json($path) |
File::readJson |
f_read_csv($path) |
File::readCsv |
f_write($path, $content) |
File::write |
f_write_json($path, $data) |
File::writeJson |
f_write_csv($path, $rows) |
File::writeCsv |
f_append($path, $content) |
File::append |
f_prepend($path, $content) |
File::prepend |
f_replace($path, $search, $replace) |
File::replace |
f_head($path, $lines) |
File::head |
f_tail($path, $lines) |
File::tail |
f_copy($source, $target) |
File::copy |
f_move($source, $target) |
File::move |
f_delete($paths) |
File::delete |
f_exists($path) |
File::exists |
f_is_file($path) |
File::isFile |
f_is_dir($path) |
Folder::isDir |
f_temp($prefix, $dir) |
File::temp |
f_env($path, $key) |
File::getEnvValue |
f_env_set($path, $key, $value) |
File::setEnvValue |
f_size($path) |
Metadata::size |
f_size_format($pathOrBytes) |
Metadata::formattedSize |
f_mime($path) |
Metadata::mimeType |
f_hash($path, $algo) |
Metadata::hash |
f_is_same($fileA, $fileB) |
Metadata::isSame |
f_dir_create($path) |
Folder::create |
f_dir_delete($path) |
Folder::delete |
f_dir_empty($path) |
Folder::empty |
f_dir_copy($source, $target) |
Folder::copy |
f_dir_move($source, $target) |
Folder::move |
f_dir_list($path) |
Folder::list |
f_dir_size($path) |
Folder::size |
f_scoped_temp($callback) |
Folder::scopedTemp |
f_mirror($source, $target, $options) |
Folder::mirror |
f_purge($path, $olderThan) |
Folder::purge |
f_with_lock($path, $callback) |
File::withLock |
f_shred($path) |
File::shred / Folder::shred |
f_transaction($callback) |
FilesystemTransaction::run |
f_finder() |
Finder::create |
f_zip($sources, $destination) |
Zip::archive |
f_unzip($zip, $destination) |
Zip::extract |
f_tar($sources, $destination) |
Tar::archive |
f_untar($tar, $destination) |
Tar::extract |
f_ftp($config) |
Ftp::connect |
f_sftp($config) |
Sftp::connect |
Using with AI coding agents
llms.txt— short overview for LLMs (the llms.txt convention).llms-full.txt— full API reference: every method signature, allf_*helpers, callback signatures and the exception tree.AGENTS.md— commands and conventions for agents contributing to this repository.
Point your assistant at llms-full.txt (for example "use vendor/phatnt93/php-file-helper/llms-full.txt as the API reference") to get accurate method names and parameters.
Contributing & security
- CONTRIBUTING.md — setup, test/analyse/style commands, pull request checklist.
- SECURITY.md — supported versions and how to report a vulnerability privately.
- CHANGELOG.md — release notes.
License
MIT License. Copyright (c) 2026 Phat Nguyen. See LICENSE.