Skip to content
LaravelBlog

laravel/framework: Enums now accepted in FilesystemManager::set(), forgetDisk() and purge()

LaravelBlogBot
LaravelBlogBot

Contributed by riesjart

Part of the framework v13.35.0 release

Laravel's filesystem manager has accepted backed enums in Storage::disk() for some time, but the companion methods that manage resolved disks — set(), forgetDisk() and purge() — still required plain strings. Passing an enum to any of them either crashed with a TypeError or, in the case of forgetDisk(), silently did nothing. Pull request #61868 fixes all three by normalizing enum arguments through the framework's enum_value() helper, bringing these methods in line with the enum support developers already rely on elsewhere in the Storage facade.

What changed

Three methods on Illuminate\Filesystem\FilesystemManager now accept enums in place of (or alongside) string names:

  • set($name, $disk) stores the instance under enum_value($name) instead of using the raw enum object as an array key.
  • forgetDisk($disk) normalizes each entry with enum_value() and switches its argument coercion from an (array) cast to Arr::wrap().
  • purge($name) resolves the name with enum_value() before unsetting; a null argument still falls back to the default driver, because enum_value() passes null straight through.

The argument docblocks were widened to \UnitEnum|string (and array<\UnitEnum|string> for forgetDisk()), the Storage facade docblock for set() was updated to match, and new tests cover all three methods using a string-backed enum.

The two bugs behind the fix

The first bug is the TypeError. PHP arrays can only be keyed by integers and strings, and enums are objects. With the old code, Storage::set(Disk::Backup, $fake) evaluated $this->disks[Disk::Backup] = $fake, and PHP threw TypeError: Cannot access offset of type Disk on array. The same applied to unset($this->disks[$name]) inside purge(). The inconsistency was the frustrating part: Storage::disk(Disk::Backup) worked, while Storage::purge(Disk::Backup) — a method you would naturally pair with it — crashed.

The second bug is subtler. forgetDisk() iterated with foreach ((array) $disk as $diskName). Casting a backed enum to an array does not produce a one-element array; it produces the enum's property table: ['name' => 'Backup', 'value' => 'backup']. The loop therefore iterated over the strings 'name' and 'value', and unset() removed keys that do not exist in $this->disks — a no-op. No exception was thrown, but the cached disk survived untouched. Arr::wrap() fixes this by wrapping non-array values as [$value], preserving the enum so enum_value() can convert it into its backing string.

What it looks like now

namespace App\Enums;

enum Disk: string
{
    case Backup = 'backup';
    case Archive = 'archive';
}
use App\Enums\Disk;
use Illuminate\Support\Facades\Storage;

// Inject a fake/mock disk (handy in tests) — threw a TypeError before
Storage::set(Disk::Backup, $fakeDisk);

// Force these disks to be re-resolved on next access — was a silent no-op before
Storage::forgetDisk([Disk::Backup, Disk::Archive]);

// Disconnect one disk and drop it from the cache — also threw before
Storage::purge(Disk::Backup);

Because enum_value(Disk::Backup) returns 'backup', the disk is keyed exactly as if you had passed the string. The new test testCanSetDiskUsingEnum demonstrates this: a disk set with the enum is immediately retrievable via $filesystem->disk('local'), so Storage::disk('backup') and Storage::disk(Disk::Backup) resolve the same instance.

Upgrade impact

This is a widening of accepted input, not a breaking change:

  • Existing string-based calls behave identically; enum_value() passes strings through untouched.
  • Code that previously crashed calling set() or purge() with an enum now works.
  • Code that "worked" by calling forgetDisk() with an enum was silently doing nothing; it now actually forgets the disk. That is the behavior you want, but worth knowing if any test depended on stale disk instances persisting.
  • purge() called with no argument (or null) still disconnects the default driver, since enum_value(null) resolves to null and the ?? fallback kicks in.
  • If you subclass FilesystemManager and override any of these methods, normalize arguments with enum_value() the same way the base class now does, so enum and string keys line up.

No configuration, container or environment changes are required — the upgrade is simply a matter of pulling the framework version.

Takeaways

  • FilesystemManager::set(), forgetDisk() and purge() now accept backed enums, matching the existing Storage::disk() behavior.
  • enum_value() converts enums to their string keys, so enum and string lookups share the same resolved-disk cache slots.
  • The (array) cast bug in forgetDisk() is fixed with Arr::wrap(), so enums are no longer exploded into name/value entries.
  • The change is purely additive: nothing to do for string usage, and only custom subclasses overriding these methods deserve a second look.

Category: Bug fix (9)

Sources

More from this release

Related Articles

Chore 4 mins read

laravel/framework: Downloads no longer crash on filenames without an ASCII equivalent

**Category:** Bug fix (9) PR #61888 fixes an `InvalidArgumentException` thrown by `Storage::download()`, `Storage::response()`, `Storage::serve()`, and `response()->streamDownload()` whenever the download filename contains characters with no ASCII equivalent — for example `請求書` or `😀`. After this change, those methods emit a `Content-Disposition` header with one underscore per character as the ASCII fallback, matching the behavior `response()->download()` already had via Symfony's `BinaryFileResponse`. The fix is two small edits to `fallbackName()` plus test coverage, and it should be a painless upgrade from v13.34.0. ## What broke When Laravel builds a download response for a non-ASCII filename, it produces both parameters of an RFC 5987/6266 `Content-Disposition` header: a plain `filename=` fallback for legacy clients, and `filename*=utf-8''...` carrying the real name percent-encoded. That fallback comes from `fallbackName()`, which pipes the name through `Str::ascii()`. For names that transliterate cleanly, this works: `привет.txt` becomes `privet.txt`. But some characters have no ASCII mapping at all. `Str::ascii('請求書')` returns an empty string, and the disposition logic then rejects the empty (or original, non-ASCII) fallback: ```php Storage::put('file.txt', 'Hello'); return Storage::download('file.txt', '請求書'); // InvalidArgumentException: // The filename fallback must only contain ASCII characters. ``` Meanwhile `response()->download()` already survived this exact input, because Symfony's `BinaryFileResponse` substitutes one `_` per character when the fallback comes back empty. The streamed and Storage paths had no such safety net, so the framework behaved inconsistently depending on which download method you picked. ## The fix Both `fallbackName()` implementations — in `Illuminate\Filesystem\FilesystemAdapter` and `Illuminate\Routing\ResponseFactory` — now check whether transliteration left anything behind, and substitute underscores if it didn't: ```php protected function fallbackName($name) { $fallback = str_replace('%', '', Str::ascii($name)); return $fallback !== '' ? $fallback : str_repeat('_', mb_strlen($name)); } ``` Note the use of `mb_strlen()`: the underscore count matches the character count of the original name, so `請求書` (three characters) yields `filename=___`, and a single emoji yields `filename=_`. The existing `str_replace('%', '', ...)` behavior — stripping percent signs so the fallback can't be mistaken for an encoded sequence — is untouched. ## Consistent headers across every download method After the fix, all four previously crashing paths produce the same header as `response()->download()`: ```php // Before response()->download($path, '請求書'); // filename=___ response()->streamDownload($cb, '請求書'); // Crash Storage::download('file.txt', '請求書'); // Crash // After — all three // attachment; filename=___; filename*=utf-8''%E8%AB%8B%E6%B1%82%E6%9B%B8 ``` The `filename*` parameter still carries the exact UTF-8 name, so modern browsers continue showing `請求書` in the save dialog. The underscores exist purely as the legacy `filename` fallback for clients that don't understand `filename*`. Critically, the fallback only engages when transliteration produces nothing at all. Names that already work don't change: `привет.txt` still becomes `privet.txt` (not `______.txt`), and `naïve résumé.txt` still becomes `naive resume.txt`. ## Test coverage Two new tests lock in the exact header. `FilesystemAdapterTest::testDownloadFilenameWithoutAsciiEquivalent` covers `Storage::download()`, and a brand-new `RoutingResponseFactoryTest` covers `streamDownload()`: ```php $response = $factory->streamDownload(fn () => null, '請求書'); $this->assertSame( 'attachment; filename=___; filename*=utf-8\'\'%E8%AB%8B%E6%B1%82%E6%9B%B8', $response->headers->get('content-disposition') ); ``` ## Upgrade impact For anyone upgrading from v13.34.0, this is a drop-in bug fix. There are no method signature changes, no config changes, and no new dependencies. The only behavioral difference is for filenames that previously threw the exception. If you worked around the crash — catching the `InvalidArgumentException`, or pre-sanitizing names with something like `Str::ascii($name) ?: 'download'` — you can remove that code and pass user-supplied filenames straight to the download methods. **Takeaways:** - `Storage::download()`, `Storage::response()`, `Storage::serve()`, and `response()->streamDownload()` no longer throw for filenames with no ASCII equivalent. - Empty transliteration fallbacks now become one `_` per character, matching Symfony's `BinaryFileResponse` and `response()->download()`. - Names that transliterate (e.g. `привет.txt` → `privet.txt`) behave exactly as before, and `filename*` still carries the true UTF-8 name. - Safe upgrade: no API or config changes; drop any try/catch workarounds you had around the old exception.

LaravelBlogBot
LaravelBlogBot