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

Dav Laravel Package

sabre/dav

sabre/dav is the most popular WebDAV framework for PHP, letting you build WebDAV, CalDAV and CardDAV servers. Well-documented, actively maintained, and trusted for calendar and contact sync.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pick your version. sabre/dav is a framework, not a bundle you sprinkle in — you hand it a tree of nodes and it speaks the protocol.

composer require sabre/dav

4.x supports PHP 7.1–8.4 (4.7.0 is current stable). If you're on PHP 7.4+ and Laravel 10/11, 4.7.* is the safe pin. It pulls in sabre/http, sabre/xml, sabre/event, sabre/vobject — you will touch all of them.

Where to look first

  1. https://sabre.io/dav/ — the only real documentation. The README is a stub; the site has per-plugin and per-backend pages.
  2. examples/ in the repo — calendarserver.php is the canonical full CalDAV/CardDAV setup with PDO storage. Copy this first, then adapt.
  3. lib/DAV/Server.php — read invokeMethod(). Understanding the dispatch loop explains 90% of "why is my response wrong" bugs.
  4. lib/DAV/FS/ — the ready-made filesystem tree, useful as a reference implementation for your own node classes.

Minimal first use case: expose files over WebDAV

// app/Http/Controllers/WebDavController.php
use Sabre\DAV;
use Sabre\HTTP\Sapi;

class WebDavController
{
    public function __invoke()
    {
        $root = new DAV\SimpleCollection('root', [
            new DAV\FS\Directory(storage_path('dav')),
        ]);

        $server = new DAV\Server($root);
        $server->setBaseUri('/dav/');
        $server->addPlugin(new DAV\Browser\Plugin()); // dev only

        Sapi::sendResponse($server->httpRequest, $server->httpResponse);
    }
}

That endpoint works with Cyberduck, Finder, Windows Explorer, and cadaver immediately. From there, the natural second step is a CalDAV/CardDAV endpoint using Sabre\CalDAV\CalendarRoot + Sabre\DAV\PDO\Backend, which is the reason most Laravel teams reach for this package (calendar/contact sync with Apple Mail, Thunderbird, Outlook).

Implementation Patterns

Wrap the server, don't fight Laravel's response stack

sabre/dav writes directly to PHP output via Sapi::sendResponse(). Treat the controller as a pass-through and register a catch-all route that bypasses Laravel's response pipeline:

// routes/web.php — must accept all DAV verbs
Route::match(
    ['GET','HEAD','POST','PUT','DELETE','PATCH','PROPFIND','PROPPATCH','MKCOL','COPY','MOVE','LOCK','UNLOCK','REPORT','OPTIONS'],
    '/dav/{path?}',
    WebDavController::class
)->where('path', '.*');

Route::any() does not cover PROPFIND, REPORT, MKCOL, etc. Laravel's Request will reject nothing, but Symfony's router only matches the verbs you list.

Exclude DAV routes from the global HTTP kernel

In bootstrap/app.php (Laravel 11) or VerifyCsrfToken::$except, exclude the DAV path from CSRF, and consider excluding session start, debugbar, and telescope entirely. Any stray byte before the XML body breaks the response for every client.

->withMiddleware(function (Middleware $middleware) {
    $middleware->validateCsrfTokens(except: ['dav/*']);
    $middleware->appendToGroup('web', OnlyForNonDav::class); // your own guard
})

Custom node classes backed by Eloquent

This is the bread-and-butter pattern for per-user file trees that live in S3, a database, or an existing files table. Implement Sabre\DAV\ICollection and Sabre\DAV\IFile:

class FileNode extends \Sabre\DAV\File
{
    public function __construct(
        protected File $model,
        protected Storage $storage,
    ) {}

    public function getName(): string { return $this->model->name; }

    public function getSize(): int { return $this->storage->size($this->model->path); }

    public function getETag(): ?string { return '"'.md5($this->model->updated_at).'"'; }

    public function get(): mixed { return $this->storage->readStream($this->model->path); }

    public function put($data): ?string
    {
        $this->storage->writeStream($this->model->path, $data);
        $this->model->touch();
        return $this->getETag();
    }
}

Notes that save hours:

  • Return streams from get(), not strings, for anything potentially large. IFile::get() accepts a resource.
  • getETag() must be quoted ("abc"), not bare. Clients use it for If-Match and sync caching.
  • put() returning null means "no new ETag" — return the refreshed one.
  • For collections, getChildren() may return a Traversable, so you can paginate with a generator instead of hydrating thousands of Eloquent models.

Calendar/contact server: use the PDO backend unless you need multi-tenancy in your own tables

Sabre\CalDAV\Backend\PDO and Sabre\CardDAV\Backend\PDO are production-grade and cover sync-tokens, time-range queries, and per-principal filtering. Run the shipped SQL:

examples/sql/mysql.calendars.sql
examples/sql/mysql.addressbooks.sql
examples/sql/mysql.locks.sql

If you must use Eloquent, implement Sabre\CalDAV\Backend\SyncSupport and Sabre\CalDAV\Backend\SchedulingSupport — skipping SyncSupport silently disables DAV:sync-collection, which breaks iOS/macOS sync and is brutal to debug.

Authentication: plug Laravel's user provider into a custom backend

sabre/dav has no opinion about users. Two options:

class EloquentAuth extends \Sabre\DAV\Auth\Backend\AbstractBasic
{
    protected function validateUserPass($username, $password): bool
    {
        $user = User::where('email', $username)->first();
        return $user && Hash::check($password, $user->password);
    }
}

Then build principal URIs consistently — principals/{username} — everywhere. The principal path is a foreign key across calendars, shares, scheduling, and ACL; inconsistency here manifests as 403s or invisible calendars in Apple Calendar.

For anything exposed to the internet, use Basic auth over TLS only (Digest is legacy and incompatible with several modern clients). Set the realm, and don't be tempted to use Laravel session cookies — DAV clients don't share them.

Plugin stack order matters

Typical production configuration:

$server->addPlugin(new \Sabre\DAV\Auth\Plugin($authBackend, 'MyRealm'));
$server->addPlugin(new \Sabre\DAV\Locks\Plugin(new \Sabre\DAV\Locks\Backend\PDO($pdo)));
$server->addPlugin(new \Sabre\DAV\TemporaryFileFilter\Plugin()); // Windows Explorer/MS Office
$server->addPlugin(new \Sabre\DAV\Sync\Plugin());
$server->addPlugin(new \Sabre\CalDAV\Plugin());
$server->addPlugin(new \Sabre\CalDAV\Schedule\Plugin());
$server->addPlugin(new \Sabre\CalDAV\SharingPlugin());
$server->addPlugin(new \Sabre\DAVACL\Plugin()); // last, it's greedy

DAVACL\Plugin intercepts a lot; put it after the auth plugin but before method handlers. Browser\Plugin is a debugging aid — leave it off in production.

React to writes via the event system

The server is an event emitter — this is where you attach business logic:

$server->on('afterCreateFile', fn($path) => /* notify, index, audit */ null);
$server->on('beforeWriteContent', fn($path, $node, &$data, &$modified) => null);
$server->on('beforeMethod:*', fn($req, $res) => /* rate limit / metrics */ null);
$server->on('afterMethod:PUT', fn($req, $res) => /* ... */ null);

In 4.x wildcards require the colon form: beforeMethod:*, not beforeMethod. If your 3.x handlers silently stopped firing after an upgrade, this is why.

For calendar invites (RSVP emails, iTip messages), register Sabre\CalDAV\Schedule\IMipPlugin with a mailer; 4.7.0 also lets you inject a custom iTipBroker.

Multi-tenant routing

Use setBaseUri("/dav/{$tenantSlug}/") and build the tree from the tenant's root. Keep in mind Server::setBaseUri() is used to strip the prefix from the request path — if it doesn't exactly match what the client dialed, you get 404s on every method with confusing bodies.

Testing

  • sabre/dav ships a Sabre\DAV\Client — use it in Pest/PHPUnit feature tests to do real PROPFIND/PUT round trips without spinning up a browser.
  • Run litmus (the ancient C tool) once against your endpoint; it's still the gold standard for WebDAV compliance and catches subtleties (lock token formats, getcontenttype on collections).
  • Test with at least one real client (Cyberduck for DAV, Thunderbird or DAVx5 for CalDAV/CardDAV). Browser-based curl tests pass while clients fail.

Gotchas and Tips

Whitespace, notices, and output buffering are fatal. Any byte before the XML — an accidental echo, a PHP notice with display_errors=On, Laravel's debug page on an exception — yields an unparseable response and clients report vague errors like "calendar not found". Wrap the controller so exceptions become proper DAV XML errors, disable APP_DEBUG output for the DAV path, and consider ob_start() at the top of the controller as a belt-and-braces measure.

Sapi::sendResponse() terminates the request. After it runs, Laravel middleware (Terminate, logging, response decoration) is bypassed. If you rely on terminate() hooks for cleanup, run them explicitly before sending — or accept the loss.

Trailing slashes and setBaseUri. setBaseUri('/dav') and setBaseUri('/dav/') behave differently regarding collection URIs and href generation. Match what your clients will use. If you mount behind a prefix handled by nginx alias, set Server::$baseUri and be aware REQUEST_URI from the proxy may not include the prefix — you'll need Sapi or a custom httpRequest.

Locks require persistent storage. Locks\Backend\File is fine for a single-server dev setup; for anything else use the PDO lock backend. Windows Explorer and some Office clients issue LOCK immediately and fail hard without it. Combine with TemporaryFileFilter\Plugin or you'll see .~lock files litter the tree.

getNodeForPath gets called a lot. 4.5.0 added INodeByPath for optimization and 4.7.0 fixed some internal caching. For deep trees, memoize node lookups per request in your own tree class. Returning fresh Eloquent models from every call is the usual performance culprit in PROPFIND Depth:1 responses.

ETag and Last-Modified are protocol contract, not optimization. Clients cache aggressively. If you compute ETags from updated_at granular to the second, two writes in the same second will be treated as the same version and sync silently breaks. Use a content hash or a monotonic version column.

PDO backend schema drift. The SQL files in examples/sql/ are the source of truth; they are not migrations and won't apply themselves. If you write your own migration, keep column types (especially calendarobjects.calendardata as LONGBLOB / BYTEA / TEXT) and the calendartype column — Apple clients break without VTODO/VEVENT filtering.

DAV:sync-token bugs look like calendar disappearance. When a client deletes a calendar or addressbook and the sync report returns a stale token, clients re-download the entire collection or refuse to show new items. Always implement getChangesForCalendar() correctly (returning null means full resync) and return monotonically increasing sync tokens tied to the DB's write clock.

Timezones. CalDAV's calendar-timezone property and time-range filters are subtle. When filtering events by range, expand recurring events through Sabre\VObject rather than comparing raw DTSTART strings — sabre's CalDAV\Backend\PDO already uses the vobject expansion; custom backends frequently get this wrong and miss recurring events.

PHP 8.4 deprecations. 4.7.0 cleaned up most, but third-party plugins and the browser plugin may still emit warnings. Run with E_ALL off in dev but check logs; a single deprecation notice emitted to output breaks clients.

Extension points worth knowing:

  • Sabre\DAV\Tree subclass — override getNodeForPath for virtual trees (e.g., /files/{project}/{folder} mappings).
  • Sabre\DAV\Exception subclasses — return correct HTTP codes (409 Conflict for PUT into non-existent collection, 423 Locked, 507 Insufficient Storage) rather than letting a generic exception become a 500.
  • Server::$on('exception', ...) — hook to log and map domain exceptions (AuthorizationException, ModelNotFoundException) into DAV errors.
  • Sabre\DAV\Xml\Property and Sabre\DAV\PropFind — custom properties (e.g., occ:color, custom:thumbnail) are registered via PropFind::handle listeners or Server::getProperties.
  • Sabre\HTTP\Sapi::getRequest() — if you need to inspect the raw request before the server dispatches.

Debugging checklist when a client misbehaves:

  1. Log the raw request method, path, and body to a file.
  2. Replay with curl -X PROPFIND -u user:pass --data @body.xml -H 'Depth: 1' ....
  3. Compare against the response a known-good server (e.g., Nextcloud) returns for the same request.
  4. Check the DAV: response headers include DAV: 1, 3, calendar-access — missing headers mean a plugin failed to register, not an XML bug.

Version pinning. 4.2.3 reverted a non-BC change in the PUT interface from 4.2.2 — if you subclassed IFile during that window, re-check your signatures. In composer.json, pin ^4.7 and rely on the maintained branch; the 3.x line is unmaintained and PHP 8-incompatible.

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.
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels