PHPackages                             zenstruck/filesystem - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [File &amp; Storage](/categories/file-storage)
4. /
5. zenstruck/filesystem

ActiveLibrary[File &amp; Storage](/categories/file-storage)

zenstruck/filesystem
====================

Wrapper for league/flysystem with alternate API and added functionality.

v1.0.1(8mo ago)4333.0k↓22.8%4[17 issues](https://github.com/zenstruck/filesystem/issues)[4 PRs](https://github.com/zenstruck/filesystem/pulls)1MITPHPPHP &gt;=8.1CI failing

Since Apr 14Pushed 6mo ago2 watchersCompare

[ Source](https://github.com/zenstruck/filesystem)[ Packagist](https://packagist.org/packages/zenstruck/filesystem)[ Docs](https://github.com/zenstruck/filesystem)[ GitHub Sponsors](https://github.com/kbond)[ GitHub Sponsors](https://github.com/nikophil)[ RSS](/packages/zenstruck-filesystem/feed)WikiDiscussions 1.x Synced 2w ago

READMEChangelog (4)Dependencies (35)Versions (5)Used By (1)

zenstruck/filesystem
====================

[](#zenstruckfilesystem)

[![CI](https://github.com/zenstruck/filesystem/actions/workflows/ci.yml/badge.svg)](https://github.com/zenstruck/filesystem/actions/workflows/ci.yml)[![codecov](https://camo.githubusercontent.com/d02663f3b8a40e95df2b8239d9301189713407e6702dfcf18649a9d9cf32f13b/68747470733a2f2f636f6465636f762e696f2f67682f7a656e73747275636b2f66696c6573797374656d2f6272616e63682f312e782f67726170682f62616467652e7376673f746f6b656e3d3543777838594a6b784e)](https://codecov.io/gh/zenstruck/filesystem)

This library is a wrapper for the excellent [league/flysystem](https://flysystem.thephpleague.com/docs/)*File Storage Abstraction* library. It provides an *alternate* [API](#api) with the following major changes:

1. The main difference is the concept of [`Directory`](#directory), [`File`](#file) and [`Image`](#image) objects. These are wrappers for an individual *filesystem node* and provide info, metadata and more features. These can be passed around (ie sent to your templates) or even used as [Doctrine Types](#doctrine-integration).
2. Combine certain Flysystem methods. For example, `delete()` removes both files and directories, `write()` can write both strings and streams (+ more).
3. Eases the use of filesystem files as *real, local files*. Many 3rd party libraries that manipulate files require local files.

Additionally, the following features are provided:

1. Filesystem *wrappers* to add additional functionality (ie [`MultiFilesystem`](#multifilesystem), and [`LoggableFilesystem`](#loggablefilesystem)).
2. Powerful [testing helpers](#testfilesystem).
3. [`ZipFile`](#zipfile)/[`TarFile`](#tarfile) representing a local zip/tar(.gz/bz2) file that acts as both a filesystem *and* a real file.
4. [Doctrine Integration](#doctrine-integration).
5. [Symfony Integration](#symfony-integration)
    - [Custom Responses](#responses)
    - [Validators](#validators)
    - [Bundle](#bundle) to help configure filesystem services, wire the Doctrine integration and additional testing helpers.

Installation
------------

[](#installation)

```
composer require zenstruck/filesystem
```

API
---

[](#api)

### `Filesystem`

[](#filesystem)

```
/** @var \Zenstruck\Filesystem $filesystem */

// read operations
$filesystem->has('some/path'); // bool
$filesystem->node('some/path'); // Zenstruck\Filesystem\Node\File|Zenstruck\Filesystem\Node\Directory or throws NodeNotFound
$filesystem->file('some/path.txt'); // Zenstruck\Filesystem\Node\File or throws NodeNotFound or NodeTypeMismatch (if exists but not a file)
$filesystem->image('some/path.png'); // Zenstruck\Filesystem\Node\File\Image or throws NodeNotFound or NodeTypeMismatch (if exists but not an image)
$filesystem->directory('some/path'); // Zenstruck\Filesystem\Node\Directory or throws NodeNotFound or NodeTypeMismatch (if exists but not a directory)

// write operations (returns Zenstruck\Filesystem\File)
$filesystem->write('some/path.txt', 'string contents'); // write a string
$filesystem->write('some/path.txt', $resource); // write a resource
$filesystem->write('some/path.txt', new \SplFileInfo('path/to/local/file.txt')); // write a local file
$filesystem->write('some/path.txt', $file); // write a Zenstruck\Filesystem\Node\File

$filesystem->copy('from/file.txt', 'dest/file.txt'); // Zenstruck\Filesystem\Node\File (dest/file.txt)

$filesystem->move('from/file.txt', 'dest/file.txt'); // Zenstruck\Filesystem\Node\File (dest/file.txt)

$filesystem->delete('some/file.txt'); // returns self
$filesystem->delete('some/directory'); // returns self

// mkdir operations (returns Zenstruck\Filesystem\Node\Directory)
$filesystem->mkdir('some/directory'); // create an empty directory
$filesystem->mkdir('some/prefix', $directory); // create directory with files from Zenstruck\Filesystem\Node\Directory
$filesystem->mkdir('some/prefix', new \SplFileInfo('path/to/local/directory')); // create directory with files from local directory

$filesystem->chmod('some/file.txt', 'private'); // Zenstruck\Filesystem\Node (some/file.txt)

// utility methods
$filesystem->name(); // string - human-readable name for the filesystem
```

### `Node`

[](#node)

Interface: `Zenstruck\Filesystem\Node`.

```
/** @var \Zenstruck\Filesystem\Node $node */

$node->path(); // Zenstruck\Filesystem\Node\Path
$node->path()->toString(); // string - the full path
(string) $node->path(); // same as above
$node->path()->name(); // string - filename with extension
$node->path()->basename(); // string - filename without extension
$node->path()->extension(); // string|null - file extension
$node->path()->dirname(); // string|null - the parent directory (or null if there is none)

$node->dsn(); // Zenstruck\Filesystem\Node\Dsn
$node->dsn()->toString(); // string - ://
(string) $node->dsn(); // same as above
$node->dsn()->path(); // Zenstruck\Filesystem\Node\Path
$node->dsn()->filesystem(); // string - name of the filesystem this node belongs to

$node->directory(); // Zenstruck\Filesystem\Node\Directory|null - parent directory object (or null if there is none)

$node->visibility(); // string - ie "public" or "private"
$node->lastModified(); // \DateTimeImmutable (in currently configured timezone)

$node->isDirectory(); // bool
$node->isFile(); // bool
$node->isImage(); // bool

$node->exists(); // bool
$node->ensureExists(); // static or throws NodeNotFound

$node->refresh(); // static and clears any cached metadata

$node->ensureDirectory(); // Zenstruck\Filesystem\Node\Directory or throws NodeTypeMismatch (if not a directory)
$node->ensureFile(); // Zenstruck\Filesystem\Node\File or throws NodeTypeMismatch (if not a file)
$node->ensureImage(); // Zenstruck\Filesystem\Node\Image or throws NodeTypeMismatch (if not an image)
```

### `File`

[](#file)

Interface: `Zenstruck\Filesystem\Node\File` (extends [`Node`](#node)).

```
/** @var \Zenstruck\Filesystem\Node\File $file */

$file->contents(); // string - the file's contents

$file->stream(); // \Zenstruck\Stream - wrapper for a resource

$file->read(); // "raw" resource

$file->size(); // int

$file->guessExtension(); // string|null - returns extension if available or attempts to guess from mime-type

$file->checksum(); // string - using FilesystemAdapter's default algorithm
$file->checksum('md5'); // string - specify the algorithm

$file->publicUrl(); // string (needs to be configured)
$file->temporaryUrl(new \DateTimeImmutable('+30 minutes')); // string - expiring url (needs to be configured)
$file->temporaryUrl('+30 minutes'); // equivalent to above

$file->tempFile(); // \SplFileInfo - temporary local file that's deleted at the end of the script
```

Note

See [`zenstruck/temp-file`](https://github.com/zenstruck/temp-file#zenstrucktemp-file) for more details about `File::tempFile()`.

Note

See [`zenstruck/stream`](https://github.com/zenstruck/stream#zenstruckstream) for more details about `File::stream()`.

#### `PendingFile`

[](#pendingfile)

Class: `Zenstruck\Filesystem\Node\File\PendingFile` (extends `\SplFileInfo` and implements [`File`](#file)).

```
use Zenstruck\Filesystem\Node\File\PendingFile;

$file = new PendingFile('/path/to/local/file.txt');
$file->path()->toString(); // "/path/to/local/file.txt"

/** @var \Symfony\Component\HttpFoundation\File\UploadedFile $uploadedFile */

$file = new PendingFile($uploadedFile);
$file->path()->toString(); // $uploadedFile->getClientOriginalName()
```

#### `Image`

[](#image)

Interface: `Zenstruck\Filesystem\Node\File\Image` (extends [`File`](#file)).

```
/** @var \Zenstruck\Filesystem\Node\File\Image $image */

$image->dimensions(); // Zenstruck\Image\Dimensions
$image->dimensions()->height(); // int
$image->dimensions()->width(); // int
$image->dimensions()->pixels(); // int
$image->dimensions()->aspectRatio(); // float
$image->dimensions()->isSquare(); // bool
$image->dimensions()->isPortrait(); // bool
$image->dimensions()->isLandscape(); // bool

$image->exif(); // array - image exif data if available
$image->iptc(); // array - image iptc data if available

$thumbHash = $image->thumbHash(); // Zenstruck\Image\Hash\ThumbHash (requires srwiez/thumbhash)
$thumbHash->dataUri(); // string - data uri for the thumb
$thumbHash->key(); // string - hash for the thumb (to store in database/cache)

$image->transformUrl('filter-name'); // string (needs to be configured)
$image->transformUrl(['w' => 100, 'h' => 50]); // string (needs to be configured)

$image->transform(
    function(ManipulationObject $image) {
        // make manipulations

        return $image;
    }
); // PendingImage
```

Note

See [`zenstruck/image`](https://github.com/zenstruck/image#zenstruckimage) for more details about `Image::transform()` and `Image::thumbHash()`.

##### `PendingImage`

[](#pendingimage)

Class: `Zenstruck\Filesystem\Node\File\Image\PendingImage` (extends [`PendingFile`](#pendingfile) and implements [`Image`](#image)).

```
use Zenstruck\Filesystem\Node\File\Image\PendingImage;

$image = new PendingImage('/path/to/local/file.txt');
$image = new PendingImage($symfonyUploadedFile);

// transform and overwrite
$image->transformInPlace(
    function(ManipulationObject $image) {
        // make manipulations

        return $image;
    }
); // self
```

### `Directory`

[](#directory)

Interface: `Zenstruck\Filesystem\Node\Directory` (extends [`Node`](#node)).

```
/** @var Zenstruck\Filesystem\Node\Directory $directory */

// iterate over nodes (non-recursive)
foreach ($directory as $node) {
    /** @var Zenstruck\Filesystem\Node $node */
}

// iterate over only files (non-recursive)
foreach ($directory->files() as $file) {
    /** @var Zenstruck\Filesystem\Node\File $file */
}

// iterate over only directories (non-recursive)
foreach ($directory->directories() as $dir) {
    /** @var Zenstruck\Filesystem\Node\Directory $dir */
}

// recursively iterate
foreach ($directory->recursive() as $node) {
    /** @var Zenstruck\Filesystem\Node $node */
}

// advanced filter
$directories = $directory
    ->recursive()
    ->files()
    ->largerThan('10M')
    ->smallerThan('1G')
    ->olderThan('30 days ago')
    ->newerThan('20 days ago')
    ->matchingFilename('*.twig')
    ->notMatchingFilename('*.txt.twig')
    ->matchingPath('/files/')
    ->notMatchingPath('/exclude/')
    ->filter(function(File $file) { // custom filter
        if ($someCondition) {
            return false; // exclude
        }

        return true; // include
    })
;

// get first matching node
$directories->first(); // null|\Zenstruck\Filesystem\Node
```

Note

Most of the *advanced filters* require `symfony/finder` (`composer require symfony/finder`).

Filesystems
-----------

[](#filesystems)

### `FlysystemFilesystem`

[](#flysystemfilesystem)

```
use Zenstruck\Filesystem\FlysystemFilesystem;

/** @var \League\Flysystem\FilesystemOperator $operator */
/** @var \League\Flysystem\FilesystemAdapter $adapter */

// create from an already configured Flysystem Filesystem Operator
$filesystem = new FlysystemFilesystem($operator);

// create from an already configured Flysystem Filesystem Adapter
$filesystem = new FlysystemFilesystem($operator);

// create for local directory
$filesystem = new FlysystemFilesystem('/path/to/local/dir');

// create for dsn (see available DSNs below)
$filesystem = new FlysystemFilesystem('flysystem+ftp://user:pass@host.com:21/root');
```

#### Filesystem DSNs

[](#filesystem-dsns)

DSNAdapter`%kernel.project_dir%/public/files``LocalAdapter``in-memory:``InMemoryFilesystemAdapter` (requires [`league/flysystem-memory`](https://flysystem.thephpleague.com/docs/adapter/in-memory/))`in-memory:name`*Static* `InMemoryFilesystemAdapter` (requires [`league/flysystem-memory`](https://flysystem.thephpleague.com/docs/adapter/in-memory/))`flysystem+ftp://user:pass@host.com/root``FtpAdapter` (requires [`league/flysystem-ftp`](https://flysystem.thephpleague.com/docs/adapter/ftp/))`flysystem+ftps://user:pass@host.com/root``FtpAdapter` (requires [`league/flysystem-ftp`](https://flysystem.thephpleague.com/docs/adapter/ftp/))`flysystem+sftp://user:pass@host.com:22/root``SftpAdapter` (requires [`league/flysystem-sftp-v3`](https://flysystem.thephpleague.com/docs/adapter/sftp-v3/))`flysystem+s3://accessKeyId:accessKeySecret@bucket/prefix#us-east-1``AsyncAwsS3Adapter`/`AwsS3V3Adapter` (requires [`league/flysystem-async-aws-s3`](https://flysystem.thephpleague.com/docs/adapter/async-aws-s3/) or [`league/flysystem-aws-s3-v3`](https://flysystem.thephpleague.com/docs/adapter/aws-s3-v3/))`readonly:``ReadOnlyFilesystemAdapter` (requires [`league/flysystem-read-only`](https://flysystem.thephpleague.com/docs/adapter/read-only/))### `ScopedFilesystem`

[](#scopedfilesystem)

```
use Zenstruck\Filesystem\ScopedFilesystem;

/** @var \Zenstruck\Filesystem $primaryFilesystem */

$scopedFilesystem = new ScopedFilesystem($primaryFilesystem, 'some/prefix');

// paths are prefixed
$scopedFilesystem
    ->write('file.txt', 'content')
    ->path()->toString(); // "some/prefix/file.txt"
;

// prefix is stripped from path
$scopedFilesystem
    ->write('some/prefix/file.txt', 'content')
    ->path()->toString(); // "some/prefix/file.txt"
;
```

### `MultiFilesystem`

[](#multifilesystem)

```
use Zenstruck\Filesystem\MultiFilesystem;

/** @var \Zenstruck\Filesystem $filesystem1 */
/** @var \Zenstruck\Filesystem $filesystem2 */

$filesystem = new MultiFilesystem([
    'filesystem1' => $filesystem1,
    'filesystem2' => $filesystem2,
]);

// prefix paths with a "scheme" as the filesystem's name
$filesystem->file('filesystem1://some/file.txt'); // File from "filesystem1"
$filesystem->file('filesystem2://another/file.txt'); // File from "filesystem2"

// can copy and move across filesystems
$filesystem->copy('filesystem1://file.txt', 'filesystem2://file.txt');
$filesystem->move('filesystem1://file.txt', 'filesystem2://file.txt');

// set a default filesystem for when no scheme is set
$filesystem = new MultiFilesystem(
    [
        'filesystem1' => $filesystem1,
        'filesystem2' => $filesystem2,
    ],
    default: 'filesystem2'
);

$filesystem->file('another/file.txt'); // File from "filesystem2"
```

### `CacheFilesystem`

[](#cachefilesystem)

Note

A `psr/cache-implementation` is required.

```
use Zenstruck\Filesystem\CacheFilesystem;
use Zenstruck\Filesystem\Node\Mapping;

/** @var \Zenstruck\Filesystem $inner */
/** @var \Psr\Cache\CacheItemPoolInterface $cache */

$filesystem = new CacheFilesystem(
    inner: $inner,
    cache: $cache,
    metadata: [ // array of metadata to cache (see Zenstruck\Filesystem\Node\Mapping)
        Mapping::LAST_MODIFIED,
        Mapping::SIZE,
    ],
    ttl: 3600, // or null for no TTL
);

$filesystem->write('file.txt', 'content'); // caches metadata

$file = $filesystem->file('file.txt');
$file->lastModified(); // cached value
$file->size(); // cached value
$file->checksum(); // real value (as this wasn't configured to be cached)
$file->contents(); // actually reads the file (contents cannot be cached)
```

### `LoggableFilesystem`

[](#loggablefilesystem)

Note

A `psr/log-implementation` is required.

```
use Zenstruck\Filesystem\LoggableFilesystem;
use Zenstruck\Filesystem\Operation;
use Psr\Log\LogLevel;

/** @var \Zenstruck\Filesystem $inner */
/** @var \Psr\Log\LoggerInterface $logger */

$filesystem = new LoggableFilesystem($inner, $logger);

// operations are logged
$filesystem->write('file.txt', 'content'); // logged as '[info] Writing "string" to "file.txt" on filesystem ""'

// customize the log levels for each operation
$filesystem = new LoggableFilesystem($inner, $logger, [
    Operation::READ => false, // disable logging read operations
    Operation::WRITE => LogLevel::DEBUG,
    Operation::MOVE => LogLevel::ALERT,
    Operation::COPY => LogLevel::CRITICAL,
    Operation::DELETE => LogLevel::EMERGENCY,
    Operation::CHMOD => LogLevel::ERROR,
    Operation::MKDIR => LogLevel::NOTICE,
]);
```

### `EventDispatcherFilesystem`

[](#eventdispatcherfilesystem)

Note

A `psr/event-dispatcher-implementation` is required.

```
use Zenstruck\Filesystem\Event\EventDispatcherFilesystem;
use Zenstruck\Filesystem\Operation;

/** @var \Zenstruck\Filesystem $inner */
/** @var \Psr\EventDispatcher\EventDispatcherInterface $dispatcher */

$filesystem = new EventDispatcherFilesystem($inner, $dispatcher, [
    // set these to false or exclude to disable dispatching operation's event
    Operation::WRITE => true,
    Operation::COPY => true,
    Operation::MOVE => true,
    Operation::DELETE => true,
    Operation::CHMOD => true,
    Operation::MKDIR => true,
]);

$filesystem->write('foo', 'bar'); // PreWriteEvent/PostWriteEvent dispatched
$filesystem->mkdir('bar'); // PreMkdirEvent/PostMkdirEvent dispatched
$filesystem->chmod('foo', 'public'); // PreChmodEvent/PostChmodEvent dispatched
$filesystem->copy('foo', 'file.png'); // PreCopyEvent/PostCopyEvent dispatched
$filesystem->delete('foo'); // PreDeleteEvent/PostDeleteEvent dispatched
$filesystem->move('file.png', 'file2.png'); // PreMoveEvent/PostMoveEvent dispatched
;
```

Note

See event classes to see what is made available to them.

Note

The `Pre*Event` properties can be manipulated.

### `ZipFile`

[](#zipfile)

Note

`league/flysystem-ziparchive` is required (`composer require league/flysystem-ziparchive`).

This is a special filesystem wrapping a local zip archive. It acts as both a `Filesystem` and `\SplFileInfo` object:

```
use Zenstruck\Filesystem\Archive\ZipFile;

$archive = new ZipFile('/local/path/to/archive.zip');
$archive->file('some/file.txt');
$archive->write('another/file.txt', 'content');

(string) $archive; // /local/path/to/archive.zip
```

When creating without a path, creates a temporary archive file (that's deleted at the end of the script):

```
use Zenstruck\Filesystem\Archive\ZipFile;

$archive = new ZipFile();

$archive->write('some/file.txt', 'content');
$archive->write('another/file.txt', 'content');

(string) $archive; // /tmp/...
```

Write operations can be queued and committed via a *transaction*:

```
use Zenstruck\Filesystem\Archive\ZipFile;

$archive = new ZipFile();

$archive->beginTransaction(); // start the transaction
$archive->write('some/file.txt', 'content');
$archive->write('another/file.txt', 'content');
$archive->commit(); // actually writes the above files

// optionally pass a progress callback to commit
$archive->commit(function() use ($progress) { // callback is called at most, 100 times
    $progress->advance();
});
```

Static helper for quickly creating `zip` archives:

```
use Zenstruck\Filesystem\Archive\ZipFile;

// compress a local file
$zipFile = ZipFile::compress(new \SplFileInfo('/some/local/file.txt')); // ZipFile (temp file deleted on script end)

// compress a local directory (all files (recursive) in "some/local/directory" are added to archive)
$zipFile = ZipFile::compress(new \SplFileInfo('some/local/directory'));

/** @var \Zenstruck\Filesystem\Node\File $file */

// compress a filesystem file
$zipFile = ZipFile::compress($file);

/** @var \Zenstruck\Filesystem\Node\Directory $directory */

// compress a filesystem directory
$zipFile = ZipFile::compress($directory);

// compress several local/filesystem files
$zipFile = ZipFile::compress([
    new \SplFileInfo('/some/local/file.txt'),
    $file,
    'customize/path.txt' => $file, // use a string array key to set the path for the file
]);

// customize the output filename (will not be deleted at end of script)
$zipFile = ZipFile::compress(..., filename: 'path/to/archive.zip');
```

### `TarFile`

[](#tarfile)

Note

`league/flysystem-read-only` is required (`composer require league/flysystem-read-only`).

This is a special filesystem wrapping an existing local tar(.gz/bz2) archive. It acts as both a *readonly*`Filesystem` and `\SplFileInfo` object:

```
use Zenstruck\Filesystem\Archive\TarFile;

$archive = new TarFile('/local/path/to/archive.tar');
$archive = new TarFile('/local/path/to/archive.tar.gz');
$archive = new TarFile('/local/path/to/archive.tar.bz2');

$archive->file('some/file.txt'); // \Zenstruck\Filesystem\Node\File
```

`TestFilesystem`
----------------

[](#testfilesystem)

This filesystem wraps another and provides assertions for your tests. When using PHPUnit, these assertions are converted to PHPUnit assertions.

Note

`zenstruck/assert` is required to use the assertions (`composer require --dev zenstruck/assert`).

```
use Zenstruck\Filesystem\Test\TestFilesystem;
use Zenstruck\Filesystem\Test\Node\TestDirectory;
use Zenstruck\Filesystem\Test\Node\TestFile
use Zenstruck\Filesystem\Test\Node\TestImage;

/** @var \Zenstruck\Filesystem $filesystem */

$filesystem = new TestFilesystem($filesystem);

$filesystem
    ->assertExists('foo')
    ->assertNotExists('invalid')
    ->assertFileExists('file1.txt')
    ->assertDirectoryExists('foo')
    ->assertImageExists('symfony.png')
    ->assertSame('symfony.png', 'fixture://symfony.png')
    ->assertNotSame('file1.txt', 'fixture://symfony.png')
    ->assertDirectoryExists('foo', function(TestDirectory $dir) {
        $dir
            ->assertCount(4)
            ->files()->assertCount(2)
        ;

        $dir
            ->recursive()
            ->assertCount(5)
            ->files()->assertCount(3)
        ;
    })
    ->assertFileExists('file1.txt', function(TestFile $file) {
        $file
            ->assertVisibilityIs('public')
            ->assertChecksum($file->checksum()->toString())
            ->assertContentIs('contents1')
            ->assertContentIsNot('foo')
            ->assertContentContains('1')
            ->assertContentDoesNotContain('foo')
            ->assertMimeTypeIs('text/plain')
            ->assertMimeTypeIsNot('foo')
            ->assertLastModified('2023-01-01 08:54')
            ->assertLastModified(function(\DateTimeInterface $actual) {
                // ...
            })
            ->assertSize(9)
        ;
    })
    ->assertImageExists('symfony.png', function(TestImage $image) {
        $image
            ->assertHeight(678)
            ->assertWidth(563)
        ;
    })
;

$file = $filesystem->realFile('symfony.png'); // \SplFileInfo('/tmp/symfony.png') - deleted at the end of the script
```

### `InteractsWithFilesystem`

[](#interactswithfilesystem)

Use the `InteractsWithFilesystem` trait in your unit tests to quickly provide an in-memory filesystem.

Note

By default, `league/flysystem-memory` is required (`composer require --dev league/flysystem-memory`).

```
use PHPUnit\Framework\TestCase;
use Zenstruck\Filesystem\Test\InteractsWithFilesystem;

class MyTest extends TestCase
{
    use InteractsWithFilesystem;

    public function test_1(): void
    {
        $filesystem = $this->filesystem(); // instance of TestFilesystem wrapping an in-memory filesystem
        $filesystem->write('file.txt', 'content');
        $filesystem->assertExists('file.txt');
    }
}
```

#### `FilesystemProvider`

[](#filesystemprovider)

To provide your own filesystem for your tests, have your tests (or base test-case) implement `FilesystemProvider`:

```
use PHPUnit\Framework\TestCase;
use Zenstruck\Filesystem;
use Zenstruck\Filesystem\Test\InteractsWithFilesystem;
use Zenstruck\Filesystem\Test\FilesystemProvider;

class MyTest extends TestCase implements FilesystemProvider
{
    use InteractsWithFilesystem;

    public function test_1(): void
    {
        $filesystem = $this->filesystem(); // instance of TestFilesystem wrapping the AdapterFilesystem defined below
        $filesystem->write('file.txt', 'content');
        $filesystem->assertExists('file.txt');
    }

    public function createFilesystem(): Filesystem|FilesystemAdapter|string
    {
        return '/some/temp/dir';
    }
}
```

Note

By default, the provided filesystem isn't reset before each test. See the [`ResetFilesystem`](#resetfilesystem) to enable this behaviour.

#### `FixtureFilesystemProvider`

[](#fixturefilesystemprovider)

A common requirement for filesystem tests, is to have a set of known fixture files that are used in your tests. Have your test's (or base test-case) implement `FixtureFilesystemProvider` to provide in your tests:

```
use PHPUnit\Framework\TestCase;
use Zenstruck\Filesystem;
use Zenstruck\Filesystem\Test\InteractsWithFilesystem;
use Zenstruck\Filesystem\Test\FixtureFilesystemProvider;

class MyTest extends TestCase implements FixtureFilesystemProvider
{
    use InteractsWithFilesystem;

    public function test_1(): void
    {
        $filesystem = $this->filesystem(); // instance of TestFilesystem wrapping a MultiFilesystem

        $filesystem->write('file.txt', 'content'); // accesses your test filesystem
        $filesystem->assertExists('file.txt');
        $filesystem->copy('fixture://some/file.txt', 'file.txt'); // copy a fixture to your test filesystem
    }

    public function createFixtureFilesystem(): Filesystem|FilesystemAdapter|string;
    {
        return __DIR__.'/../fixtures';
    }
}
```

Note

If the [`league/flysystem-read-only`](https://flysystem.thephpleague.com/docs/adapter/read-only/)adapter is available, it's used to wrap your fixture adapter to ensure you don't accidentally overwrite/delete your fixture files (`composer require --dev league/flysystem-read-only`).

### `ResetFilesystem`

[](#resetfilesystem)

If using your own [`FilesystemProvider`](#filesystemprovider), you can use the `ResetFilesystem` trait to purge your filesystem before each test.

```
use PHPUnit\Framework\TestCase;
use Zenstruck\Filesystem;
use Zenstruck\Filesystem\Test\ResetFilesystem
use Zenstruck\Filesystem\Test\InteractsWithFilesystem;
use Zenstruck\Filesystem\Test\FilesystemProvider;

class MyTest extends TestCase implements FilesystemProvider
{
    use InteractsWithFilesystem, ResetFilesystem;

    public function test_1(): void
    {
        $this->filesystem()->write('file.txt', 'content')
        $this->filesystem()->assertExists('file.txt')
    }

    public function test_2(): void
    {
        $this->filesystem()->assertNotExists('file.txt'); // file created in test_1 was deleted before this test
    }

    public function createFilesystem(): Filesystem|FilesystemAdapter|string;
    {
        return '/some/temp/dir';
    }
}
```

Symfony Integration
-------------------

[](#symfony-integration)

### Responses

[](#responses)

Helpful custom Symfony responses are provided.

#### `FileResponse`

[](#fileresponse)

Take a filesystem [`File`](#file) and send as a response:

```
use Zenstruck\Filesystem\Symfony\HttpFoundation\FileResponse;

/** @var \Zenstruck\Filesystem\File $file */

$response = new FileResponse($file); // auto-adds content-type/last-modified headers

// create inline/attachment responses
$response = FileResponse::attachment($file); // auto names by the filename (file.txt)
$response = FileResponse::inline($file); // auto names by the filename (file.txt)

// customize the filename used for the content-disposition header
$response = FileResponse::attachment($file, 'different-name.txt');
$response = FileResponse::inline($file, 'different-name.txt');
```

#### `ArchiveResponse`

[](#archiveresponse)

Zip file(s) and send as a response. Can be created with a local file, local directory, instance of [`File`](#file) or instance of [`Directory`](#directory).

```
use Zenstruck\Filesystem\Symfony\HttpFoundation\ArchiveResponse;

/** @var \SplFileInfo|\Zenstruck\Filesystem\Node\File|\Zenstruck\Filesystem\Node\Directory $what */

$response = ArchiveResponse::zip($what);
$response = ArchiveResponse::zip($what, 'data.zip'); // customize the content-disposition name (defaults to archive.zip)
```

### Validators

[](#validators)

Both a [`PendingFile`](#pendingfile) and [`PendingImage`](#pendingimage) validator is provided. The constraints have the same API as Symfony's native [`File`](https://symfony.com/doc/current/reference/constraints/File.html) and [`Image`](https://symfony.com/doc/current/reference/constraints/Image.html) constraints.

```
use Zenstruck\Filesystem\Symfony\Validator\PendingFileConstraint;
use Zenstruck\Filesystem\Symfony\Validator\PendingImageConstraint;

/** @var \Symfony\Component\Validator\Validator\ValidatorInterface $validator */
/** @var \Zenstruck\Filesystem\Node\File $file */
/** @var \Zenstruck\Filesystem\Node\File\Image $image */

$validator->validate($file, new PendingFileConstraint(maxSize: '1M')));

$validator->validate($image, new PendingImageConstraint(maxWidth: 200, maxHeight: 200)));
```

### Bundle

[](#bundle)

#### Configuration

[](#configuration)

#### Services

[](#services)

#### Serializer

[](#serializer)

#### Form Types

[](#form-types)

##### `PendingFileType`

[](#pendingfiletype)

##### `PendingImageType`

[](#pendingimagetype)

#### Commands

[](#commands)

##### `zenstruck:filesystem:purge`

[](#zenstruckfilesystempurge)

#### Routing

[](#routing)

##### Public Url Route

[](#public-url-route)

##### Temporary Url Route

[](#temporary-url-route)

##### Transform Url Route

[](#transform-url-route)

##### `RouteTransformUrlGenerator`

[](#routetransformurlgenerator)

#### Doctrine Integration

[](#doctrine-integration)

#### Functional/Integration Testing

[](#functionalintegration-testing)

TODO: Note about `Mock::pendingFile()/pendingImage()`. TODO: Note about `Foundry\LazyMock::pendingFile()/pendingImage()`.

##### Testing Performance

[](#testing-performance)

### Full Default Bundle Configuration

[](#full-default-bundle-configuration)

```
zenstruck_filesystem:

    # Filesystem configurations
    filesystems:

        # Prototype
        name:

            # Flysystem adapter DSN or, if prefixed with "@" flysystem adapter service id
            dsn:                  ~ # Required

                # Examples:
                # - '%kernel.project_dir%/public/files'
                # - 'flysystem+ftp://foo:bar@example.com/path'
                # - 'flysystem+s3://accessKeyId:accessKeySecret@bucket/prefix#us-east-1'
                # - 'static-in-memory'
                # - 'scoped::'
                # - '@my_adapter_service'

            # Extra global adapter filesystem config
            config:               []

            # Lazily load the filesystem when the first call occurs
            lazy:                 true

            # Public URL generator for this filesystem
            public_url:

                # URL prefix or multiple prefixes to use for this filesystem (can be an array)
                prefix:

                    # Examples:
                    # - /files
                    # - 'https://cdn1.example.com'
                    # - 'https://cdn2.example.com'

                # Service id for a League\Flysystem\UrlGeneration\PublicUrlGenerator
                service:              null

                # Generate with a route
                route:

                    # Route name
                    name:                 ~ # Required

                    # Route parameters
                    parameters:           []

                    # Sign by default?
                    sign:                 false

                    # Default expiry
                    expires:              null # Example: '+ 30 minutes'

                # Enables cache busting for public urls
                version:
                    enabled:              false

                    # The metadata to use for versioning
                    metadata:             last_modified # One of "last_modified"; "size"; "checksum"

                    # The query parameter to use for versioning
                    parameter:            v

            # Temporary URL generator for this filesystem
            temporary_url:

                # Service id for a League\Flysystem\UrlGeneration\TemporaryUrlGenerator
                service:              null

                # Generate with a route
                route:

                    # Route name
                    name:                 ~ # Required

                    # Route parameters
                    parameters:           []

            # Image Transform URL generator for this filesystem
            image_url:

                # Service id for a League\Flysystem\UrlGeneration\PublicUrlGenerator
                service:              null

                # Generate with a route
                route:

                    # Route name
                    name:                 ~ # Required

                    # Route parameters
                    parameters:           []

                    # Sign by default?
                    sign:                 false

                    # Default expiry
                    expires:              null # Example: '+ 30 minutes'

            # Cache file/image metadata
            cache:
                enabled:              false

                # PSR-6 cache pool service id
                pool:                 cache.app

                # Cache TTL (null for no TTL)
                ttl:                  null

                # File/image metadata to cache (see Zenstruck\Filesystem\Node\Mapping)
                metadata:             ~

                    # Examples:
                    # - last_modified
                    # - size
                    # - dimensions

            # Dispatch filesystem operation events
            events:
                enabled:              false
                write:                true
                delete:               true
                mkdir:                true
                chmod:                true
                copy:                 true
                move:                 true

            # Log filesystem operations
            log:
                enabled:              true
                read:                 debug # One of false; "emergency"; "alert"; "critical"; "error"; "warning"; "notice"; "info"; "debug"
                write:                info # One of false; "emergency"; "alert"; "critical"; "error"; "warning"; "notice"; "info"; "debug"
                move:                 ~ # One of false; "emergency"; "alert"; "critical"; "error"; "warning"; "notice"; "info"; "debug"
                copy:                 ~ # One of false; "emergency"; "alert"; "critical"; "error"; "warning"; "notice"; "info"; "debug"
                delete:               ~ # One of false; "emergency"; "alert"; "critical"; "error"; "warning"; "notice"; "info"; "debug"
                chmod:                ~ # One of false; "emergency"; "alert"; "critical"; "error"; "warning"; "notice"; "info"; "debug"
                mkdir:                ~ # One of false; "emergency"; "alert"; "critical"; "error"; "warning"; "notice"; "info"; "debug"

            # If true, and using the ResetFilesystem trait
            # in your KernelTestCase's, delete this filesystem
            # before each test.
            reset_before_tests:   false

    # Default filesystem name used to autowire Zenstruck\Filesystem
    default_filesystem:   null

    # Doctrine configuration
    doctrine:
        enabled:              true

        # Global lifecycle events (can be disabled on a property-by-property basis)
        lifecycle:

            # Whether to auto load file type columns during object load
            autoload:             true

            # Whether to delete files on object removal
            delete_on_remove:     true
```

Backward Compatibility Promise
------------------------------

[](#backward-compatibility-promise)

This library follows [Symfony's BC Promise](https://symfony.com/doc/current/contributing/code/bc.html) with the following exceptions:

1. `Zenstruck/Filesystem` and any implementations are considered *internal* for *implementation*/*extension*.
2. `Zenstruck/Filesystem/Node` and any implementations are considered *internal* for *implementation*/*extension*.

###  Health Score

44

—

FairBetter than 90% of packages

Maintenance57

Moderate activity, may be stable

Popularity41

Moderate usage in the ecosystem

Community17

Small or concentrated contributor base

Maturity50

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 99.7% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~77 days

Total

5

Last Release

183d ago

Major Versions

v0.2.0 → v1.0.02025-12-07

### Community

Maintainers

![](https://www.gravatar.com/avatar/707369cc916e0ea1aacbf077dcba464f611cef879f024d8944311a54a15224b3?d=identicon)[kbond](/maintainers/kbond)

---

Top Contributors

[![kbond](https://avatars.githubusercontent.com/u/127811?v=4)](https://github.com/kbond "kbond (296 commits)")[![Lustmored](https://avatars.githubusercontent.com/u/2358046?v=4)](https://github.com/Lustmored "Lustmored (1 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Type Coverage Yes

### Embed Badge

![Health badge](/badges/zenstruck-filesystem/health.svg)

```
[![Health](https://phpackages.com/badges/zenstruck-filesystem/health.svg)](https://phpackages.com/packages/zenstruck-filesystem)
```

###  Alternatives

[laravel/framework

The Laravel Framework.

34.9k556.2M21.5k](/packages/laravel-framework)[league/flysystem-aws-s3-v3

AWS S3 filesystem adapter for Flysystem.

1.7k293.8M1.2k](/packages/league-flysystem-aws-s3-v3)[unisharp/laravel-filemanager

A file upload/editor intended for use with Laravel 5 to 10 and CKEditor / TinyMCE

2.2k3.6M90](/packages/unisharp-laravel-filemanager)[tempest/framework

The PHP framework that gets out of your way.

2.3k37.6k21](/packages/tempest-framework)[filegator/filegator

Filegator

3.0k1.3k](/packages/filegator-filegator)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

595.8M674](/packages/shopware-core)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
