laravel/framework: Accept filesystem instance in copyToDisk() and moveToDisk() methods
Contributed by ziadoz
Part of the framework v13.36.0 release
Laravel's filesystem layer keeps receiving small but meaningful ergonomics upgrades, and pull request #61519 is one of them. It refines the recently introduced copyToDisk() and moveToDisk() methods on Illuminate\Filesystem\FilesystemAdapter so their first argument no longer has to be a disk name. You can now pass an actual filesystem instance — anything implementing Illuminate\Contracts\Filesystem\Filesystem, including FilesystemAdapter itself — and Laravel will use it directly as the copy/move destination. String disk names keep working exactly as before, so the change is purely additive.
What changed
The $disk parameter of both methods was widened from string to a union type:
public function copyToDisk($disk, $from, $to = null): bool
public function moveToDisk($disk, $from, $to = null): bool
Internally, the adapter now inspects what it received before resolving anything:
$destination = $disk instanceof FilesystemContract
? $disk
: Container::getInstance()->make(FilesystemFactory::class)->disk($disk);
If you pass an instance, it is used as the destination as-is. If you pass a string, it is still resolved through the filesystem manager exactly as it was before. The resolution logic lives in copyToDisk(), and moveToDisk() — which copies the file and then removes the source — inherits the same behavior. The Storage facade docblock was also updated to advertise the widened signature, so IDE autocomplete and static analysis stay accurate:
@method static bool copyToDisk(string|\Illuminate\Contracts\Filesystem\Filesystem $disk, string $from, string|null $to = null)
Why pass an instance?
Until now, the only way to identify a destination was a registered disk name, which forced a resolution through the filesystem factory on every call. Passing an instance is convenient in several situations:
- You already hold the destination adapter — for example, one resolved earlier in the method or injected as a dependency.
- You work with packages (multi-tenancy, on-the-fly storage mounting) that build filesystem instances that aren't registered under a name.
- You want to target a standalone, ad-hoc
FilesystemAdapterthat exists nowhere in yourfilesystems.phpconfig. - You're writing tests and want to hand a
Storage::fake()return value straight to the method instead of re-resolving it.
Copying and moving in practice
Here is the pattern from the pull request, mixing instance arguments with the optional destination-path argument:
use Illuminate\Support\Facades\Storage;
$disk1 = Storage::disk('s3');
$disk2 = Storage::disk('backups');
// Pass filesystem instances you already have
$disk1->moveToDisk($disk2, 'reports/report.csv'); // copy + delete source
$disk1->copyToDisk($disk2, 'reports/report.csv'); // keep the same path
$disk1->copyToDisk($disk2, 'reports/report.csv', '2026/report.csv'); // copy under a new path
// Plain disk names still work exactly as before
$disk1->copyToDisk('backups', 'reports/report.csv');
It also works with adapters that were never registered in the container, which is what the new test coverage exercises:
$fake = Storage::fake('local');
Storage::disk('s3')->copyToDisk($fake, 'reports/report.csv');
Both methods return bool, so you can keep wrapping calls in your existing success/failure handling.
Guard rails carried over
The safety check introduced alongside these methods still applies, and now covers instance arguments too. When the destination resolves to the same adapter instance and the destination path equals the source path ($to ?? $from === $from), Laravel throws:
throw new InvalidArgumentException('Cannot copy a file to the same disk and path.');
A dedicated test confirms this: calling $adapter->copyToDisk($adapter, 'file.txt') — passing the very same instance with the same path — throws the exception. Note that the comparison is by instance identity (===), so two distinct instances that happen to point at the same underlying storage will still be allowed through with matching paths; the guard is about avoiding a meaningless self-copy through a single adapter object.
Upgrade impact
There are no breaking changes. If you are upgrading from v12.70.0, keep in mind that copyToDisk() and moveToDisk() themselves arrived after that release (via the companion pull request #61511), so you are effectively picking up the methods and this widened signature together in one go.
Concretely:
- Existing code passing string disk names behaves identically — no action required.
- Code passing instances previously didn't exist, so there's nothing to migrate away from.
- If you subclass
FilesystemAdapterand override either method, consider widening your own signatures tostring|\Illuminate\Contracts\Filesystem\Filesystemfor consistency. - If your team maintains facade stubs or IDE helper files, regenerate them so the docblock change is reflected; this only affects tooling, not runtime behavior.
Takeaways
copyToDisk()andmoveToDisk()now accept either a disk name (string) or a filesystem instance (Illuminate\Contracts\Filesystem\Filesystem).- Instances are used directly; strings are still resolved through the filesystem manager, so existing calls are unaffected.
- Handy for ad-hoc adapters, dependency-injected disks, tenancy packages, and test fakes.
- The "cannot copy to the same disk and path" guard now also triggers when you pass the identical instance with a matching path.
- Fully backwards compatible — the only visible change beyond behavior is the updated facade docblock.