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(5mo ago)4324.4k—1.7%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 3mo 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 1mo 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

47

—

FairBetter than 94% of packages

Maintenance70

Regular maintenance activity

Popularity40

Moderate usage in the ecosystem

Community16

Small or concentrated contributor base

Maturity49

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

92d 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

[league/flysystem-aws-s3-v3

AWS S3 filesystem adapter for Flysystem.

1.6k263.6M790](/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.3M74](/packages/unisharp-laravel-filemanager)[league/flysystem-local

Local filesystem adapter for Flysystem.

225231.8M39](/packages/league-flysystem-local)[league/flysystem-bundle

Symfony bundle integrating Flysystem into Symfony applications

40129.5M87](/packages/league-flysystem-bundle)[league/flysystem-sftp-v3

SFTP filesystem adapter for Flysystem.

6129.6M91](/packages/league-flysystem-sftp-v3)[league/flysystem-memory

In-memory filesystem adapter for Flysystem.

8533.6M194](/packages/league-flysystem-memory)

PHPackages © 2026

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