symfony/filesystem
Symfony Filesystem provides basic utilities for working with files and directories: creating, copying, moving, and removing files, dumping content safely, making paths relative, and normalizing paths. It's a lightweight, standalone component usable in any PHP project.
Installation:
composer require symfony/filesystem
Add to composer.json under require (pick the line that matches your Symfony/Laravel version):
"symfony/filesystem": "^8.0"
First Use Case:
Use the Filesystem class for basic file/directory operations:
use Symfony\Component\Filesystem\Filesystem;
$fs = new Filesystem();
// Create a directory
$fs->mkdir('path/to/directory');
// Copy a file (source permissions are preserved as of v5.4.54)
$fs->copy('source.txt', 'destination.txt');
// Remove a file
$fs->remove('file.txt');
Where to Look First:
Symfony\Component\Filesystem\Filesystem class methods (e.g., mkdir, copy, remove, dumpFile, exists, mirror, makePathRelative).Symfony\Component\Filesystem\Path class for path manipulation (e.g., normalize, getFilename, isAbsolute, makeRelative, canonicalize).Upgrade Note (v5.4.54):
^5.4.54, ^6.4.x, ^7.x, or the equivalent ^8.0.x) and run composer update symfony/filesystem before relying on the copy/temp-file behaviour described below.dumpFile() to write files atomically, reducing race conditions:
$fs->dumpFile('config.json', json_encode($config));
mirror() to replicate directory structures:
$fs->mirror('source_dir', 'destination_dir');
copy() now keeps the source file's permission bits instead of falling back to umask defaults. If you were relying on the old behaviour, apply an explicit chmod() after the copy:
$fs->copy($source, $target);
$fs->chmod($target, 0644); // only if you need to *narrow* what the source had
$normalizedPath = Path::normalize('/path/with/../segments');
Path::makeRelative() (or Filesystem::makePathRelative() when you need the trailing-slash style used internally by mirror()):
$relativePath = Path::makeRelative('/var/www/html/storage', '/var/www/html');
chmod() for secure file access:
$fs->chmod('file.txt', 0644);
chown() (Linux/macOS only):
$fs->chown('file.txt', 1000);
$tempFile = tempnam(sys_get_temp_dir(), 'prefix');
file_put_contents($tempFile, 'data');
$fs->remove($tempFile); // Cleanup
tempnam() output world-readable when a suffix is appended; the resulting file is created with restrictive (0600) permissions. This means a temp file handed to another process/user (e.g. a queue worker running as a different user) may now be unreadable — chmod() it explicitly if that sharing is intentional:
$fs->chmod($tempFile, 0644); // only when another user must read it
// app/Providers/AppServiceProvider.php
public function register()
{
$this->app->singleton(Filesystem::class, function () {
return new Filesystem();
});
}
use Symfony\Component\Filesystem\Filesystem;
class BackupCommand extends Command
{
protected $fs;
public function __construct(Filesystem $fs)
{
parent::__construct();
$this->fs = $fs;
}
public function handle()
{
$this->fs->mirror(storage_path('app'), backup_path('app'));
}
}
if ($fs->exists('file.txt')) {
$fs->remove('file.txt');
}
try {
$fs->remove('nonexistent_file.txt');
} catch (\RuntimeException $e) {
// Log or handle error
}
Windows vs. UNIX Path Handling:
Path::normalize() to ensure consistency:
$path = Path::normalize('C:\Users\file.txt'); // Converts to forward slashes
Permission Denied Errors:
try-catch blocks and log errors:
try {
$fs->chmod('protected_file.txt', 0644);
} catch (\RuntimeException $e) {
Log::error('Permission denied: ' . $e->getMessage());
}
Temp File Cleanup on Windows:
Filesystem::remove() explicitly:
$fs->remove($tempFile);
Temp Files Are Now Private by Default (v5.4.54):
tempnam()-style files created with a suffix are no longer world-readable, so cross-user / cross-container reads that used to "just work" can start failing with Permission denied.$fs->chmod($tempFile, 0644);
Copy Now Inherits the Source Mode (v5.4.54):
0600 secret file (e.g. .env) is now also 0600 — good for security, but it may break a deploy step that expected umask defaults (0644) on the destination. Conversely, copying a 0777 file propagates that looseness.copy() when the mode matters.Atomic Writes and Race Conditions:
dumpFile() for atomic writes:
$fs->dumpFile('log.txt', $newContent, 0644);
Path Injection Risks:
$path = Path::canonicalize($userInputPath);
if (!Path::isAbsolute($path)) {
throw new \InvalidArgumentException('Invalid path');
}
Wrap and Log Every Operation:
try {
$fs->copy($source, $target);
} catch (\RuntimeException $e) {
Log::error('copy failed', ['source' => $source, 'target' => $target, 'error' => $e->getMessage()]);
throw $e;
}
Check Path Normalization:
$normalized = Path::normalize('/path/with/../segments');
dd($normalized);
Inspect Modes After Copy/Temp Operations:
copy() and suffixed tempnam() files carry source-side permissions — verify with fileperms() when a permission bug is suspected:
dump(decoct(fileperms($target) & 0777));
Handle Windows-Specific Quirks:
str_replace('\\', '/', $path) as a fallback for legacy systems:
$path = str_replace('\\', '/', $path);
$normalized = Path::normalize($path);
Custom Filesystem Adapter:
Filesystem to add custom logic:
class CustomFilesystem extends Filesystem
{
public function copyPrivate(string $origin, string $target): void
{
$this->copy($origin, $target);
$this->chmod($target, 0600);
}
}
Wrap Operations in a Service:
class LoggingFilesystem extends Filesystem
{
public function copy(string $origin, string $target, bool $overwrite = false): void
{
event(new FilesystemOperationEvent('copy', [$origin, $target]));
parent::copy($origin, $target, $overwrite);
}
}
Integration with Laravel Events:
event(new FilesystemOperationEvent('copy', ['source.txt', 'destination.txt']));
No Runtime Symlink Toggle:
mirror() has a copy_on_windows option; there is no disableSymlinks() switch. Control symlink handling per call instead:
$fs->mirror('source_dir', 'destination_dir', null, ['copy_on_windows' => true]);
Custom Temp Directory:
putenv('TMPDIR=' . storage_path('framework/tests'));
Case-Insensitive Paths (Windows):
$normalized = Path::normalize(strtolower($path));
Batch Operations:
Filesystem::remove() with an array for multiple files:
$fs->remove(['file1.txt', 'file2.txt']);
Avoid Redundant Checks:
if ($fs->exists($path)) {
// Process file
}
Use mirror() for Large Directories:
$fs->mirror('source_dir', 'destination_dir', null, ['override' => true]);
How can I help you explore Laravel packages today?