php-extended/php-file-interface
php-extended/php-file-interface provides PHP contracts for file and directory abstractions, covering paths, metadata, content access, and filesystem operations. It enables consistent, implementation-agnostic file handling across compatible php-extended packages.
php-extended/php-file-interface is an interfaces-only package. composer require php-extended/php-file-interface installs a set of PHP contracts (namespace PhpExtended\File\) that model files, directories, paths and file systems — there is no working filesystem code in the box. The first thing to internalise is that this is a ports library: it exists so your application code can stop depending on fopen, SplFileObject, Illuminate\Filesystem\Filesystem or Flysystem directly.
Minimal steps:
composer require php-extended/php-file-interface
Then look at the interface sources in vendor/php-extended/php-file-interface/src/ before writing anything. Start with, in this order:
FileInterface — the single-file contract (contents, name, extension, size, timestamps, copy/move/delete).DirectoryInterface — listing and creating entries.FilesystemInterface / FileSystemInterface — the entry point that resolves a path string into a FileInterface or DirectoryInterface.PathInterface — normalisation, parents, segments, extensions.Before coding against them, decide which implementation you will bind — either the sibling concrete package from the same vendor (php-extended/php-file), or a thin adapter you write over Laravel's filesystem/Flysystem. Without one, every type-hint resolves to nothing at runtime.
First use case — inject the filesystem, don't construct it:
use PhpExtended\File\FilesystemInterface;
use PhpExtended\File\FileInterface;
final class InvoiceArchiver
{
public function __construct(private FilesystemInterface $filesystem) {}
public function store(string $path, string $contents): FileInterface
{
$file = $this->filesystem->getFile($path);
$file->write($contents); // verify exact method name in the interface source
return $file;
}
}
Bind the interface once in a service provider, and from that point the rest of the application never touches a raw path.
Ports and adapters, bound in the container. The canonical Laravel wiring is a single binding in AppServiceProvider:
$this->app->singleton(
\PhpExtended\File\FilesystemInterface::class,
fn () => new NativeFilesystemAdapter(base_path()) // your implementation
);
Everything downstream — controllers, jobs, console commands — type-hints the interface. Swapping local disk for an in-memory or test implementation becomes a one-line change.
Adapter over what you already have. Because the package ships no behaviour, the highest-value pattern is a small adapter class per interface, delegating to Illuminate\Filesystem\Filesystem or a Flysystem FilesystemOperator. Keep the adapter dumb: translate method calls, translate exceptions, nothing else.
Decorators instead of configuration. Interfaces are the natural seam for cross-cutting file concerns, and this is where the package earns its keep day-to-day:
ReadOnlyFile implements FileInterface — delegates reads, throws on writes.SandboxedFilesystem implements FilesystemInterface — rejects paths escaping a configured root (a path-jail), which is the single most useful decorator for anything touching user-supplied filenames.LoggingFilesystem / CachingFilesystem — record every mutation, or memoise metadata lookups during a long import.All of these compose without touching your domain code.
Iteration and bulk operations. Use DirectoryInterface plus the iterator/visitor/filter interfaces (where present in the package) for scans, cleanups and imports rather than glob()/scandir(). A job that walks a directory tree through the interfaces can be tested against a fake in-memory tree with three or four files instead of fixture directories on disk.
Path handling through PathInterface. Resolve, normalise and inspect paths via the interface rather than str_replace('//', '/') and explode('/'). It keeps Windows separators, trailing slashes and .. segments in one place instead of scattered across the codebase.
Testing. This is the strongest day-to-day argument for the package. Unit tests implement the interface inline (or with an anonymous class) to simulate a missing file, a permission error or a full disk — cases that are painful to reproduce against a real filesystem. Feature tests can bind an in-memory implementation and assert on recorded writes.
PhpExtended\File\FilesystemInterface vs Illuminate\Contracts\Filesystem\Filesystem vs Symfony's Finder will bite you in any file that touches both. Alias on import (use PhpExtended\File\FilesystemInterface as FsPort;) and keep adapters in their own namespace so collisions never reach domain code.UnableToWriteFile, PHP warnings) at the adapter boundary, otherwise swapping the implementation breaks catch blocks everywhere.BindingResolutionException on an interface type-hint almost always means the service provider binding is missing or misspelled — check ::class constants rather than string class names.How can I help you explore Laravel packages today?