Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Webdav Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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).

Implementation Patterns

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.

Gotchas and Tips

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.

  • Verify the upgrade before you commit to it. Run 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.
  • Laravel 9+ / Flysystem 3 is still unclaimed. 1.11.0 raises the floor (away from Laravel 6/7) but the notes say nothing about Laravel 9/10 or Flysystem 3. Check 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.
  • Migration path. The forward-looking option remains 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

  • Purely a support-matrix change for most apps — no API or config migration called out. The realistic breakage is CI: if any pipeline still runs PHP 7.3 or a Laravel 6/7 matrix, that job will now fail to resolve. Bump the matrix in the same PR.
  • If you were carrying a 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."
  • Nothing in the release changes 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.
  • Sabre option casing is userName, not username. A typo here fails as an authentication error, which sends you hunting in the wrong place.
  • Basic auth only. No OAuth, no digest flow, no bearer tokens. If your provider requires OAuth (Box, some SharePoint setups), this driver is the wrong tool.
  • Self-signed or internal-CA certificates will throw. You cannot configure Guzzle verify options through the disk config — you'd have to override the client.

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.
  • Permissions and file locking aren't exposed. Two workers writing the same path will happily clobber each other — namespace your paths per job/tenant.

Debugging

  • Errors surface as Flysystem exceptions (UnableToWriteFile, UnableToReadFile, FilesystemException). Wrap writes and log the path plus the previous exception; the top-level message is often generic.
  • To see the actual HTTP conversation, instantiate 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

  • Spaces, #, ?, 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.
  • Wrapping the disk behind a small repository/service class (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.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast