Filesystem
The Filesystem component provides type-safe functions for common file system operations. It replaces PHP's procedural file functions with proper error handling through exceptions, clear parameter types, and consistent behavior.
Unlike the File component (which deals with reading and writing file contents through handles), Filesystem focuses on managing files and directories themselves: checking existence, creating, copying, deleting, and inspecting metadata.
Checking Existence and Type
use Psl\Filesystem;
use Psl\IO;
// Create temp directory and file for demonstration
$dir = sys_get_temp_dir() . '/psl-fs-check-' . uniqid();
Filesystem\create_directory($dir);
$file = $dir . '/test.txt';
Filesystem\create_file($file);
$link = $dir . '/test-link';
Filesystem\create_symbolic_link($file, $link);
IO\write_line('exists(file): %s', Filesystem\exists($file) ? 'true' : 'false');
IO\write_line('is_file(file): %s', Filesystem\is_file($file) ? 'true' : 'false');
IO\write_line('is_directory(dir): %s', Filesystem\is_directory($dir) ? 'true' : 'false');
IO\write_line('is_symbolic_link: %s', Filesystem\is_symbolic_link($link) ? 'true' : 'false');
IO\write_line('is_readable(file): %s', Filesystem\is_readable($file) ? 'true' : 'false');
IO\write_line('is_writable(file): %s', Filesystem\is_writable($file) ? 'true' : 'false');
Filesystem\delete_file($link);
Filesystem\delete_file($file);
Filesystem\delete_directory($dir);
Creating Files and Directories
use Psl\Filesystem;
use Psl\IO;
$base = sys_get_temp_dir() . '/psl-fs-create-' . uniqid();
// Create a file (parent directories are created automatically)
$file = $base . '/nested/file.txt';
Filesystem\create_file($file);
IO\write_line('File created: %s', Filesystem\is_file($file) ? 'yes' : 'no');
// Create a directory (recursive by default)
$dir = $base . '/another/nested/dir';
Filesystem\create_directory($dir);
Filesystem\create_directory($dir, 0o755);
IO\write_line('Directory created: %s', Filesystem\is_directory($dir) ? 'yes' : 'no');
// Create a private temporary file without reusing an existing name.
$tmp = Filesystem\create_temporary_file();
IO\write_line('Temp file: %s', $tmp);
$tmp2 = Filesystem\create_temporary_file($base, prefix: 'app_');
IO\write_line('Prefixed temp file: %s', Filesystem\get_basename($tmp2));
// Clean up
Filesystem\delete_file($tmp);
Filesystem\delete_file($tmp2);
Filesystem\delete_directory($base, recursive: true);
create_temporary_file() creates a new file exclusively with permissions 0600. If a name already exists, it tries another name without changing the existing file. It adds 16 random hexadecimal characters to the prefix to keep names short enough for Unix socket paths under macOS's temporary directory.
Copying and Deleting
use Psl\File;
use Psl\Filesystem;
use Psl\IO;
$base = sys_get_temp_dir() . '/psl-fs-copy-' . uniqid();
Filesystem\create_directory($base);
$src = $base . '/source.txt';
$dst = $base . '/dest.txt';
File\write($src, 'hello world');
// Copy a file (preserves executable bits)
Filesystem\copy($src, $dst);
IO\write_line('Copied: %s', File\read($dst));
// Copy with overwrite
File\write($src, 'updated content');
Filesystem\copy($src, $dst, overwrite: true);
IO\write_line('Overwritten: %s', File\read($dst));
// Delete a file
Filesystem\delete_file($dst);
IO\write_line('File deleted: %s', !Filesystem\exists($dst) ? 'yes' : 'no');
// Delete a directory (empty, or recursive)
$subDir = $base . '/subdir';
Filesystem\create_directory($subDir);
Filesystem\delete_directory($subDir);
// Delete a directory with contents recursively
Filesystem\delete_directory($base, recursive: true);
IO\write_line('Directory deleted: %s', !Filesystem\exists($base) ? 'yes' : 'no');
Metadata and Permissions
use Psl\File;
use Psl\Filesystem;
use Psl\IO;
$tmp = Filesystem\create_temporary_file(prefix: 'psl_meta_');
File\write($tmp, 'some content here');
$bytes = Filesystem\file_size($tmp);
$mtime = Filesystem\get_modification_time($tmp);
$perms = Filesystem\get_permissions($tmp);
$owner = Filesystem\get_owner($tmp);
IO\write_line('Size: %d bytes', $bytes);
IO\write_line('Modified: %s', date('Y-m-d H:i:s', $mtime));
IO\write_line('Permissions: %o', $perms);
IO\write_line('Owner UID: %d', $owner);
Filesystem\change_permissions($tmp, 0o644);
IO\write_line('New permissions: %o', Filesystem\get_permissions($tmp));
Filesystem\delete_file($tmp);
Symbolic Links
use Psl\Filesystem;
use Psl\IO;
$base = sys_get_temp_dir() . '/psl-fs-links-' . uniqid();
Filesystem\create_directory($base);
$target = $base . '/target.txt';
$symlink = $base . '/symlink.txt';
$hardlink = $base . '/hardlink.txt';
Filesystem\create_file($target);
Filesystem\create_symbolic_link($target, $symlink);
Filesystem\create_hard_link($target, $hardlink);
$resolved = Filesystem\read_symbolic_link($symlink);
IO\write_line('Symlink points to: %s', $resolved);
IO\write_line('Is symbolic link: %s', Filesystem\is_symbolic_link($symlink) ? 'true' : 'false');
IO\write_line('Hard link exists: %s', Filesystem\is_file($hardlink) ? 'true' : 'false');
Filesystem\delete_directory($base, recursive: true);
Path Operations
use Psl\Filesystem;
use Psl\IO;
$base = sys_get_temp_dir() . '/psl-fs-paths-' . uniqid();
Filesystem\create_directory($base . '/subdir', 0o755);
$file = $base . '/subdir/file.txt';
Filesystem\create_file($file);
// Resolve to absolute path (returns null if path does not exist)
$real = Filesystem\canonicalize($base . '/subdir/../subdir/file.txt');
IO\write_line('Canonical: %s', $real ?? 'null');
IO\write_line('Basename: %s', Filesystem\get_basename($file)); // "file.txt"
IO\write_line('Without ext: %s', Filesystem\get_basename($file, '.txt')); // "file"
IO\write_line('Directory: %s', Filesystem\get_directory($file));
IO\write_line('Extension: %s', Filesystem\get_extension($file) ?? 'null'); // "txt"
$noExt = $base . '/subdir/Makefile';
Filesystem\create_file($noExt);
IO\write_line('No extension: %s', Filesystem\get_extension($noExt) ?? 'null'); // null
Filesystem\delete_directory($base, recursive: true);
Reading Directory Contents
use Psl\Filesystem;
use Psl\IO;
$dir = sys_get_temp_dir() . '/psl-fs-readdir-' . uniqid();
Filesystem\create_directory($dir);
Filesystem\create_file($dir . '/alpha.txt');
Filesystem\create_file($dir . '/beta.txt');
Filesystem\create_directory($dir . '/subdir');
$entries = Filesystem\read_directory($dir);
// Returns a list of absolute path strings (excludes . and ..)
foreach ($entries as $entry) {
IO\write_line(' %s', Filesystem\get_basename($entry));
}
Filesystem\delete_directory($dir, recursive: true);
Error Handling
All operations throw specific exceptions on failure:
use Psl\Filesystem;
use Psl\IO;
$missing = sys_get_temp_dir() . '/psl-fs-missing-' . uniqid() . '.txt';
try {
Filesystem\delete_file($missing);
} catch (Filesystem\Exception\NotFoundException) {
IO\write_line('Caught NotFoundException: file does not exist');
} catch (Filesystem\Exception\NotFileException) {
IO\write_line('Caught NotFileException: path exists but is not a file');
}
A path containing a NUL byte can never exist. Predicates such as exists() and is_file() return false for it, and canonicalize() returns null. Passing such a path where an existing node is expected, such as to file_size() or as the directory of create_temporary_file(), throws NotFoundException. Passing it as a node to create, such as the destination of copy() or the path given to create_file(), throws InvalidArgumentException.
See src/Psl/Filesystem/ for the full API.