movex-ukraine/secure-array-files
A PHP package for securely storing arrays in files. It persists array data to disk while guarding against unsafe unserialization, offering Laravel apps a simple file-backed alternative to databases or caches for configuration and small datasets.
This is a Laravel Nova field that stores uploaded files as an array of objects on a single attribute (typically a json/text column) rather than as separate media records, and serves those files back through a download response. Install it like any Nova field:
composer require <vendor>/<package>
php artisan vendor:publish --tag=<package>-config # if a config is shipped
php artisan migrate # only if it ships a migration
Look first at the field's constructor/fluent API (make(), disk, path, accepted types, max size) and at the config file if one is published — that's where the storage disk and download behaviour are decided.
Minimum wiring:
json column to the resource's table.$casts so the array round-trips:
protected $casts = ['attachments' => 'array'];
FileArray::make('Attachments')
->disk('private')
->maxFiles(5);
First real use case: a record that needs a handful of supporting documents (contracts, receipts, photos) without spinning up a full Media model. You upload several files in one form, they land on a private disk, the field persists [{name, path, size, mime}], and the detail view exposes a download action that streams the file back through Laravel rather than a public URL.
Field definition per context. Define the field once for forms, then constrain it on index/detail so you don't render N download links per row:
FileArray::make('Attachments')
->disk('private')
->path('records/'.auth()->id())
->acceptedTypes(['.pdf', '.jpg', '.png'])
->maxSize(10240) // KB, if supported
->hideFromIndex();
Storing vs. appending. Most implementations treat the form submission as the source of truth: whatever is in the array after the request is what gets persisted. Decide early whether users replace the set or append to it — if you need append semantics with a create/update split, handle it in beforeSave/afterSave on the field or in the model observer rather than fighting the field.
Reading the array outside Nova. Because it's plain JSON on the model, you get full flexibility:
foreach ($record->attachments as $file) {
Storage::disk('private')->download($file['path'], $file['name']);
}
Wrap that in a controller + route and put the authorization in a policy — do not rely on the Nova field's download endpoint for non-Nova consumers.
Custom download behaviour. If the field exposes a download callback (->downloadUsing(fn ($file) => response()->download(...))) or a configurable route/controller, use it to inject authorisation checks, audit logging, or on-the-fly decryption. That is the cleanest extension point for "secure" requirements.
Testing. Use Storage::fake('private') and UploadedFile::fake()->create(...), then assert the persisted JSON shape rather than the raw disk contents. Assert on $record->attachments[0]['path'] so you're testing the contract, not the package's internal naming.
Backfilling data. If you're migrating from single-file columns or another media package, write a one-off command that maps old paths into the array-of-objects shape and run it before flipping the field over. Keep the legacy column until you've verified downloads.
array vs. object casting. A plain array cast gives you arrays, not objects — callbacks and view code that expect $file->path will break. Use AsArrayObject if the package's docs assume object access, and be consistent across Nova and your own controllers.private/local with no storage:link exposure. Verify a raw URL to the stored path 403s. If the package defaults to public, override it explicitly in every field definition.deleted hook and a diff in updating to remove files no longer present, otherwise the disk grows forever.json is fine; older text columns truncate silently at 64KB, which bites once you store many files or verbose metadata. Check the migration.->hideFromIndex() or a ->displayUsing() that renders a count.Content-Disposition and streaming. If downloads open inline in the browser or arrive with mangled filenames, the response headers (not the field) are the problem — check whether the package uses download() vs inline(), and whether filenames are sanitised for non-ASCII.acceptedTypes is advisory. Add server-side mimes:/max: rules in the Nova resource's rules() — MIME sniffing beats trusting the extension.config/filesystems.php, the stored path has no leading slash, the file actually exists (Storage::disk(...)->exists()), and the authenticated user passes the policy. Log the persisted array on save once — it's the fastest way to see what the package is really writing.How can I help you explore Laravel packages today?