pbmedia/laravel-webdav
Laravel WebDAV adds a WebDAV filesystem driver to Laravel, making it easy to list, read, write, and delete files on remote WebDAV servers through Laravel's familiar Storage API and Flysystem integration.
Install and wire up a disk (5 minutes)
composer require pbmedia/laravel-webdav
Before you run that, check your stack against 1.11.0: it requires PHP 7.4+ (8.1 supported) and Laravel 8+. PHP 7.3, Laravel 6, and Laravel 7 are no longer supported — if you're on those, you're pinned to 1.10.x.
The service provider is auto-discovered. First place to look is config/filesystems.php — add a disk using the webdav driver:
'disks' => [
'webdav' => [
'driver' => 'webdav',
'baseUri' => env('WEBDAV_BASE_URI'), // e.g. https://cloud.example.com/remote.php/dav/files/jane/
'userName' => env('WEBDAV_USERNAME'),
'password' => env('WEBDAV_PASSWORD'),
],
],
The config array is handed to the underlying Sabre WebDAV client, so the option names are Sabre's (baseUri, userName, password) — not Laravel's usual snake_case.
First use case: dump a generated file somewhere durable.
use Illuminate\Support\Facades\Storage;
Storage::disk('webdav')->put('reports/2024-06.pdf', $pdfContents);
Storage::disk('webdav')->exists('reports/2024-06.pdf'); // true
Storage::disk('webdav')->files('reports');
Because it plugs into Laravel's filesystem abstraction, everything you already know — put, get, delete, files, allFiles, copy, move — works unchanged. The fastest sanity check is php artisan tinker with Storage::disk('webdav')->files('/') against a real server before you build anything on top of it.
Where to look first, in order: the README disk example → config/filesystems.php → the package's service provider in src/ (that is where Storage::extend('webdav', ...) happens) → the adapter class. That last file tells you exactly which Flysystem version you are bound to, which still matters a lot (see Gotchas).
Multiple disks for multiple roots. WebDAV roots are usually per-user or per-tenant paths. Rather than one disk with dynamic paths, define a disk per endpoint and pick at runtime:
'webdav_tenant' => [
'driver' => 'webdav',
'baseUri' => 'https://dav.example.com/tenants/' . env('TENANT_SLUG') . '/',
'userName' => env('WEBDAV_USERNAME'),
'password' => env('WEBDAV_PASSWORD'),
],
For genuinely dynamic endpoints, build the disk on the fly (Laravel 9+):
$disk = Storage::build([
'driver' => 'webdav',
'baseUri' => $tenant->dav_url,
'userName' => $tenant->dav_user,
'password' => $tenant->dav_password,
]);
Use it as a backup/mirror target. The most common real-world use for a WebDAV driver is "ship files off this box." Point config/backup.php (spatie/laravel-backup) at the disk, or write a small command:
foreach (Storage::disk('local')->allFiles('exports') as $file) {
Storage::disk('webdav')->put($file, Storage::disk('local')->readStream($file));
}
Use readStream/writeStream rather than get/put for anything that could be large — it avoids buffering whole files in PHP memory:
$stream = Storage::disk('local')->readStream($path);
Storage::disk('webdav')->writeStream("archive/{$path}", $stream);
Push transfers to the queue. WebDAV round-trips are network-bound and the driver has no retry logic. A job with $tries = 3 and backoff() around the upload is far more reliable than a synchronous call in a request:
class PushToWebdav implements ShouldQueue
{
public int $tries = 3;
public array $backoff = [10, 60];
public function __construct(public string $localPath) {}
public function handle(): void
{
Storage::disk('webdav')->writeStream(
$this->localPath,
Storage::disk('local')->readStream($this->localPath)
);
}
}
Reach past the facade when you need options. Storage::disk('webdav')->getDriver() gives you the Flysystem adapter; from there you can drop to the Sabre client for things the abstraction doesn't expose (custom PROPFIND depth, headers, Guzzle timeouts). Treat this as an escape hatch, not a pattern — it couples you to package internals that change between minor releases.
Testing. Storage::fake() replaces the disk with a local one and will silently pass while your real WebDAV integration is broken. Keep at least one test that hits a real Sabre/Nextcloud container, or assert against a thin interface you inject so you can stub it in unit tests.
Maintenance is low-frequency, not zero — but treat it as a risk you're accepting. 1.11.0 (PHP 8.1 support, PHP 7.3 and Laravel 6/7 dropped) shows the package still gets compatibility releases, but there's no indication of ongoing feature work or a stated support window. The release notes are a version-bump story, not a roadmap.
composer why-not pbmedia/laravel-webdav 1.11.0 to see exactly what's blocking you. If the answer is a Flysystem or Laravel framework constraint, don't --ignore-platform-reqs your way past it on a production filesystem driver.composer.json constraints and the adapter's implements clause before assuming compatibility — the Flysystem 1/2-era API is still what this package was written against.league/flysystem-webdav (v3-compatible) registered yourself via a small Storage::extend('webdav', ...) in a service provider. If you're starting new code on Laravel 10+, budget an afternoon for this rather than adopting the package. If you keep it, pin the version explicitly ("pbmedia/laravel-webdav": "^1.11") and note the pin in your upgrade docs so a future PHP/framework bump doesn't surprise you.Upgrading to 1.11.0 specifically
config.platform.php override for 7.3, remove it, or Composer will keep resolving 1.10.x and you'll wonder why the upgrade "didn't happen."baseUri/userName/password handling, so existing disk configs carry over untouched.Config quirks
baseUri must include the scheme and the full DAV root path. For Nextcloud that is https://host/remote.php/dav/files/USERNAME/. A missing trailing slash or a missing remote.php/dav/... segment produces confusing 404s rather than auth errors.userName, not username. A typo here fails as an authentication error, which sends you hunting in the wrong place.The Laravel filesystem contract leaks
url() and temporaryUrl() are not meaningful here. Don't build download links on them; proxy the file through a controller using readStream.visibility / 'public' vs 'private' is a Flysystem concept with no WebDAV equivalent. Setting it does nothing.Debugging
UnableToWriteFile, UnableToReadFile, FilesystemException). Wrap writes and log the path plus the previous exception; the top-level message is often generic.Sabre\DAV\Client directly in tinker with the same credentials and ['debug' => true] on the Guzzle client. That isolates "wrong config" from "driver bug" in about a minute.files('/') performance depends entirely on the server's PROPFIND depth handling. Directories with thousands of entries can take seconds per call — cache listings, or request specific paths instead of enumerating.Filenames
#, ?, and non-ASCII characters are the classic breakage point. Percent-encoding bugs here surface as "file not found" on write then "found" on read. Normalize filenames yourself (Str::slug() on the basename, keep the extension) before handing them to the disk.Extension points worth knowing
Storage::extend('webdav', fn ($app, $config) => ...) in a service provider lets you swap in your own adapter without touching config — the cleanest way to replace this package later, since the disk name stays identical and call sites don't change.FileArchive::push($path)) is cheap insurance: when you migrate off the package — or when a future release shifts the support matrix — one class changes instead of every Storage::disk('webdav') call in the codebase.How can I help you explore Laravel packages today?