fungio/google-calendar-bundle
Symfony bundle for integrating Google Calendar into your PHP app. Wraps the Google API client with ready-made services to authenticate, manage calendars, and create or sync events, simplifying scheduling features in Symfony projects.
Reality check first. fungio/google-calendar-bundle is a Symfony 3 bundle (last activity around the Symfony 3 era), not a Laravel package. There is no service provider, no facade, no config publish tag, and no Laravel auto-discovery. If you are on a modern Laravel (9/10/11), Composer will almost certainly refuse to install it because it pins symfony/* to ^3.x alongside an old google/apiclient. Before writing any code, decide which of two paths you are on:
Service/ as a reference implementation and re-implement the ~200 lines against a current google/apiclient. This is the path you will take 90% of the time.Where to look first in the repo
DependencyInjection/Configuration.php — the authoritative list of config keys (expected shape: client_id, client_secret, redirect_uri, developer_key, possibly a default calendar_id).Resources/config/services.yml — the service IDs you would inject; note they are likely private in Symfony 3.4+, so you may need an alias or public: true.Service/ — the actual API surface: the class that wraps Google_Service_Calendar and exposes getCalendarEvents, insertEvent, updateEvent, deleteEvent, getCalendars-style methods.README.md — usually the only documentation; treat method signatures there as approximate and verify against the class.Minimal setup (Symfony side)
composer require fungio/google-calendar-bundle
# config.yml
fungio_google_calendar:
client_id: "%google_client_id%"
client_secret: "%google_client_secret%"
redirect_uri: "%google_redirect_uri%"
developer_key: "%google_api_key%"
You still need, outside the bundle: a Google Cloud project, the Calendar API enabled, an OAuth 2.0 client (or a service account with domain-wide delegation), and a stored refresh token per user. The bundle typically does not persist tokens for you — that is your job (a google_tokens table keyed by user).
First use case: read events for a date range to render a dashboard "upcoming meetings" widget. That exercises auth, timezone handling, and pagination in one shot and is easy to fake in tests.
Laravel bridge, if you must use the class directly
// app/Providers/GoogleCalendarServiceProvider.php
public function register(): void
{
$this->app->singleton(GoogleCalendarService::class, function ($app) {
$client = new \Google_Client();
$client->setClientId(config('services.google.client_id'));
$client->setClientSecret(config('services.google.client_secret'));
$client->setRedirectUri(config('services.google.redirect_uri'));
$client->setAccessToken(
\Crypt::decryptString($app['auth']->user()->google_token)
);
return new GoogleCalendarService($client);
});
}
Wrap it in your own interface (CalendarGateway) immediately — see Patterns.
1. Put a seam between your app and the bundle. Never inject the bundle's service into controllers or jobs directly. Define a tiny port and adapt:
interface CalendarGateway
{
/** @return CalendarEvent[] */
public function between(string $calendarId, CarbonInterface $from, CarbonInterface $to): array;
public function create(string $calendarId, CalendarEvent $event): string; // returns google event id
public function update(string $calendarId, string $eventId, CalendarEvent $event): void;
public function delete(string $calendarId, string $eventId): void;
}
The adapter holds the bundle service. Everything else holds your interface. This makes the inevitable rip-and-replace (see Gotchas) a one-file change, and makes tests trivial.
2. Read path — always bound the query. Google returns everything if you don't constrain it:
$events = $this->calendar->getCalendarEvents(
$calendarId,
$from->toRfc3339String(), // timeMin
$to->toRfc3339String(), // timeMax
['singleEvents' => true, 'orderBy' => 'startTime', 'maxResults' => 250]
);
singleEvents=true + orderBy=startTime is mandatory if you want expanded recurrence instances; without it you get RRULE masters you cannot render.maxResults caps at 2500 but paginate at 250 for latency; follow nextPageToken until empty.status === 'cancelled' — cancelled instances still appear in expanded lists.3. Write path — store the Google event ID on your model. The single most important pattern:
$eventId = $gateway->create($calendarId, $event);
$booking->update(['google_event_id' => $eventId, 'google_calendar_id' => $calendarId]);
Then updates/deletes are idempotent and reconcilable. If google_event_id is null, it's a create; if set, it's an update. Add a sync_state enum (pending, synced, failed) and a google_etag column if you ever need conflict detection.
4. Queue every write. Google's API is a network call with a p99 you do not want in a request cycle. Dispatch a job:
class PushBookingToGoogle implements ShouldQueue
{
public $tries = 5;
public $backoff = [1, 5, 15, 60, 300]; // 429 / 5xx
public function handle(CalendarGateway $gateway): void { /* create/update/delete */ }
}
For deletes, do them after your DB commit and use afterCommit on the job; a rollback after a remote create leaves an orphan event.
5. Per-tenant / per-user calendars. Pass calendarId on every call rather than relying on a bundle default — multi-tenant apps almost always end up with one calendar per customer or per resource (room, technician). Resolve the calendar ID from your own table; treat primary as a dev-only convenience.
6. Token handling is yours. The bundle expects a Google_Client that already has an access token. Pattern:
if ($client->isAccessTokenExpired()) {
$client->fetchAccessTokenWithRefreshToken($user->google_refresh_token);
$user->update(['google_token' => encrypt($client->getAccessToken()['access_token'])]);
}
Refresh tokens are long-lived but can be revoked; handle invalid_grant by marking the integration disconnected and prompting re-consent, rather than throwing a 500.
7. Timezone discipline. Normalize at the boundary:
CarbonImmutable in your DB.start.dateTime with an explicit offset (2024-05-01T09:00:00+02:00) and set start.timeZone to the calendar's IANA zone.8. Webhooks / push notifications are not in this bundle. If you need change notifications, you set up watch channels yourself against the Calendar API and handle the X-Goog-* headers in a route. Plan for it explicitly; do not assume the bundle covers it.
9. Testing. Bind a fake CalendarGateway in the container for feature tests; for adapter tests, use a Google_Client mock or record/replay HTTP fixtures. Never let the test suite touch Google — CI will flake on quota and auth.
symfony/* and an old google/apiclient will exceed the cost of copying the service class.google/apiclient v2-era class names (Google_Service_Calendar, Google_Service_Exception) which were renamed in v1/v2 of the modern library (Google\Service\Calendar, Google\Service\Exception). Any snippet you copy from the README will need renaming if you port it forward.$container->get('fungio_google_calendar...') in a controller will throw. Inject via constructor, or add a public alias in services.yml.start.date (not dateTime), and the end.date is exclusive — a one-day event on May 1 has end.date = 2024-05-02. Getting this wrong shifts every all-day event by a day.timeMin/timeMax must be RFC3339 with an offset or Z. 2024-05-01 alone is rejected or silently misinterpreted.developer_key is not a credential. It is a quota/billing key. Auth still needs OAuth or a service account; conflating the two is the #1 configuration mistake.rateLimitExceeded / 429. Always exponential-backoff with jitter and cap concurrency per calendar.403 ... requires a quota project on service-account calls; set quota_project_id on the client.singleEvents=true to find the instance ID, and updates to a single instance require the instance's own event ID, not the master's.$client->setLogger($psrLogger)) at debug level. The most common failures are 401 (expired/revoked token), 403 (scope — you need https://www.googleapis.com/auth/calendar for writes, not calendar.readonly), and 404 (wrong calendarId, or the calendar isn't shared with the service account).app/Services/GoogleCalendar, drop the bundle, and depend on google/apiclient directly. You keep the ergonomics, you lose the Symfony 3 anchor, and you gain access to syncToken incremental sync, watch channels, and freebusy — none of which this bundle exposes.symfony/* constraint.How can I help you explore Laravel packages today?