fungio/outlook-calendar-bundle
Symfony bundle that connects PHP applications to Microsoft Outlook calendars via the Microsoft Graph API. Manage events, sync schedules, and authenticate users with OAuth 2.0 for clean, reliable access to Office 365 calendar data.
Reality check before you composer require: this is a Symfony 3 bundle, not a Laravel package. It has no service provider, no facade, no publishable config, and its composer.json will pull symfony/framework-bundle:^3.x, symfony/dependency-injection, and symfony/config — none of which coexist cleanly with a modern Laravel install. With 5 stars, 0 dependents, and a 0.235 score, treat it as a reference implementation of the Outlook API surface, not a dependency you install into vendor/.
Where to look first (in the repo):
README.md — for the expected Azure app registration fields (client id, secret, redirect URI) and the advertised service names.DependencyInjection/Configuration.php and DependencyInjection/*Extension.php — the canonical list of config keys and default service wiring. This is your de facto API documentation.Resources/config/services.yml — the actual service IDs and class names you'd otherwise resolve from the container.Service/, Api/, or Client/ directory — these are the classes worth extracting.Minimal path to a first use case:
Calendars.Read (or Calendars.ReadWrite).laravel/socialite with a Microsoft/Graph driver (or microsoft/microsoft-graph directly). Do not try to reuse the bundle's Symfony OAuth wiring.app/Services/OutlookCalendar/ and replace constructor-injected Symfony services with Laravel contracts.// app/Services/OutlookCalendar/OutlookClient.php (ported)
public function __construct(
private \Illuminate\Http\Client\Factory $http,
private string $accessToken,
) {}
public function events(string $startIso, string $endIso): array
{
return $this->http
->withToken($this->accessToken)
->get('https://graph.microsoft.com/v1.0/me/calendarView', [
'startDateTime' => $startIso,
'endDateTime' => $endIso,
'$select' => 'id,subject,start,end,location,organizer,isAllDay',
'$top' => 100,
])
->throw()
->json('value', []);
}
// routes/web.php
Route::get('/outlook/redirect', fn () => Socialite::driver('microsoft')->redirect());
Route::get('/outlook/callback', function () {
$user = Socialite::driver('microsoft')->user();
OutlookAccount::updateOrCreate(
['user_id' => auth()->id()],
['access_token' => encrypt($user->token),
'refresh_token' => encrypt($user->refreshToken),
'expires_at' => now()->addSeconds($user->expiresIn)]
);
return redirect('/dashboard');
});
Pattern A — Sidecar service (lowest risk, highest ops cost). Stand up a small Symfony 3 app on PHP 7.1/7.4 that actually installs the bundle, expose two or three JSON endpoints (/events, /calendars), and call it from Laravel with Http::baseUrl(...). Only worth it if you need the bundle's exact behavior for parity with an existing system. You inherit Symfony 3's EOL status and a second deployment target.
Pattern B — Extract and adapt (recommended). Pull the API client, DTOs, and response mappers out of the bundle into app/Services/OutlookCalendar/. Replace:
ContainerAware / autowired constructor args → Laravel constructor injection or a ServiceProvider binding.ParameterBag config → config/outlook-calendar.php + env().Bind an interface so the rest of the app never sees the port:
// app/Providers/OutlookCalendarServiceProvider.php
$this->app->bind(OutlookCalendar::class, function ($app) {
return new GraphOutlookCalendar(
http: $app['Illuminate\Http\Client\Factory'],
tokens: $app[OutlookTokenStore::class],
);
});
Token lifecycle as a first-class concern. Store access + refresh tokens per user (encrypted), check expires_at before every call, and refresh in a job rather than inline:
class RefreshOutlookToken implements ShouldQueue
{
public function handle(OutlookAccount $account): void
{
$res = Http::asForm()->post('https://login.microsoftonline.com/common/oauth2/v2.0/token', [
'grant_type' => 'refresh_token',
'client_id' => config('outlook-calendar.client_id'),
'client_secret' => config('outlook-calendar.client_secret'),
'refresh_token' => decrypt($account->refresh_token),
'scope' => 'offline_access Calendars.ReadWrite',
])->throw()->json();
$account->update([
'access_token' => encrypt($res['access_token']),
'refresh_token' => encrypt($res['refresh_token'] ?? decrypt($account->refresh_token)),
'expires_at' => now()->addSeconds($res['expires_in']),
]);
}
}
Sync pattern. Poll calendarView on a scheduler for a rolling window (e.g. −7 to +60 days) per connected account, upsert into a local calendar_events table keyed on (account_id, outlook_id), and dispatch domain events for downstream features:
Schedule::job(new SyncOutlookCalendars)->everyFifteenMinutes();
Keep a last_synced_at and an outlook_change_token column so you can migrate to Graph delta queries later without a schema change.
Integration tips.
->retry(3, 500)), and metrics.$this->mock(OutlookCalendar::class)) instead of hitting Graph.start.dateTime / start.timeZone shape into controllers.symfony/framework-bundle ^3.x, which conflicts with Laravel's illuminate/* constraints and modern PHP. composer require fungio/outlook-calendar-bundle in a Laravel app is likely to fail or to downgrade unrelated packages. Use composer show --tree on a scratch project to see the damage before trying it in your real app.DependencyInjection/Configuration.php are not Laravel config files. Any key you find there must be transcribed by hand into config/outlook-calendar.php.HWIOAuthBundle (or similar) to supply tokens via a service reference. There is no drop-in equivalent in Laravel — you're rebuilding the OAuth handshake with Socialite or Graph.graph.microsoft.com/v1.0). If the bundle targets /api/v2.0/me/events, expect deprecation and behavior gaps. Verify the base URL in the client class before assuming the shape."Romance Standard Time") rather than IANA ("Europe/Paris"). Carbon::parse() will silently misinterpret these. Use the windowsTimezone extension on Graph (Prefer: outlook.timezone="UTC") or map explicitly.value will double-count or miss moved occurrences. Use calendarView (which expands recurrence) rather than /events.$top and return @odata.nextLink. A loop that ignores it silently truncates busy calendars — a classic "why are half our events missing" bug.429 with Retry-After. Respect it; ->retry() without backoff gets you throttled harder.410 Gone. If you build a delta sync, handle 410 by resetting the token and doing a full resync rather than crashing the scheduler.Calendars.Read delegated permission still needs a user to consent (or tenant admin consent). Local success in a personal tenant frequently fails in an enterprise tenant — test with the customer's actual tenant early.Authorization), and response body in a dedicated channel. Nine out of ten failures here are redirect-URI mismatch, missing scopes, or an expired refresh token — all invisible without the raw exchange.OutlookCalendar interface → GraphOutlookCalendar adapter → CachingOutlookCalendar decorator. If Microsoft changes something, you patch one class.Verdict for a Laravel team: use the repository as a specification for the Outlook calendar operations you need, port ~200–400 lines into app/Services/OutlookCalendar/, and build on microsoft/microsoft-graph or Socialite. Do not add it to composer.json.
How can I help you explore Laravel packages today?