atoolo/events-calendar-bundle
Sitepark event calendar bundle for publishing and managing events on websites, with CMS entry, web forms, database imports, and filtered search by category, keyword, and time period.
Know what you are installing. atoolo/events-calendar-bundle is a Symfony bundle, not a Laravel package. It has zero Laravel integration surface: no extra.laravel providers, no publishable config, no facade, no Blade components. It is also part of the Sitepark Atoolo stack — it expects atoolo/resource-bundle, atoolo/graphql-search-bundle and an Atoolo search index to be present. Do not expect composer require to be the end of the story.
Two integration paths — pick one deliberately:
Scheduling* DTOs and indexers, and it couples your Laravel upgrades to Atoolo's.Minimal steps for path 1:
composer create-project symfony/skeleton events-calendar
cd events-calendar
composer require sitepark/atoolo-events-calendar-bundle
Then enable the bundle in config/bundles.php, add the Atoolo resource/GraphQL bundles and their config, and confirm routes with php bin/console debug:router. Run it behind your normal reverse proxy, and point Laravel at it with a base URL in config/services.php.
Look here first, in this order:
https://sitepark.github.io/atoolo-docs/develop/bundles/events-calendar/ — the only real documentation; the README is a product blurb.bin/console debug:router in the sidecar — the iCal API routes are the most immediately useful surface (/api/ical/..., including a search variant).eventTeaser and a symbolicAsset field with a variant argument; those are your listing-page primitives.First use case to ship: a public event list backed by the GraphQL eventTeaser, plus a per-event "add to calendar" link hitting the iCal endpoint. That exercises indexing, serialization and the asset field in one slice, and tells you fast whether the index is populated correctly.
Before you build filters, decide which date question you are asking. As of 1.18.1 the bundle explicitly distinguishes expand (turn a recurring series into concrete occurrences) from search by date (filter a result set to a window). These produce different answers for the same event and were conflated before the fix. Write down which one each screen needs — a "what's on this week" page is search-by-date, a "show me every occurrence of this series" page is expand.
Wrap every call in one Laravel-side client. Create App\Services\EventCalendarClient with Http::baseUrl(config('services.events_calendar.url')), ->timeout(...), ->retry(...), and typed methods (upcoming(), expand(string $id), teaser(string $id), ical(string $id)). Everything else in the app — controllers, Livewire, Inertia — depends on that class, not on raw Http calls. This is your only real abstraction seam, so make it a good one.
Keep "expand" and "search by date" as separate client methods. Do not let a single events($filters) method hide the choice. A recurring event series is either one row filtered by date, or N rows of occurrences — and the 1.18.1 fix means the bundle now treats these as different operations. Mirroring that split in your client keeps the intent readable and makes the eventual behaviour change obvious at the call site rather than buried in GraphQL variables.
Treat GraphQL as the read API and Eloquent as the cache. Two workable shapes:
events / event_dates tables, and your listing, filtering and sorting are plain Eloquent. You get pagination, scopes and whereDate for free, and the calendar can be down without taking the site down. If you materialize, you must decide whether you are storing series or occurrences — that is the same expand/search decision, made once at import time. Storing occurrences is usually the right call for a public listing.If you materialize, map the bundle's Scheduling DTO faithfully: it carries a begin date/time and an optional end. Its recent history is explicitly about not defaulting endDate to beginDate — preserve that null in your schema (nullable timestamp), or you will silently turn single-day events into multi-day ones.
Use iCal as a thin, stateless endpoint. Fetch it per request or cache it briefly; it is not a data source to model against. Pass language and location as the endpoints expect, and note there are two forms: one with language in the path, one without. The occurrence parameter exists for recurring series — decide per event whether you emit "all occurrences" or "this one" and put that choice in your client method rather than in views.
Mirror the configuration, don't hardcode it. The bundle's indexer behaviour is driven by config that has changed names across 1.x releases: highlightCategory became simpleCategoryMap, and there is a separate venue category mapping. If your Laravel app filters by category, read those mappings from a config file of your own that you keep in sync with the sidecar's — do not let two divergent lists exist.
Plan for multi-instance from day one. Release 1.1.0 added multiple-instance support for both the bundle and the RCE indexer. If your Laravel app serves several brands/sites, key your client config by instance name (services.events_calendar.sites.{site}.url) rather than assuming one global URL. Retrofitting this later means touching every call site.
Keep the sidecar's GraphQL bundle in lockstep with the calendar bundle. Chores in releases routinely bump atoolo/graphql-search-bundle alongside. Bump both together in the sidecar's composer.json and commit the resulting lock — a drifted GraphQL version is the most likely source of "the schema no longer has that field" surprises from Laravel, where you have no profiler to tell you why.
Hook reindexing into your deploy and content events. Anything index-backed is stale until the sidecar reindexes. Invalidate your Laravel-side cache after any write in the CMS/admin flow, and make reindexing a step in deploy rather than something a human remembers.
Keep extensions on the Symfony side. Custom indexers, extra GraphQL fields, and category-map changes are Symfony-bundle work, committed in the sidecar repo. Do not attempt to monkey-patch bundle internals from Laravel.
vendor:publish. You will not find config to publish. Assume you are writing the wiring yourself and budget for it.EventDate was renamed to Scheduling, symbolicImage to symbolicAsset, highlightCategory to simpleCategoryMap, and the iCal search route moved from a path segment to a query parameter. The current line is 1.18.x. Pin an exact version, and read each release note before bumping — these are breaking renames shipped as minor features.atoolo/graphql-search-bundle and shuffles the phpstan/phar tooling. Tooling chores are noise to you; the GraphQL bump is not. Check the sidecar's composer.lock diff, not just the calendar bundle's release notes.gmdate, so the bundle's internal arithmetic is UTC-based. Store UTC, convert at the presentation edge, and test an event that crosses midnight in a non-UTC locale.endDate falling back to beginDate in two separate places. Audit any code (yours included) that does $end ?? $begin.debug:container, debug:router and real stack traces. From Laravel you get an HTTP status and a log line. Log the full response body (truncated) on non-2xx in your client, and build a health/ping route in the sidecar — you will need it.atoolo-e2e-test, the Sitepark changelogs, and the commit diffs themselves — at this release cadence, the diff is often more informative than the one-line note attached to it. Plan for reading source, and be comfortable pinning to a known-good tag for long stretches.How can I help you explore Laravel packages today?