Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

File System Bundle Laravel Package

kitpages/file-system-bundle

Kitpages FileSystemBundle is a Symfony bundle for working with files and directories through reusable filesystem services and helpers, simplifying common storage and file-management tasks in your applications.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the stack check. kitpages/file-system-bundle is a Symfony 2–era bundle: no ServiceProvider, no facade, no publishable config. On a Laravel app Composer will autoload the classes, but nothing binds into the container — you'd hand-wire every adapter. On Laravel the realistic move is to read it, don't require it, and reproduce its one good idea on top of Storage/Flysystem. If you are on a legacy Symfony 2/3 codebase, install it properly:

composer require kitpages/file-system-bundle
// app/AppKernel.php
new Kitpages\FileSystemBundle\KitpagesFileSystemBundle(),

Where to look first. Skip the README and read two files: Service/FileSystem/FileSystemInterface and Service/FileSystemManager. They define the actual contract — a thin set/get/delete/exists surface over a path, not a content string. That is the whole package. The rest is DI plumbing.

Configure one named filesystem before writing code. The config model is "adapter + adapter_config, addressed by name":

# app/config/config.yml
kitpages_file_system:
    file_system_list:
        uploads:
            adapter: local
            adapter_config:
                base_dir: "%kernel.root_dir%/../var/uploads"

Treat the DI keys as unverified until you read DependencyInjection/Configuration.php — this bundle predates strict config validation, so a typo'd key silently does nothing.

First use case worth wiring. A stream-to-stream copy that never materializes the file:

$source = fopen($tmpUpload, 'rb');
$target = fopen($destPath, 'wb');
stream_copy_to_stream($source, $target);
fclose($source);
fclose($target);

That three-line pattern is the package's entire value proposition (2 GB file, 128 MB memory limit). Prove it to yourself on a large file in a throwaway app before deciding anything else. The Laravel-native equivalent is one line: Storage::writeStream($path, fopen($tmp, 'rb')).


Implementation Patterns

Stream-in, stream-out at the boundary. The rule the package enforces, and the one worth enforcing in review, is: no method signature accepts or returns file content. Accept a path or a resource; return a path or a resource. If a helper's parameter is named $content or $body and typed string, it is a latent OOM.

Laravel translation table. Everything the bundle does has a maintained equivalent:

Bundle concept Laravel / Flysystem 3
FileSystemManager + named FS Storage::disk('uploads')
set(path, file) Storage::putFileAs(...) / writeStream
get(path) Storage::readStream($path)
delete / exists Storage::delete / Storage::exists
Streaming download response()->streamDownload(...) or Storage::download
S3 backend league/flysystem-aws-s3-v3

Custom adapter contract, if you do mirror it. Keep it typed and PSR-friendly for PHP 8.1+, and make "never a string" structural, not a convention:

interface FileStoreInterface
{
    public function putStream(string $path, $stream): void;   // resource, not string
    public function getStream(string $path);                  // returns resource
    public function delete(string $path): void;
    public function exists(string $path): bool;
    public function url(string $path): string;                // presigned where possible
}

Atomic writes: temp file + rename(). Write to uploads/.tmp/<uuid> on the same filesystem, fsync if it matters, then rename() into place. rename() within a filesystem is atomic; a direct fopen($dest, 'wb') leaves a truncated file visible to readers the instant the first byte lands. For S3, atomicity = multipart completion, not the first UploadPart.

Attaching files to entities. In Symfony this meant a Doctrine listener; in Laravel just store the relative path in a column and keep bytes out of the database and out of model events:

$path = $request->file('doc')->storeAs('uploads', $name, 'local'); // returns path
$report->update(['document_path' => $path]);

Never generate a second write inside a model observer to "normalize" the file — that's where double reads and temp leakage start.

Server-side moves belong on a queue. A large copy between two storage backends pins a PHP-FPM worker for the entire transfer duration. Dispatch it, don't inline it in the request that created the record. Same for checksum generation.

Presigned URLs beat proxying. For anything user-facing and large, the best "leverage" is to not touch the bytes at all: hand the client a presigned PUT/GET. PHP then serves zero megabytes, and memory, throughput, and worker-exhaustion concerns all evaporate at once.


Gotchas and Tips

Do not attempt to co-install this in a Laravel 10/11 app. Pinned symfony/* ~2.x constraints conflict with the Symfony 6/7 components Laravel ships; expect a resolver fight, PSR-0-style autoloading, PHP 5.3–5.6 idioms (each(), create_function), and possibly a transitive AWS SDK v1/v2 with its own CVE history. That transitive SDK is a bigger risk than the bundle itself.

Streaming solves memory, not throughput. A 2 GB upload at 10 MB/s occupies a worker for ~200 seconds. Under concurrency you hit worker starvation long before OOM. Review max_execution_time, request_terminate_timeout, and PHP-FPM pool sizing alongside any streaming change.

Close handles in finally. The most common production incident with this style of code is leaked file handles and orphaned temp files on the exception path — which surfaces later as disk exhaustion and unrelated cascading failures:

$src = fopen($a, 'rb');
try {
    $dst = fopen($b, 'wb');
    try { stream_copy_to_stream($src, $dst); } finally { fclose($dst); }
} finally { fclose($src); }

Silence the output buffer before streaming a response. ob_end_clean() (repeatedly, until ob_get_level() is 0), session_write_close() to release the session lock, and set_time_limit(0) for long transfers. Without the session write-close, one streaming download serializes every other request from that user.

No checksums, no idempotency, no range requests. The original design verifies nothing: a truncated transfer looks like success. Add hash_file('sha256', ...) after write and compare before promoting the temp file. No range support also means no video seeking and no resumable downloads — a hard requirement that needs a real object store, not this pattern.

Guard the OOM regression with a test. The failure mode of an abstraction like this is silent: a future refactor "helpfully" reads the file into a string, tests still pass, and production dies at 2 GB. Write a test that sets a low memory_limit, generates a file well above it, and asserts the copy succeeds:

public function test_streams_beyond_memory_limit(): void
{
    $limit = ini_get('memory_limit');
    ini_set('memory_limit', '32M');
    try {
        // create ~64M file, copy via the adapter, assert hash matches
    } finally {
        ini_set('memory_limit', $limit);
    }
}

Config keys fail silently. Because the bundle predates strict validation, a misspelled adapter_config key doesn't error — it defaults, and files land somewhere unexpected. After any config change, dump the resolved service and confirm base_dir.

Verify PHP 8 compatibility before writing a line. Zero dependents means zero downstream pressure: no PHP 8 fixes, no security triage, no issue responses. Assume you are the sole maintainer of any fork, and budget the AWS SDK / Symfony contract upgrades on your own roadmap.

License is clean (MIT) — forking and vendoring internally is legally unencumbered. That is the one thing about adoption here that carries no risk.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings