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

Simple Caldav Client Laravel Package

thecsea/simple-caldav-client

A lightweight PHP CalDAV client for connecting to calendar servers like Nextcloud, iCloud, or Google Calendar. List calendars and create, update, or delete events with a simple API, installable via Composer and easy to drop into any PHP or Laravel project.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and locate the source first.

composer require thecsea/simple-caldav-client

There is no service provider, facade, config file, migration, or publishable asset. The package is a single class (SimpleCalDAVClient) plus an exception type. Open vendor/thecsea/simple-caldav-client/src/SimpleCalDAVClient.php before writing anything — you will spend more time reading this file than the README, and the README's examples lag the code in places (notably the array shape returned by getEvents() and the argument types accepted by createEvent()).

Minimal first run.

use SimpleCalDAVClient;

$client = new SimpleCalDAVClient();
$client->connect(config('services.caldav.url'), config('services.caldav.user'), config('services.caldav.password'));

$calendars = $client->findCalendars();      // displayname => calendar array
$client->setCalendar($calendars['Personal']);

$events = $client->getEvents('2024-01-01 00:00:00', '2024-01-31 23:59:59');

Put the three credentials in config/services.php and .env (CALDAV_URL, CALDAV_USER, CALDAV_PASSWORD). Do this in config/services.php, not in a custom config file — it is one less file to remember.

First use case to build: a read-only "next 7 days" widget. connect() → findCalendars() → setCalendar() → getEvents() for a rolling window, mapped into a view. Do this in a queued job from day one; even the widget path makes 3–4 HTTP round-trips and will visibly stall an uncached page.

Verify PHP compatibility before committing. The library is plain PHP with no strict types and few type hints; on PHP 8.2+ run a quick smoke test under error_reporting(E_ALL) to catch dynamic-property deprecations. The 2024-10-01 release addressed much of this, but pin the exact version in composer.json — 21 stars and zero dependents means no upstream safety net.

Implementation Patterns

Wrap it, don't sprinkle it. The class is stateful (setCalendar() mutates internal state) and does its own cURL calls, which means it is invisible to Laravel's HTTP client, its retry middleware, and Http::fake(). Bind a thin interface in a service provider and inject that:

interface CalendarGateway {
    public function calendars(): array;
    public function events(string $calendarHref, Carbon $from, Carbon $to): array;
    public function create(string $calendarHref, array $event): string;
    public function update(string $href, array $event): void;
    public function delete(string $href): void;
}

A CalDavGateway implementation creates a fresh SimpleCalDAVClient per operation (or at minimum per calendar). Reusing a singleton across tenants is the fastest way to write one user's event into another user's calendar.

The pattern that actually pays off: local mirror + sync job.

  1. A scheduled command (everyFifteenMinutes) loops over configured calendars and dispatches one SyncCalendar job per calendar.
  2. The job pulls a window (e.g. −30/+180 days), and for each event compares the etag against a caldav_etag column on a local events table.
  3. Unchanged etag → skip parsing entirely. Changed or new → parse, upsert. Missing href → mark deleted.

Then all list views, filters, and "today" widgets hit Eloquent, and the CalDAV server is only touched by the sync job. This is the difference between a usable integration and one that times out under load. Store href as a unique index — it is the only stable identifier the server gives you, and the library returns it on createEvent(); keep it.

Writing events. Build a small factory that converts your domain model into whatever createEvent()/changeEvent() expect (an event array in the documented examples; some versions accept a full VCALENDAR string — confirm against the installed source). Always:

  • Convert all times to UTC before handing them over, or emit explicit TZID parameters. Mixing local Carbon values with a server in another timezone is the single most common source of "the event is on the wrong day" bugs.
  • Set a deterministic UID yourself if the library lets you; otherwise capture the returned href immediately and persist it in the same transaction that saves your local record. If the transaction rolls back after a successful PUT, you have an orphaned remote event with no local pointer.
  • Use changeEvent($href, $event) — never delete-then-create for edits. Delete-and-recreate breaks attendees' invitations and changes the UID.

Deletes are best-effort. Wrap deleteEvent() so a 404 (already deleted remotely) is treated as success. Otherwise a failed sync leaves rows that can never be cleaned up.

Queues and retries. Wrap every call in a job with $tries = 3, backoff() of [30, 120, 600], and failed() that logs the calendar href and operation. The library surfaces server errors as generic exceptions with little context, so log the operation name yourself — that log line is your only debugging breadcrumb later.

Multi-calendar, multi-user. findCalendars() returns an associative array keyed by display name. Display names are not unique and users rename them. Immediately re-key by href and persist href + displayname in a calendars table; never look a calendar up by display name at runtime.

Testing. Point the gateway at a local Radicale or Baïkal container in docker-compose.yml for integration tests, and bind a fake implementation for feature tests. There is nothing to mock otherwise — the library is not friendly to Http::fake().

Gotchas and Tips

  • setCalendar() is sticky state. A long-lived worker (Horizon, Octane) reusing one instance will silently keep the previous calendar. Instantiate per job, or explicitly call setCalendar() before every read/write. Under Octane, never resolve the client as a singleton.
  • The iCal parser is minimal. Expect problems with folded lines (75-octet continuation), escaped commas/semicolons in SUMMARY/DESCRIPTION, and VTIMEZONE blocks. If you need the content of an event reliably, take the raw payload the library returns and parse it with sabre/vobject; use this package purely as the transport.
  • Recurring events are not expanded. getEvents() over a window typically returns the recurring master, not the occurrences that fall inside it. Either run an RRULE expansion (sabre/vobject's RRuleIterator or a hand-rolled loop honoring EXDATE) or you will show "weekly standup" exactly once, on the wrong day.
  • Timeouts and TLS. The internal cURL handle is not configurable through Laravel. Self-hosted Nextcloud/Radicale boxes with self-signed certs will fail with a bare false return and no exception — check the return value of connect() explicitly. If you must talk to such a server, fork the class or pass a stream/cURL context through connect() (the fourth parameter accepts one) rather than disabling verification globally.
  • Error messages are unhelpful. Failures frequently surface as an empty string, a 0, or a raw XML blob. Any debug tooling you build should dump the last request URL and response body, which means either subclassing or patching in a lastResponse property — budget an hour for this early, it will save days.
  • No optimistic concurrency. changeEvent() does not send If-Match with the etag you read, so concurrent edits (phone + web) last-write-win silently. If that matters, store the etag and compare it locally before pushing.
  • iCloud and Google need special handling. iCloud: https://caldav.icloud.com with an app-specific password (never the Apple ID password). Google Calendar has deprecated CalDAV access for most accounts — verify with the target provider before scoping work; if it's Google, use the Calendar API instead.
  • Licensing. GPL-2.0. Fine for internal/SaaS use, but if you ever redistribute the application (on-prem installers, an Electron-wrapped client, an app shipped to a client), the GPL obligations follow the library. sabre/dav is BSD-3 — worth a thought if distribution is on the roadmap.
  • No caching layer. Nothing is memoized; findCalendars() is a fresh PROPFIND every time. Cache discovery results in the Laravel cache for the day, keyed by user id, and refresh on auth failure.
  • Todos use the same machinery. getTodos()/createTodo()/changeTodo()/deleteTodo() exist and behave symmetrically; if you build the sync job generically over VTODO/VEVENT you get task lists almost for free.
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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin