In Laravel, what do scoped and read-only disks add on top of an existing disk, and when would you create a disk at runtime with Storage::build()?
answer
- wrappers, not new storage
- scoped needs disk plus prefix
- read-only => true wraps any disk
- extra Flysystem packages per wrapper
- build(): string root means local
basics
~20 sA scoped disk prefixes every path of another disk; a read-only disk (read-only => true) rejects writes. Storage::build() creates an uncached disk from a config array or a local root path, for settings known only at runtime.
solid answer
~40 sA disk with `'driver' => 'scoped'` needs `disk` (a disk name or config array) and `prefix`; Laravel copies the parent config, adds the prefix through Flysystem's `PathPrefixedAdapter`, and can override `visibility` and `throw`, so `Storage::disk('portfolio-9')->put('cover.jpg', ...)` lands at `portfolios/9/cover.jpg` on the parent. Adding `'read-only' => true` to any disk wraps its adapter so writes, deletes and visibility changes fail, and with `throw` off `put()` just returns `false`. Each wrapper needs its Flysystem package: `league/flysystem-path-prefixing` or `league/flysystem-read-only`. `Storage::build()` takes a config array, or a string that becomes the root of a local disk, and returns a disk that is not cached by name; it suits settings known only at runtime, such as a per-designer scoped disk or a customer's SFTP server.
code
php · 19 lines<?php
use App\Models\Designer;
use Illuminate\Support\Facades\Storage;
function portfolioDisk(Designer $designer)
{
return Storage::build([
'driver' => 'scoped',
'disk' => 's3',
'prefix' => "portfolios/{$designer->id}",
]);
}
// Written to portfolios/{id}/cover.jpg in the bucket
portfolioDisk($designer)->put('cover.jpg', $bytes);
// A string builds a local disk rooted at that path
$work = Storage::build(storage_path('app/tmp/export-'.$designer->id));go deeper
Recall that a scoped disk adds a path prefix to another disk and that read-only => true blocks writes.
Explain how the scoped driver merges the parent config, which extra packages each wrapper needs, and what put() returns on a read-only disk.
Use Storage::build() for runtime disks such as per-tenant scopes or customer SFTP servers, and pair scopes with authorization instead of trusting the prefix.
Decide which storage boundaries belong in reviewed configuration and which may be built from data, and how that affects isolation between tenants.
## Wrappers around a disk Both **scoped** and **read-only** disks decorate another disk rather than storing anything themselves. Laravel builds them on Flysystem decorators, adapters that wrap another adapter and change its behaviour: | Feature | How you declare it | Flysystem decorator | Extra package | |---|---|---|---| | Scoped | `'driver' => 'scoped'`, `'disk' => ...`, `'prefix' => ...` | `PathPrefixedAdapter` | `league/flysystem-path-prefixing` | | Read-only | `'read-only' => true` on any disk config | `ReadOnlyFilesystemAdapter` | `league/flysystem-read-only` | ## Scoped disks A design-portfolio site might keep every designer's files under `portfolios/{id}/` in one bucket. A scoped disk makes that prefix automatic: ```php 'portfolio-samples' => [ 'driver' => 'scoped', 'disk' => 's3', 'prefix' => 'portfolios/samples', ], ``` What `createScopedDriver` does: 1. Throws `InvalidArgumentException` if `disk` or `prefix` is missing. 2. Takes the parent's config (by name, or the array you gave). 3. Sets `prefix`, joining it to any prefix the parent already has with the directory separator, so scopes can nest. 4. Copies `visibility` and `throw` from the scoped config if you set them. 5. Builds a fresh disk from the merged config. Some consequences worth knowing: - **It is a separate instance.** A scoped disk named after `s3` gets its own S3 client built from the same config; it does not share the parent's object. - **URLs include the prefix.** `url()` on the scoped disk prepends the prefix, so links point at the real key. - **It is not a sandbox on its own.** Flysystem's path normalizer rejects `..` segments that climb above the root with `PathTraversalDetected`, and the prefix keeps well-formed paths inside the scope; authorization still decides which designer may use which scope. ## Read-only disks Adding `'read-only' => true` to a disk config wraps its adapter before Laravel builds the Flysystem instance. Reads, listings and metadata work; `write`, `writeStream`, `delete`, `deleteDirectory`, `createDirectory`, `setVisibility`, `move` and `copy` throw Flysystem's `UnableTo...` exceptions. Laravel's adapter then applies its usual failure policy: with `'throw' => false` a `put()` returns `false`, with `'throw' => true` the exception reaches your code. Typical uses: - a disk over an archive bucket that code should never modify; - a disk shared with another system that owns the writes; - defence in depth for a reporting job that only needs to read. ## Storage::build() `Storage::build($config)` resolves a disk from configuration you pass at runtime: - **An array** is resolved like a configured disk, with any driver including `scoped` and `read-through`. - **A string** is treated as the root of a `local` driver disk. - **No caching by name.** The result is not stored in the manager's `$disks`, so each call builds a new instance; keep the returned object if you use it repeatedly. When it fits: - a scoped disk per designer or tenant: `Storage::build(['driver' => 'scoped', 'disk' => 's3', 'prefix' => "portfolios/{$designer->id}"])`; - a customer-supplied SFTP or FTP server whose credentials live in the database; - a temporary working directory in an Artisan command, built from a string path. When it does not: disks every request needs belong in `config/filesystems.php`, where they are named, reviewable and resolved once per process. ## Combining wrappers: the scoped read-only trap The scoped driver copies only `prefix`, `visibility` and `throw` from its own entry onto the parent config. A `'read-only' => true` written next to `'driver' => 'scoped'` is therefore **not applied**; the scoped disk stays writable. To get a read-only view of one prefix, put the flag in the parent config instead, either on the named parent disk or in an inline array passed as `disk`: ```php 'samples-readonly' => [ 'driver' => 'scoped', 'prefix' => 'portfolios/samples', 'disk' => [ 'driver' => 's3', // key, secret, region, bucket ... 'read-only' => true, ], ], ``` Because the merged config is built into one Flysystem instance, the read-only decorator and the prefix decorator then both apply. ## Choosing between them - Need the same bucket split into areas? **Scoped disks.** - Need a guarantee that code cannot change files? **Read-only.** - Need a disk whose settings come from data? **`Storage::build()`**, often combined with the scoped driver.
- What does put() return on a read-only disk?The read-only adapter throws `UnableToWriteFile`. Laravel's adapter catches it and, with the default `'throw' => false`, returns `false` (and reports the exception only if `report` is on). With `'throw' => true` the exception propagates, which is usually what you want for a disk that should never be written.
- Does a scoped disk stop a user from reaching another designer's files?It keeps well-formed paths under the prefix, and Flysystem rejects `..` segments that climb above the root with `PathTraversalDetected`. But choosing the prefix is your code's job: if the designer id comes from the request without an authorization check, the scope protects nothing. Treat it as a path convenience backed by policies.
- Why not call Storage::build() inside a loop for every file?`build()` does not cache by name, so each call constructs a new adapter and, for S3, a new client. Build the disk once per unit of work and reuse the returned instance.
saying these in an interview costs you the question
- A scoped disk shares the same client object as its parent disk
- read-only is a separate driver you set instead of s3 or local
- A write to a read-only disk is silently ignored and reported as success
- Storage::build() caches the disk by name like Storage::disk() does
- A scoped prefix alone is enough to authorize access to a tenant's files