PHPackages                             rasuvaeff/yii3-filestorage - 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. [HTTP &amp; Networking](/categories/http)
4. /
5. rasuvaeff/yii3-filestorage

ActiveLibrary[HTTP &amp; Networking](/categories/http)

rasuvaeff/yii3-filestorage
==========================

DI-native file storage for Yii3: one facade over swappable physical and metadata backends, PSR-7 streaming, safe delivery

v0.1.1(today)074↑2656.8%1BSD-3-ClausePHPPHP 8.3 - 8.5CI passing

Since Aug 8Pushed todayCompare

[ Source](https://github.com/rasuvaeff/yii3-filestorage)[ Packagist](https://packagist.org/packages/rasuvaeff/yii3-filestorage)[ Docs](https://github.com/rasuvaeff/yii3-filestorage)[ RSS](/packages/rasuvaeff-yii3-filestorage/feed)WikiDiscussions master Synced today

READMEChangelog (2)Dependencies (21)Versions (3)Used By (1)

rasuvaeff/yii3-filestorage
==========================

[](#rasuvaeffyii3-filestorage)

[![Latest Stable Version](https://camo.githubusercontent.com/f3f87d7e230aa1feee3ca75e47a6be10c4afa765cba06608db9bbecb827f0f93/68747470733a2f2f706f7365722e707567782e6f72672f7261737576616566662f796969332d66696c6573746f726167652f76)](https://packagist.org/packages/rasuvaeff/yii3-filestorage)[![Total Downloads](https://camo.githubusercontent.com/a9ad4408683f596f158f25c68aa3da0cc38c224fb73ec74287d48cf7cbc18a67/68747470733a2f2f706f7365722e707567782e6f72672f7261737576616566662f796969332d66696c6573746f726167652f646f776e6c6f616473)](https://packagist.org/packages/rasuvaeff/yii3-filestorage)[![Build](https://github.com/rasuvaeff/yii3-filestorage/actions/workflows/build.yml/badge.svg)](https://github.com/rasuvaeff/yii3-filestorage/actions/workflows/build.yml)[![Static analysis](https://github.com/rasuvaeff/yii3-filestorage/actions/workflows/static-analysis.yml/badge.svg)](https://github.com/rasuvaeff/yii3-filestorage/actions/workflows/static-analysis.yml)[![Psalm level](https://camo.githubusercontent.com/68f7f31799f2b93c710b14ba3877072e7fe07ec9d7cee3fdf67e14beab3e1b6f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f7073616c6d2d6c6576656c5f312d626c75652e737667)](https://github.com/rasuvaeff/yii3-filestorage/actions/workflows/static-analysis.yml)[![PHP](https://camo.githubusercontent.com/7aad14eeb3a3841aca3445e9182bd0b36be70a5f18b4c8fe532812bcb0ae9c90/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646570656e64656e63792d762f7261737576616566662f796969332d66696c6573746f726167652f706870)](https://packagist.org/packages/rasuvaeff/yii3-filestorage)[![License](https://camo.githubusercontent.com/6cb285b57819f8de0acfb34923298f4f569f962544e8fe35331da2d163f4e485/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4253442d2d332d2d436c617573652d626c75652e737667)](LICENSE.md)[Русская версия](README.ru.md)

One facade — `add()`, `find()`, `remove()`, `stream()`, `urlFor()` — over a swappable physical backend and a swappable metadata backend. PSR-7 streaming end to end, authoritative MIME detection, per-group accept rules, and delivery that does not accidentally serve an uploaded HTML file from your own origin.

> Using an AI coding assistant? [llms.txt](llms.txt) contains a compact API reference you can share with the model. Projects using the [llm/skills](https://github.com/roxblnfk/skills) Composer plugin also get this package's agent skill synced into `.agents/skills/` automatically on install.

**Status: `0.x`.** The API may still change while the database and Flysystem backends are built against it. The path layout and the signed-token format are already frozen — see [Frozen decisions](#frozen-decisions).

Requirements
------------

[](#requirements)

- PHP 8.3+
- `ext-fileinfo`
- `psr/clock` ^1.0
- `psr/http-message` ^2.0, `psr/http-factory` ^1.0 (and a PSR-17 implementation in your application, e.g. `nyholm/psr7` or `httpsoft/http-message`)
- `symfony/console` ^6.4 || ^7.0 || ^8.0
- `symfony/mime` ^6.4 || ^7.0 || ^8.0
- `yiisoft/files` ^2.0

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

[](#installation)

```
composer require rasuvaeff/yii3-filestorage
```

The core is intentionally incomplete on its own: it binds the facade and its own services, but **not** `StoreInterface` and **not** `RepositoryInterface`. Those come from a backend package, or from your application.

You wantInstall / bindFiles on local disk, metadata in a databasethis package + `rasuvaeff/yii3-filestorage-db`, bind `FileSystemStore`Files on S3/GCS/Azurethis package + `rasuvaeff/yii3-filestorage-flysystem`Signed download URLs, uploads, `Range` supportadd `rasuvaeff/yii3-filestorage-web`Just trying it outbind `FileSystemStore` and `Test\MemoryRepository` yourself (below)Usage
-----

[](#usage)

### Wiring

[](#wiring)

```
// config/common/di/filestorage.php
use Psr\Http\Message\StreamFactoryInterface;
use Rasuvaeff\Yii3Filestorage\Repository\RepositoryInterface;
use Rasuvaeff\Yii3Filestorage\Store\FileSystem\FileSystemStore;
use Rasuvaeff\Yii3Filestorage\Store\StoreInterface;
use Rasuvaeff\Yii3Filestorage\Test\MemoryRepository;

return [
    StoreInterface::class => static fn (StreamFactoryInterface $streams): StoreInterface
        => new FileSystemStore(
            name: 'upload',
            rootPath: '/app/runtime/upload',
            streamFactory: $streams,
        ),

    // Development only — every record is lost when the process ends.
    RepositoryInterface::class => MemoryRepository::class,
];
```

Then check it:

```
./yii filestorage:check
```

### Storing

[](#storing)

```
use Rasuvaeff\Yii3Filestorage\StorageInterface;
use Rasuvaeff\Yii3Filestorage\Upload;

// From an HTTP upload.
$file = $storage->add(
    Upload::fromUploadedFile($request->getUploadedFiles()['avatar'], $streamFactory),
    groupName: 'avatars',
);

// From something your application generated — a rendered PDF, an export.
$file = $storage->add(
    Upload::fromStream($pdfStream, 'invoice-2026-08.pdf', $streamFactory),
    groupName: 'documents',
    description: 'August invoice',
    metadata: ['invoiceId' => 4711],
);

// From a path.
$file = $storage->add(Upload::fromPath('/tmp/import.csv', $streamFactory));
```

`$file` is an immutable `File`: `id`, `storeName`, `groupName`, `relativePath`, `originalName`, `mimeType`, `size`, `description`, `contentHash`, `metadata`, `createdAt`, `updatedAt`. `toArray()` / `fromArray()` round-trip exactly, including microseconds.

### Reading

[](#reading)

```
$file = $storage->find($id);

$stream = $storage->stream($file);   // PSR-7, the default read path
$bytes  = $storage->content($file);  // capped; throws ContentTooLargeException
$there  = $storage->exists($file);   // are the bytes still physically there
```

### URLs

[](#urls)

```
$url = $storage->urlFor($file);                    // the one to call
$url = $storage->urlFor($file, $expiresAt);        // explicit expiry
```

`urlFor()` applies the group's delivery policy and then tries, in order:

1. a permanent public URL — **only** if the group explicitly allows one;
2. a store-native presigned URL (S3 via `-flysystem`);
3. the application's signed proxy URL (`-web`).

`url()` and `temporaryUrl()` expose steps 1 and 2 for infrastructure code. Raw is only half right: `url()` ignores the delivery policy entirely, while `temporaryUrl()` hands the group's `DeliveryOptions` to the store and gets null back when the store cannot honour them. What neither consults is `allowDirectPublicUrl` — that gate belongs to `urlFor()`. Application code and templates should not branch on whether the store happens to be public — that is what `urlFor()` is for.

### Groups and policies

[](#groups-and-policies)

A group is a use case, not a folder. Give it accept rules once, in `params`, instead of re-validating before every `add()`:

```
// config/common/params.php
return [
    'rasuvaeff/yii3-filestorage' => [
        'defaultGroup' => 'common',
        'policies' => [
            'avatars' => [
                'allowedMimeTypes' => ['image/jpeg', 'image/png', 'image/webp'],
                'maxBytes' => 5_242_880,
                'maxPixels' => 40_000_000,
                'requireImageDimensions' => true,
            ],
            'documents' => [
                'allowedMimeTypes' => ['application/pdf'],
                'maxBytes' => 52_428_800,
            ],
            '*' => ['maxBytes' => 20_971_520],
        ],
        'delivery' => [
            '*' => ['allowDirectPublicUrl' => false, 'forceDownload' => true],
        ],
    ],
];
```

An upload that fails its policy throws `PolicyViolationException` **before the store is touched**, so nothing is written.

### Parameters

[](#parameters)

KeyDefaultMeaning`defaultGroup``common`Group used when `add()` is not given one`maxInlineBytes`8 MiBCap enforced by `content()``integrityHashMaxBytes``0``0` leaves `contentHash` null; a positive value opts into a bounded SHA-256`defaultUrlTtl``PT1H`Expiry `urlFor()` uses when none is given`extensionOverrides``[]`Media type ⇒ extension, over the `symfony/mime` table`policies``['*' => …]`Per-group accept rules`delivery``['*' => …]`Per-group delivery rules### Failure handling

[](#failure-handling)

Every `add()` is individually all-or-nothing: either a metadata row with its object, or nothing.

What failedWhat you getPolicy rejects the upload`PolicyViolationException`; nothing writtenByte cap crossed while copying`UploadTooLargeException`; the partial object is removedMetadata save fails after the write`AddException`; the object is deleted on a best-effort basisObject delete fails after the row is gone`RemoveException`; the object is an orphan for `filestorage:gc`There is no `addMany()`. An atomic batch is impossible over a filesystem or an object store, and a method with that name would promise one. Loop over `add()`and handle partial failure explicitly.

### Deduplication contracts

[](#deduplication-contracts)

`Storage` never shares bytes: every `add()` owns one unique object, which is why compensation may safely delete what it just wrote. Content-addressed sharing is a different lifecycle and lives in `rasuvaeff/yii3-filestorage-db` — but the contracts it coordinates are declared here, so a consumer can implement or fake them without depending on the database package.

TypeRole`Store\BlobLedgerInterface`Who owns shared bytes, and for how long. Reserve → publish → commit, with removal only ever *scheduling* a blob`Store\BlobId`Physical ownership: one object in one store. Never a content hash, which is identical across stores, groups and tenants`Store\BlobState``writing`, `active`, `pending_delete`, `deleting` — the four states every dedup failure happens between`Store\BlobToken`An opaque, ledger-issued handle. One per claim, so a crashed writer releases only its own`Store\BlobReservation`A writer's expiring claim while bytes are being published. Several may coexist on one blob`Store\BlobLease`Exclusive, expiring permission to delete. At most one per blob; stealable once it runs out, which is how a crashed collector is recovered`Store\BlobRecord`A read-only snapshot of a ledger row, for `gc`, `verify` and `stat``Exception\BlobBusyException`Transient: the blob is being deleted, retry after the lease ends`Exception\LedgerException`Not transient: the reservation, file or content does not match what the ledger holdsThe rule the whole design turns on: **shared bytes are never deleted inside a request.** The last release only marks a blob `pending_delete`; only the collector deletes, only under a lease, and only while the reference and reservation counts are still zero in the same statement that claims it.

### Tenant scope

[](#tenant-scope)

Two contracts, both optional, both unbound by default:

InterfaceBound byAnswers`Repository\FileScopeProviderInterface`your application"which tenant is this request?" — from `rasuvaeff/yii3-tenancy`, a session, a subdomain, or a constant`Repository\ScopedFileResolverInterface``rasuvaeff/yii3-filestorage-db`"give me this file *in this scope*"The second exists because a signed download has no ambient tenant — that is the point of signing it — and the tempting fix is to turn the tenant filter off for downloads. That fix is a cross-tenant read of every file whose id leaks. Instead the scope travels inside the signed token (`SignedPayload::$scopeId`) and is matched as a second predicate.

Frozen decisions
----------------

[](#frozen-decisions)

Two things cannot change after the first release, so they are settled now.

**Path layout — one directory per file.** Every generator emits `//original.`, never a bare filename, and `delete()` removes the *directory*. That is what lets a thumbnail live at `/thumb.webp` with no schema change, and what stops derivatives leaking when a file is deleted. A rendition is described by a `DerivativeDescriptor` — a *named* preset, never free-form dimensions, because free-form parameters turn one upload into an unbounded set of addressable derivatives — and a store reports one back as a `DerivativeObject`. The extension comes from the **detected** media type through the `symfony/mime`table; the client filename never contributes it, and an unrecognised type becomes `original.bin`.

**Token format.** `v1....`, HMAC-SHA256 over everything before the signature. The payload is canonical JSON `{fileId, variant, scopeId}` — the variant is inside the signature, so a token minted for a redacted or thumbnail rendition cannot be replayed for the original. Key ids are part of the authenticated envelope, so rotation keeps unexpired URLs valid while the previous key is still in the ring.

Extending it
------------

[](#extending-it)

`StorageInterface` is a plain interface with a `final` implementation, so quotas, metrics, tracing and antivirus scanning are **decorators you write**, not packages you install:

```
final readonly class QuotaStorage implements StorageInterface
{
    public function __construct(private StorageInterface $inner, private Quotas $quotas) {}

    public function add(Upload $upload, ?string $groupName = null, /* … */): File
    {
        $this->quotas->assertRoom($upload->size());

        return $this->inner->add($upload, $groupName, /* … */);
    }

    // … delegate the rest
}
```

Bind your decorator to `StorageInterface` in the application layer, and take the inner one as `Storage`, not as `StorageInterface`:

```
// config/common/di/filestorage.php
use Rasuvaeff\Yii3Filestorage\Storage;
use Rasuvaeff\Yii3Filestorage\StorageInterface;

return [
    StorageInterface::class => static fn (Storage $inner, Quotas $quotas): StorageInterface
        => new QuotaStorage($inner, $quotas),
];
```

Core binds two ids for one facade: `Storage::class` builds it, and `StorageInterface::class` is an alias to that. `Storage::class` is the id that still means "the plain facade" after the application has rebound the interface — a decorator asking for `StorageInterface` would be handed itself, and the container answers `CircularReferenceException` for a recipe that reads perfectly. The constructor above stays typed `StorageInterface` so the class is testable with any double; only the wiring names the concrete id.

Do not fork the core for this.

Testing your own code
---------------------

[](#testing-your-own-code)

Test doubles ship in `src/`, not `tests/`, so they are actually installed:

ClassUse`Test\InMemoryStore`A store with no disk. Implements the base contract and maintenance only — **not** URLs or ranges, so you can test what your code does when a store cannot presign`Test\MemoryRepository`Metadata in an array`Test\MemoryBlobLedger`The dedup state machine in an array: revival, reservation expiry, lease stealing, conditional completion. What it cannot reproduce is concurrency — a PHP array has no isolation levels — so proving two writers race correctly still needs the databaseThere is deliberately no clock double here: `InMemoryStore` and `Storage` take any PSR-20 clock, and a Yii application already has `Yiisoft\Test\Support\Clock\StaticClock`. Shipping a second one would be duplication, not convenience.

```
$store = new InMemoryStore('test', $streamFactory, new StaticClock($now));
$storage = new Storage(
    stores: new StoreRegistry([$store]),
    repository: new MemoryRepository(),
    // …
);

$file = $storage->add(Upload::fromStream($stream, 'a.txt', $streamFactory));

Assert::same($store->writeCount(), 1);
Assert::same($store->bytesAt($file->relativePath), 'hello');
```

Security
--------

[](#security)

BoundaryRuleMedia typeOnly `finfo` output is authoritative. The client-supplied type is kept for diagnostics and never reaches a policy, a path, or a response headerPathsAlways generated, never taken from a request. `StoredObjectId` rejects `..`, NUL, backslashes and absolute paths; local stores re-check containment with `realpath()` after resolving, so a planted symlink cannot read outside the rootOriginal filenameMetadata only. It never enters a path, and CR/LF/NUL are stripped before it reaches a headerIngress sizeA non-seekable upload is spooled with a finite cap; stores enforce the group's `maxBytes` *while copying* and remove partial outputDecompression bombs`maxPixels` is checked from the image header via `getimagesizefromstring()`. Pixels are never decodedDirect public URLsOff by default. `filestorage:check` **fails** when a group combines them with a permissive or active-content allow-listSigned URLsHMAC-SHA256 over version, key id, expiry and canonical payload; strict length and schema checks; `hash_equals()`; key-ring rotation; keys shorter than 32 bytes are a configuration errorDeduplicationOwnership is a physical `BlobId`, never a hash count — a hash is content identity and says nothing about who owns the bytes`finfo` recognising `image/svg+xml` says nothing about whether serving it inline is safe: an SVG served from your own origin is a stored-XSS primitive. Keep SVG out of any group that allows direct public URLs, and let `-web` force a download.

Uploaded images keep their EXIF, which includes GPS coordinates on most phone photos. Nobody expects an avatar upload to publish their home address — strip or re-encode if the files will be served publicly.

Console
-------

[](#console)

CommandDoes`filestorage:check`Reports wiring, per-store capabilities, tenancy and per-group rules; fails on an unsafe delivery combination or on tenant mode with no scoped resolver`filestorage:stat`Counts and sizes by group, plus how much sharing has saved (physical figures withheld under tenancy)`filestorage:verify`Reports rows whose object is missing; `--deep` re-reads each one and compares its hash`filestorage:backfill-hash`Fills in `contentHash` on rows written before integrity hashing was on`filestorage:gc`Collects unreferenced shared blobs, and with `--orphans` sweeps objects no row points at`filestorage:import `Ingests a directory tree through the ordinary write path, skipping what a manifest says is already imported`gc`, `backfill-hash` and the `-db` package's `deduplicate` **report by default and act only under `--apply`**. A command whose first run deletes is one somebody eventually runs against the wrong database. `verify` has no `--apply` at all: what to do about a missing object — restore, re-upload, delete the row — is not a decision a command should make for you.

All four page by id and print the last one they reached, so a table too large for one run is a sequence of bounded runs:

```
./yii filestorage:verify --limit=10000
# Last id: 019603f2-…
./yii filestorage:verify --limit=10000 --after=019603f2-…
```

`gc`, `verify`, `backfill-hash` and `stat` need a backend implementing `MaintenanceRepositoryInterface` (`-db` does). `gc` additionally collects shared blobs only when a `BlobLedgerInterface` is bound; without one it still sweeps orphans. Under `--apply` it is the **only** thing in this family that deletes bytes another request might want, and it does so under an exclusive, expiring lease — see the deduplication section of `-db`.

Order matters after enabling deduplication: `deduplicate --apply` repoints the rows, and the objects they used to point at become orphans that `gc --orphans --apply` reclaims. Under tenancy that last step moves — `deduplicate`runs per tenant under the ambient scope, while the sweep refuses under a bound scope provider and runs once with it unbound (see below).

### Importing an existing directory tree

[](#importing-an-existing-directory-tree)

`filestorage:import` walks a directory and puts every file through `add()` — the same policy check, MIME detection, store write and metadata row as a live upload. It never inserts a row or writes an object directly, which is also why an application that enabled deduplication gets deduplicated imports for free: the command resolves `StorageInterface`, and that is the key such an application overrode.

```
./yii filestorage:import /srv/legacy/invoices                          # a report
./yii filestorage:import /srv/legacy/invoices --group=documents --apply
./yii filestorage:import /srv/legacy/avatars  --group=avatars   --apply
```

OptionDefaultNotes`--apply`offWithout it nothing is imported and the manifest is untouched`--group``defaultGroup`One group for the whole run. Different groups are different runs over different subdirectories`--store`the default storeWhich physical store to write to`--limit`1000Files per run. It bounds the work, not the memory — the listing is built and sorted up front, so it is proportional to the whole tree`--manifest``build/filestorage-import.jsonl`Where completed imports are recorded, and read back to skip them**The manifest is what makes a second run safe.** `add()` has no natural key, so without it a re-run writes a second row *and* a second object for every file. The manifest is JSON Lines, one `{"source":…,"path":…,"id":…}` per completed file, flushed as it goes — so a run that was killed halfway leaves what it finished recorded, and the next run picks up only what is new. Keep it, or a later run duplicates everything. Entries are matched on `source`, the absolute path, so **one manifest covers several roots**: the two commands above share the default and neither skips the other's files, even when both trees hold a `notes.txt`.

A file whose policy rejected it is reported, skipped, and **not** recorded — so the run after you widen the policy retries exactly those. The command exits non-zero when anything failed.

Symbolic links are not followed (a link in a legacy tree can point outside the directory you named), entries beginning with a dot are skipped at every depth, and the source path is stored in the row's `metadata['importSource']`.

### Tenancy and the orphan sweep

[](#tenancy-and-the-orphan-sweep)

`filestorage:check` reports which mode you are in, and **fails** when a `FileScopeProviderInterface` is bound with nothing binding `ScopedFileResolverInterface`: a signed download then has no scoped way to resolve a file, and resolving by id alone reads any file whose id leaks. Install a repository backend that ships a resolver (`-db` does), or unbind the scope provider if the installation is not multi-tenant. The reverse — a resolver with no scope provider — is the ordinary single-scope case and is fine.

**`gc --orphans` refuses to run when a `FileScopeProviderInterface` is bound**, and the refusal is deliberate rather than a limitation to work around. An object is an orphan when *no* row anywhere points at it. The referenced-set comes from the repository, which filters by the current tenant; the object listing is physical and filters by nothing. Comparing the two under tenancy classifies every other tenant's objects as orphans, and `--apply` deletes them. There is no tenant to run it "as", because no single tenant's rows can prove an object unreferenced.

Run the sweep from a maintenance entry point that leaves the scope provider unbound, where the repository sees every row. Blob collection is unaffected: the ledger is keyed by physical identity, not by tenant, so plain `gc --apply`works in either kind of installation.

`filestorage:stat` reads through the same scoped repository and splits along the same line, one step milder. Counts and byte totals are *logical* — they describe rows, and one tenant's rows are a correct answer to "how much does this tenant have", so they are still printed, under a `Group (current scope)` header. "Distinct objects" and the sharing savings are *physical*: they claim to know how many objects exist and how many rows point at each, and a tenant-filtered walk cannot see the rows in other scopes pointing at the same ones. It would report more distinct objects than exist and less sharing than there is, so under a bound scope provider **both are withheld** rather than estimated. Run `stat` with the provider unbound for the physical figures.

Examples
--------

[](#examples)

Runnable scripts live in [examples/](examples/README.md).

Development
-----------

[](#development)

```
make build          # validate + normalize + require-checker + cs + psalm + test
make cs-fix
make psalm
make test
make test-coverage
make mutation
make release-check
```

No PHP on the host — everything runs in the `composer:2` Docker image.

License
-------

[](#license)

BSD-3-Clause. See [LICENSE.md](LICENSE.md).

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance100

Actively maintained with recent releases

Popularity13

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity42

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% 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 ~0 days

Total

2

Last Release

0d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/b0812d5572a7041dfe36e222d295b2e6dc55833a605350fcde58a51a5965ed30?d=identicon)[rasuvaeff](/maintainers/rasuvaeff)

---

Top Contributors

[![rasuvaeff](https://avatars.githubusercontent.com/u/1352718?v=4)](https://github.com/rasuvaeff "rasuvaeff (17 commits)")

---

Tags

abstractiondifilesfilestoragemetadataphppsr-16storageyii3psr-7filesystemuploadyii3file storage

###  Code Quality

Static AnalysisPsalm, Rector

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/rasuvaeff-yii3-filestorage/health.svg)

```
[![Health](https://phpackages.com/badges/rasuvaeff-yii3-filestorage/health.svg)](https://phpackages.com/packages/rasuvaeff-yii3-filestorage)
```

###  Alternatives

[flow-php/flow

PHP ETL - Extract Transform Load - Data processing framework

86337.5k](/packages/flow-php-flow)[guzzlehttp/psr7

PSR-7 message implementation that also provides common utility methods

7.9k1.1B4.3k](/packages/guzzlehttp-psr7)[tempest/framework

The PHP framework that gets out of your way.

2.3k37.6k19](/packages/tempest-framework)[shopware/core

Shopware platform is the core for all Shopware ecommerce products.

595.8M656](/packages/shopware-core)[shopware/platform

The Shopware e-commerce core

3.4k1.5M3](/packages/shopware-platform)[typo3/cms

TYPO3 CMS is a free open source Content Management Framework initially created by Kasper Skaarhoj and licensed under GNU/GPL.

1.2k1.9M122](/packages/typo3-cms)

PHPackages © 2026

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