survos / storage-bundle
Symfony bundle for interacting with storage (via Flysystem)
Package info
github.com/survos/storage-bundle
Type:symfony-bundle
pkg:composer/survos/storage-bundle
Fund package maintenance!
Requires
- php: ^8.5
- ext-zip: *
- doctrine/doctrine-bundle: ^3.2
- doctrine/orm: ^3.7
- league/flysystem-bundle: ^3.5
- survos/data-contracts: ^2.0
- survos/jsonl-bundle: ^2.5
- survos/kit-bundle: ^2.5
- survos/tree-bundle: ^2.0
- survos/workflow-extras-bundle: ^2.43.23
- survos/workflow-messenger-bundle: ^2.43.23
- symfony/config: ^8.1
- symfony/dependency-injection: ^8.1
- symfony/http-kernel: ^8.1
- symfony/messenger: 8.2.*@dev
- symfony/security-core: ^8.1
- symfony/string: ^8.1
- twig/twig: ^3.4|^4.0
- zenstruck/bytes: ^1.0
Requires (Dev)
- league/flysystem-aws-s3-v3: ^3.29
- phpstan/phpstan: ^2.0
- phpstan/phpstan-symfony: ^2.0
- phpunit/phpunit: ^13.0
- survos/simple-datatables-bundle: ^2.5
- symfony/browser-kit: ^8.1
- symfony/console: ^8.1
- symfony/framework-bundle: ^8.1
- symfony/http-client: ^8.1
- symfony/phpunit-bridge: ^8.1
- symfony/stopwatch: ^8.1
- symfony/twig-bundle: ^8.1
- symfony/var-dumper: ^8.1
- symfony/yaml: ^8.1
Suggests
- survos/imgproxy-bundle: optional thumbnail and preview rendering for image objects
- survos/simple-datatables-bundle: searchable/sortable tables when browsing
Provides
None
Conflicts
None
Replaces
None
- dev-main
- 2.45.2
- 2.42.14
- 2.41.9
- 2.39.0
- 2.37.4
- 2.34.25
- 2.33.0
- 2.24.33
- 2.24.32
- 2.24.30
- 2.24.1
- 2.19.2
- 2.18.18
- 2.10.25
- 2.10.19
- 2.10.18
- 2.10.17
- 2.10.16
- 2.10.15
- 2.10.14
- 2.10.13
- 2.10.12
- 2.10.11
- 2.10.10
- 2.10.9
- 2.10.8
- 2.10.7
- 2.10.6
- 2.10.5
- 2.10.4
- 2.10.3
- 2.10.2
- 2.10.1
- 2.10.0
- 2.9.4
- 2.9.3
- 2.9.2
- 2.9.1
- 2.9.0
- 2.8.4
- 2.8.3
- 2.8.2
- 2.8.1
- 2.8.0
- 2.7.23
- 2.7.22
- 2.7.21
- 2.7.20
- 2.7.19
- 2.7.18
- 2.7.17
- 2.7.16
- 2.7.15
- 2.7.14
- 2.7.13
- 2.7.12
- 2.7.11
- 2.7.10
- 2.7.9
- 2.7.8
- 2.7.7
- 2.7.6
- 2.7.5
- 2.7.4
- 2.7.3
- 2.7.2
- 2.7.1
- 2.7.0
- 2.6.0
- 2.5.8
- 2.5.7
- 2.5.6
- 2.5.5
- 2.5.3
- 2.5.2
- 2.5.1
- 2.5.0
- 2.4.4
- 2.4.3
- 2.4.2
- 2.4.1
- 2.4.0
- 2.3.0
- 2.2.5
- 2.2.4
- 2.2.3
- 2.2.2
- 2.2.1
- 2.2.0
- 2.1.2
- 2.1.1
- 2.0.220
- 2.0.219
- 2.0.218
- 2.0.217
- 2.0.216
- 2.0.215
- 2.0.214
- 2.0.213
- 2.0.212
- 2.0.211
- 2.0.210
- 2.0.209
- 2.0.208
- 2.0.207
- 2.0.206
- 2.0.205
- 2.0.204
- 2.0.203
- 2.0.202
- 2.0.201
- 2.0.200
- 2.0.199
- 2.0.198
- 2.0.197
- 2.0.196
- 2.0.195
- 2.0.194
- 2.0.193
- 2.0.192
- 2.0.191
- 2.0.190
- 2.0.189
- 2.0.188
- 2.0.187
- 2.0.186
- 2.0.185
- 2.0.184
- 2.0.183
- 2.0.182
- 2.0.181
- 2.0.180
- 2.0.179
- 2.0.178
- 2.0.177
- 2.0.176
- 2.0.175
- 2.0.174
- 2.0.173
- 2.0.172
- 2.0.171
- 2.0.170
- 2.0.169
- 2.0.168
- 2.0.167
- 2.0.166
- 2.0.165
- 2.0.164
- 2.0.163
- 2.0.162
- 2.0.161
- 2.0.160
- 2.0.159
- 2.0.158
- 2.0.156
- 2.0.155
- 2.0.154
- 2.0.146
- 2.0.145
- 2.0.144
- 2.0.143
- 2.0.142
- 2.0.141
- 2.0.140
- 2.0.139
- 2.0.138
- 2.0.137
- 2.0.136
- 2.0.135
- 2.0.134
- 2.0.133
- 2.0.132
- 2.0.131
- 2.0.130
This package is auto-updated.
Last update: 2026-10-28 12:34:06 UTC
README
A Symfony bundle to interact with storage (via Flysystem). It exposes commands, controllers and twig utilities. All of the underlying storage happens through Flysystem.
Used in production by
(derived from showcase's Component/Site scan — apps whose composer.json requires survos/storage-bundle and have a deployed dokku site)
Quickstart
symfony new storage-demo --webapp && cd storage-demo composer require survos/storage-bundle
Optional thumbnail support:
composer require survos/imgproxy-bundle
Without imgproxy, the bundle still works normally and falls back to direct object links instead of generated image thumbnails/previews.
Configure Flysystem, including the relevant env vars if using something besides local
# config/packages/flysystem.yaml # Read the documentation at https://github.com/thephpleague/flysystem-bundle/blob/master/docs/1-getting-started.md flysystem: storages: default.storage: adapter: 'aws' # visibility: public # Make the uploaded file publicly accessible in S3 options: client: 'Aws\S3\S3Client' # The service ID of the Aws\S3\S3Client instance bucket: '%env(AWS_S3_BUCKET_NAME)%' streamReads: true prefix: '%env(S3_STORAGE_PREFIX)%' when@dev: flysystem: storages: default.storage: adapter: 'local' options: directory: '%kernel.project_dir%/public/storage'
symfony new storage-demo --webapp && cd storage-demo composer require survos/storage-bundle bin/console storage:config bin/console storage:list
You can browse interactively with the basic admin controller.
composer require survos/simple-datatables-bundle symfony server:start -d symfony open:local --path=/storage/zones
Storage zones (and their credentials) are configured in flysystem.yaml as shown above —
local during dev, S3/Bunny/etc. in production via env vars.
To see what got wired at runtime, run the read-only diagnostic:
bin/console storage:config
It dumps the resolved zones and their adapters (class, root/bucket), which is handy for confirming env vars resolved correctly. It makes no changes and needs no credentials.
Legacy note: earlier versions shipped a Bunny-specific
storage:config <api-key>that fetched per-zone keys from the Bunny dashboard. The bundle is now backend-agnostic (Flysystem), so that key-fetcher is gone;storage:configis purely diagnostic.
Your application now has a bare-bones controller located at /admin/storage, you may want to secure this route in security.yaml, or configure it in config/routes/survos_storage.yaml.
You also have access to a command line interface.
bin/console storage:list
+------------- museado/ -----+--------+ | ObjectName | Path | Length | +----------------+-----------+--------+ | photos finales | /museado/ | 0 | +----------------+-----------+--------+
Live browsing and persisted inventory
Flysystem browsing lists the current backend directly. Doctrine inventory stores StorageNode records with parent/child relationships; tree-bundle is required for hierarchy support. Browsing a zone does not require populating its inventory.
Populate through the StorageNode workflow
Flysystem remains authoritative. Doctrine stores a disposable inventory. A StorageNode is either a directory or a file; sidecars are ordinary files.
survos_storage: tracking_lock_factory: lock.workflow.factory storages: archive.storage: track: true sidecar_prefix: o content_patterns: ['o/*.json']
bin/console storage:populate archive.storage o bin/console messenger:consume storage.fetch --concurrency=8
Use a shared workflow lock store and configure the storage.fetch transport.
--concurrency needs Symfony Messenger's amphp/parallel support; otherwise run
multiple normal workers. sync must also be configured for explicit --sync use.
Persisting a node starts the built-in workflow Kickoff after commit. The initial
new place selects fetch_directory for directories or load_file for files.
Each directory transition performs one shallow listing, saves its children, and
lets their own Kickoff continue the traversal. Each file transition loads metadata
and, when its path matches content_patterns, stores the file bytes in
meta[StorageNode::CONTENT] and their SHA-256 in meta[StorageNode::CONTENT_SHA256].
File contents are fetched only inside the asynchronous file transition.
Running storage:populate again visits existing directories and queues files when
size/last-modified changed, contents are missing, or their loading is unfinished.
There is no separate cache rebuild. Delete the disposable StorageNodes and use
storage:populate to populate them again. --dispatch/--recursive are retained
for compatibility; --sync is only an explicit diagnostic execution scope.
Applications may listen for StorageNodeDiscoveredEvent during listing or
StorageFileLoadedEvent after loading, before the executor flushes. No AI-specific
listener or association to an original object is needed to cache a sidecar.
Mediary keeps one file per media ID and task, at the existing
MediaKeyService::archivePathFromKey($id, $task.'.json', 'o') path. Its
CachedSidecarReader derives that path and decodes the cached node contents.
The S3 payload is unchanged. Tasks remain independent because they are different
files. A cache miss falls back to storage; a cache hit performs no S3 request.
Tracked writes and changelog
track: true wraps the Flysystem adapter so directly injected operators and
StorageService writes are tracked alike. Successful writes, streams, moves,
copies, deletes and visibility changes append durable Doctrine journal rows.
Failed backend writes and reads append nothing. Apply the application migrations
for StorageChange and StorageJournalHead before enabling tracking. Journal rows
have no foreign keys to disposable StorageNodes. Redis is not required.
storage:changes:consume forwards journal entries to the same StorageNode workflow
and marks them dispatched. It never loads files itself. Mediary's Scheduler runs
this forwarding command every minute. A crash after dispatch can produce a safe
duplicate invalidation. Workers read the current file, so delayed messages do not
restore old content. Missing files are marked meta[StorageNode::EXISTS] = false
and their cached contents are removed. Per-node workflow locks serialize updates.
The admin-only GET /storage/changes?after=0&limit=100 endpoint returns events
and a next cursor. Without authorization it returns 403. Sequence allocation is
serialized through database commit; rows remain available after dispatch and
clients checkpoint after processing a page. There is no automatic pruning.
Filesystem writes and SQL journal inserts cannot be atomic. Journal failures are reported even if the file was saved: retry or explicitly record the successful write. Writes bypassing Flysystem require provider notifications or an explicit receipt; timestamps can detect changes on subsequent listings, but cannot prove content identity. ETags are opaque, not necessarily content hashes.
$journal->record('archive.storage', $relativeKey, revision: [ SidecarCache::ETAG => $etag, SidecarCache::VERSION_ID => $versionId, ]); // Maps confirmed bucket/key receipts through configured zone prefixes: $externalStorageWrite->record($bucket, $key, $revision);
storage:changes:record archive.storage path/to/object checks existence before
recording a manual receipt; --etag and --version-id are optional. Notification
provenance is saved under meta.storage_event_revision, separately from the hash
of the actual cached contents. Existing AI response provenance is left unchanged.
Mediary records Imagor originals only after its existing HEAD confirms persistence, including ETag/version when available. Resize HTTP success alone does not identify or confirm a persisted result object; its producer must supply a confirmed key before using the same recording API. No guessed resize keys are journaled.