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

Fullcalendar Bundle Laravel Package

toiba/fullcalendar-bundle

Symfony bundle that integrates FullCalendar into your app, with controllers, Twig helpers, and event loaders so you can render interactive calendars and manage events with minimal configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

Read the fine print first. toiba/fullcalendar-bundle is a Symfony bundle, not a Laravel package — it ships an Extension, a Bundle class, a CalendarEventsListenerInterface, and a controller that returns a JSON feed. It is also marked [DEPRECATED] in its own README, with the author pointing to tattali/CalendarBundle as the successor, and it has zero dependents. For a Laravel codebase, the realistic "getting started" is: do not composer require this (you'd drag in symfony/http-kernel, symfony/event-dispatcher, and a config tree for a feature you can write in ~60 lines). Instead, read it as a reference implementation of the FullCalendar feed pattern.

If you are maintaining a legacy Symfony app where it is already installed, the mental model is three small pieces:

  1. The bundle — registers a route (conventionally /fc-load-events, route name toiba_fullcalendar_load_events) plus a service that turns your data into FullCalendar JSON.
  2. CalendarEvent — a DTO with title, start, end, url, bgColor, fgColor, cssClass, id, and custom properties, serialized via toArray().
  3. CalendarEventsListenerInterface — you implement onCalendarSetData($start, $end) (or equivalent) and the bundle calls it with the start/end query params FullCalendar sends, then serializes whatever events you added.

First use case to validate the install: a month view backed by a database table. Point FullCalendar at the feed:

new FullCalendar.Calendar(el, {
  initialView: 'dayGridMonth',
  events: '/fc-load-events'   // FullCalendar appends ?start=...&end=...
});

Then implement the listener to query rows between $start and $end, map each into a CalendarEvent, and return. If the month renders with correct colors and clickable URLs, you've exercised 90% of what this package does.

For Laravel, port the shape rather than the code: a GET /calendar/feed route, a CalendarFeedController, an App\Support\CalendarEvent DTO with toArray(), and a query scoped by start/end.

Implementation Patterns

The feed endpoint is the whole architecture. Everything else is a variation on "take start/end, return an array of event objects." Once you internalize that, the bundle stops being magic.

Listener pattern → Laravel service. The Symfony listener is just a hook so apps can inject events without touching the controller. In Laravel, that's a CalendarFeed service with a collect(Carbon $start, Carbon $end): Collection method, or an event/listener pair if you need multiple sources (e.g. AppointmentsSource, HolidaysSource, DeadlinesSource) merged into one payload:

class CalendarFeed
{
    public function __construct(private array $sources) {}

    public function events(Carbon $start, Carbon $end): array
    {
        return collect($this->sources)
            ->flatMap(fn ($s) => $s->events($start, $end))
            ->map->toArray()
            ->values()
            ->all();
    }
}

Mapping models to events. Keep the DTO dumb and do the mapping in one place. The bundle's CalendarEvent accepts colors and a URL, so per-record styling (backgroundColor from a status enum, url to a detail page) belongs in the mapper, not the controller.

Multiple calendars / filters. FullCalendar's events option accepts an object with extraParams:

events: {
  url: '/calendar/feed',
  extraParams: { team_id: 3, type: 'appointments' }
}

The bundle-era approach was to branch inside the listener on request params; in Laravel use a FormRequest to validate start, end, team_id, and pass a filter object into the feed service. This scales far better than one mega-listener.

Imperative event injection. The bundle also lets you addEvent() to a calendar object directly (useful for one-off "today" markers or a synthetic "no events" placeholder). Mirror this with a decorate() step that runs after collect() so ad-hoc events don't pollute your source classes.

Recurring events. Neither this bundle nor its successor expands RRULEs reliably. The pattern that survives contact with production is: materialize occurrences in the query layer (or a scheduled job writing an occurrences table) and let the feed return concrete start/end pairs. FullCalendar renders instances, not rules, unless you use the rrule plugin — don't mix the two.

Auth. The feed route is a normal authenticated route. Add auth + a policy check that the requested start/end window is reasonable (cap at, say, 1 year) — otherwise the endpoint is a free "dump my whole calendar" API.

Caching. Event feeds are read-heavy and per-user. Cache::remember("calendar:{$user->id}:{$start}:{$end}", 60, ...) with invalidation on model saves is often the single biggest win, since FullCalendar re-requests on every navigation.

Gotchas and Tips

The deprecation is the headline. No security patches, no Symfony 5/6/7 support guarantees, no maintainer. The successor tattali/CalendarBundle is the only sane target if you're staying in Symfony; for Laravel, write the ~60 lines yourself and own it.

FullCalendar's end is exclusive. A dayGridMonth request for March sends start=2024-03-01&end=2024-04-01, and an event ending exactly at 2024-04-01T00:00:00 is not drawn in March. Query with where('start', '<', $end) and where('end', '>=', $start) — not BETWEEN, and not <= on both sides.

Date parsing is the #1 crash source. The bundle (and most naive ports) does new \DateTime($request->input('start')) on raw query input. An empty or malformed start throws a 500 mid-render and the calendar silently shows nothing while the console logs a fetch error. Always validate:

$data = $request->validate([
    'start' => ['required', 'date'],
    'end'   => ['required', 'date', 'after:start'],
]);

Timezone drift. CalendarEvent serializes with PHP's default timezone; FullCalendar interprets bare ISO strings in the browser's timezone unless timeZone is set. Pick one (store UTC, emit ...Z, set timeZone: 'UTC' on the calendar, or emit offsets) and be consistent — otherwise events shift by an hour twice a year and nobody notices until a DST boundary.

JSON shape: [] vs {}. An empty result must serialize as a JSON array. If your feed service flatMaps into a keyed collection and you forget ->values(), json_encode produces an object and FullCalendar rejects the response. This is the most common "works locally, empty in prod" bug.

One bad event kills the feed. Because everything is serialized in one response, an exception in a single mapper (null end, bad url) blanks the entire calendar. Wrap per-source collection in try/catch, log, and return partial results — a calendar missing one source beats a blank calendar.

N+1 in the mapper. $appointment->customer->name inside a map() over a month of events is 200 queries. Eager-load in the source query (with('customer')) and select only the columns you serialize.

Asset inclusion conflicts. If the bundle's config has an "include FullCalendar JS/CSS" flag (it does in the tattali successor), turn it off when you're already bundling FullCalendar via Vite/mix — double-loading produces the classic "two calendars, one styled" or "plugin already registered" errors.

Extension points, if you must stay: override the feed controller by aliasing the bundle's service or declaring your own route with the same name; add custom payload keys via the DTO's custom-property bag (FullCalendar passes them through to event.extendedProps); and pin the FullCalendar major version — v5→v6 renamed options (defaultView → initialView, eventRender → eventContent), and bundle READMEs of this vintage still show v3/v4 syntax.

Migration checklist when you leave: the contract is just {id, title, start, end, url, backgroundColor, borderColor, textColor, className}. Dump one real response from the old endpoint, snapshot it in a test, then reimplement against that fixture — the frontend won't notice the swap.

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