Filesystem
One Filesystem interface across local disk, in-memory trees, SQLite databases, and ZIP archives. Forward-slash paths everywhere — even on Windows — so the same code runs in tests, in production, and inside read-only ZIPs.
composer require wp-php-toolkit/filesystem
Code that touches the filesystem is hard to test, hard to port to Windows, and impossible to point at non-disk storage without rewriting it. Swap LocalFilesystem for InMemoryFilesystem in tests and your suite stops touching /tmp; swap it for SQLiteFilesystem and your "files" become rows in a portable database; swap it for ZipFilesystem and you can read inside an archive with the same calls.
Every backend uses forward slashes regardless of host OS. No DIRECTORY_SEPARATOR juggling, no Windows-only test failures, no surprises when a path moves between backends.
In-memory tree
The fastest backend. No disk I/O, no cleanup, no test-isolation problems.
<?php
require '/wordpress/wp-content/php-toolkit/vendor/autoload.php';
use WordPress\Filesystem\InMemoryFilesystem;
$fs = InMemoryFilesystem::create();
$fs->put_contents( '/hello.txt', 'Hello, world!' );
echo $fs->get_contents( '/hello.txt' );
Test code without touching disk
Code that takes a Filesystem parameter, instead of calling file_get_contents() directly, can be tested against an InMemoryFilesystem. The test sets up files in memory, exercises the function, and asserts on what got written — no temp directories, no cleanup.
<?php
require '/wordpress/wp-content/php-toolkit/vendor/autoload.php';
use WordPress\Filesystem\Filesystem;
use WordPress\Filesystem\InMemoryFilesystem;
function bump_version( Filesystem $fs, $path ) {
$json = json_decode( $fs->get_contents( $path ), true );
list( $maj, $min, $patch ) = explode( '.', $json['version'] );
$json['version'] = $maj . '.' . $min . '.' . ( (int) $patch + 1 );
$fs->put_contents( $path, json_encode( $json ) );
}
$fs = InMemoryFilesystem::create();
$fs->put_contents( '/package.json', '{"version":"1.2.3"}' );
bump_version( $fs, '/package.json' );
echo $fs->get_contents( '/package.json' ) . "\n";
Local disk with a chrooted root
LocalFilesystem::create($root) is implicitly chrooted: every path resolves relative to $root and a ../ cannot escape. Reach for it when a request path or CLI argument names a file inside one project directory.
<?php
require '/wordpress/wp-content/php-toolkit/vendor/autoload.php';
use WordPress\Filesystem\LocalFilesystem;
$root = sys_get_temp_dir() . '/toolkit-' . uniqid();
$fs = LocalFilesystem::create( $root );
$fs->mkdir( '/uploads', array( 'recursive' => true ) );
$fs->put_contents( '/uploads/note.txt', 'Hi from local disk.' );
echo $fs->get_contents( '/uploads/../uploads/note.txt' ) . "\n";
$fs->rmdir( '/', array( 'recursive' => true ) );
echo "exists after cleanup? " . ( is_dir( $root ) ? 'yes' : 'no' ) . "\n";
SQLite as a portable file store
The whole tree lives in one SQLite database file. Use it for self-contained scratch storage that survives process boundaries without leaving loose files behind.
<?php
require '/wordpress/wp-content/php-toolkit/vendor/autoload.php';
use WordPress\Filesystem\SQLiteFilesystem;
$fs = SQLiteFilesystem::create( ':memory:' );
$fs->mkdir( '/posts', array( 'recursive' => true ) );
for ( $i = 1; $i <= 3; $i++ ) {
$fs->put_contents( "/posts/post-{$i}.md", "# Post {$i}\n\nBody {$i}." );
}
foreach ( $fs->ls( '/posts' ) as $name ) {
$first = strtok( $fs->get_contents( '/posts/' . $name ), "\n" );
echo "{$name}: {$first}\n";
}
Copy a tree across backends
The killer composability move: copy_between_filesystems() streams files chunk-by-chunk from any source to any target. Pull a ZIP into SQLite, snapshot SQLite to disk, mirror disk into RAM — all the same call.
<?php
require '/wordpress/wp-content/php-toolkit/vendor/autoload.php';
use WordPress\Filesystem\InMemoryFilesystem;
use WordPress\Filesystem\LocalFilesystem;
use WordPress\Filesystem\SQLiteFilesystem;
use function WordPress\Filesystem\copy_between_filesystems;
$root = sys_get_temp_dir() . '/copytree-' . uniqid();
$local = LocalFilesystem::create( $root );
$local->mkdir( '/site/posts', array( 'recursive' => true ) );
$local->put_contents( '/site/posts/2024-01.md', '# Hello 2024' );
$local->put_contents( '/site/index.html', '<h1>Home</h1>' );
$sqlite = SQLiteFilesystem::create( ':memory:' );
copy_between_filesystems( array(
'source_filesystem' => $local,
'source_path' => '/site',
'target_filesystem' => $sqlite,
'target_path' => '/snapshot',
) );
$mem = InMemoryFilesystem::create();
copy_between_filesystems( array(
'source_filesystem' => $sqlite,
'source_path' => '/snapshot',
'target_filesystem' => $mem,
'target_path' => '/copy',
) );
echo "in memory after two copies:\n";
echo " posts: " . implode( ', ', $mem->ls( '/copy/posts' ) ) . "\n";
echo " index: " . $mem->get_contents( '/copy/index.html' ) . "\n";
$local->rmdir( '/', array( 'recursive' => true ) );
Atomic write via tempfile rename
Write to a sibling tempfile, then rename — that's how you avoid leaving a half-written file on crash. rename() is atomic within a single filesystem.
<?php
require '/wordpress/wp-content/php-toolkit/vendor/autoload.php';
use WordPress\Filesystem\Filesystem;
use WordPress\Filesystem\LocalFilesystem;
function atomic_put_contents( Filesystem $fs, $path, $bytes ) {
$tmp = $path . '.tmp.' . bin2hex( random_bytes( 4 ) );
$fs->put_contents( $tmp, $bytes );
$fs->rename( $tmp, $path );
}
$root = sys_get_temp_dir() . '/atomic-' . uniqid();
$fs = LocalFilesystem::create( $root );
$fs->put_contents( '/config.json', '{"v":1}' );
atomic_put_contents( $fs, '/config.json', '{"v":2}' );
echo "config: " . $fs->get_contents( '/config.json' ) . "\n";
echo "no .tmp leftovers: " . count( $fs->ls( '/' ) ) . " entries in root\n";
$fs->rmdir( '/', array( 'recursive' => true ) );
Path helpers that behave the same on Windows
Unix path semantics apply on every host OS. This matters for abstract paths such as a SQLite key or a ZIP entry name because those paths do not live on a real drive.
<?php
require '/wordpress/wp-content/php-toolkit/vendor/autoload.php';
use function WordPress\Filesystem\wp_join_unix_paths;
use function WordPress\Filesystem\wp_unix_dirname;
use function WordPress\Filesystem\wp_unix_path_resolve_dots;
echo wp_join_unix_paths( '/var/www', '/site/', '/index.php' ) . "\n";
echo wp_unix_dirname( '/a/b/c/d.txt', 2 ) . "\n";
echo wp_unix_path_resolve_dots( '/a/b/../c/./d/../e' ) . "\n";