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

Microsoft Graph Laravel Package

microsoft/microsoft-graph

The Microsoft Graph SDK for PHP lets you authenticate with Azure AD and call Microsoft 365 APIs to work with users, mail, files, calendars, and Teams from your PHP apps. Requires PHP 8.2+ and Composer.

View on GitHub
Deep Wiki
Context7

Getting Started

Understand what this is first: microsoft/microsoft-graph is not a Laravel package. It is a Kiota-generated OpenAPI client for the Microsoft Graph REST API — thousands of generated request builders and models, no service provider, no facade, no config file, no artisan command. Day-to-day you treat it like an HTTP client you happen to have typed classes for. Most of your integration work is wiring, caching tokens, and wrapping it so controllers never see Kiota types.

Install and pin.

composer require microsoft/microsoft-graph:2.48.*

Requires PHP 8.2+. Pin an exact minor — the SDK is regenerated from Graph metadata twice a month, so ^2 will silently move request-builder and model surfaces under you.

Where to look first, in this order:

  1. Azure portal → App registrations. You need this before any code: tenant ID, client ID, client secret (or certificate), API permissions, and admin consent granted.
  2. README.md — the auth contexts and the ->wait() idiom.
  3. docs/README.md and docs/authentication_samples.md (especially access token management and creating a graph client).
  4. UPGRADING.md before any major bump.
  5. Ignore src/. It is generated; reading it tells you about Kiota, not about Graph. Use Graph Explorer (aka.ms/ge) to prototype the URL and confirm the permission it needs.

First use case — app-only, from a queued job. Register a singleton so credentials and token state are shared per request:

// config/graph.php
return [
    'tenant_id'     => env('GRAPH_TENANT_ID'),
    'client_id'     => env('GRAPH_CLIENT_ID'),
    'client_secret' => env('GRAPH_CLIENT_SECRET'),
    'scopes'        => ['User.Read.All', 'Mail.Send'],
];
// AppServiceProvider::register()
$this->app->singleton(GraphServiceClient::class, fn () => new GraphServiceClient(
    new ClientCredentialContext(
        config('graph.tenant_id'),
        config('graph.client_id'),
        config('graph.client_secret'),
    ),
    config('graph.scopes'),
));

Then inject it, but wrap it immediately (see next section) so you never call ->wait() from a controller:

$user = $graph->users()->byUserId($upn)->select(['id', 'displayName', 'mail'])->get()->wait();
echo $user->getDisplayName();

That single call — "look up a user, then send a mail / invite to a Teams meeting / grant a licence" — covers 80% of real Laravel usage of this SDK.

Implementation Patterns

Isolate the client behind your own class. The generated API changes shape on regeneration; your app shouldn't. One thin service (App\Services\Graph\GraphMailer, GraphUsers) that owns ->wait(), error mapping, and retries, plus an interface you can fake in tests. Everything downstream depends on your interface, not on Microsoft\Graph\*.

Pick the auth context by permission model, not by convenience.

  • ClientCredentialContext — app-only, no signed-in user. Cannot use /me, cannot use User.Read. Needs Application permissions with tenant-wide admin consent.
  • AuthorizationCodeContext — delegated, acts as the user. Needed for /me, /me/messages, OneDrive personal, Teams chat-as-user.
  • OnBehalfOfContext — service calling Graph on behalf of a user who is already authenticated to your app (typically an API/bot scenario).

Handle the delegated flow the Laravel way. Redirect the user to https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize with your client_id, redirect_uri, scope, state (store state in session), then exchange the returned code in your callback for tokens and persist them. Many teams skip the SDK here and use Socialite's Azure driver to obtain the code/tokens, then hand a pre-acquired token to the client — see the "initialising with an already acquired access token" sample in docs/authentication_samples.md.

Token caching is the pattern that bites. The auth provider caches tokens in memory per PHP process. Under PHP-FPM that means a fresh token request roughly every request; under queue workers it's better but still lost on restart. For real workloads, supply a persistent token cache (implement the auth library's cache interface against Laravel's Cache/Database store, keyed by tenant+client+scopes) or store the delegated refresh token encrypted on the user model and refresh proactively. Also: ->default scopes for app-only, explicit scopes for delegated — build two client singletons if you need both.

Query building. Request builders expose OData parameters as fluent calls; always select() narrowly and top() deliberately:

$graph->users()
    ->filter("accountEnabled eq true and startsWith(mail,'eng-')")
    ->select(['id', 'mail', 'department'])
    ->top(50)
    ->get()->wait()->getValue(); // array of User models

Paging. Collection responses give you @odata.nextLink; either loop on getOdataNextLink() or use the SDK's PageIterator helper (in Microsoft\Graph\Core\Tasks) for a lazy callback per page. Never fetch all users in one shot. For incremental sync (users, mail, drive items), use a delta query and persist the deltaLink — that is the difference between a 2-second cron and a 40-minute one.

Batching. Microsoft\Graph\Core\Requests\BatchRequestContent / BatchResponseContent let you send up to 20 requests in one HTTP call. Use it for "get photos for 50 users" or "read presence for the whole team". It cuts throttling dramatically and is the single highest-leverage optimisation with this SDK.

Writes are just models plus post/patch. Generated models use setters/getters and accept property arrays in the constructor. Send mail from a service mailbox:

$message = new Message();
$message->setSubject('Your invoice is ready');
$message->setBody(new ItemBody(['contentType' => BodyType::TEXT, 'content' => $body]));
$message->setToRecipients([new Recipient(['emailAddress' => new EmailAddress(['address' => $to])])]);

$graph->users()->byUserId($sender)->sendMail()->post($message)->wait();

Rate limiting and queues. Put every bulk or write operation on a queued job. Use RateLimiter::for('graph', ...) for your own concurrency ceiling, and honour 429/Retry-After with exponential backoff ($job->release($seconds)).

Testing. Don't hit Graph in tests. Bind a fake implementation of your own interface. If you must exercise the SDK itself, point the request adapter at a mock HTTP client (Kiota's RequestAdapter interface allows this), or run against the Graph developer proxy / mocked Graph responses rather than a real tenant.

When to drop to raw HTTP. For a single endpoint, or anything only in beta, Http::withToken($token)->get('https://graph.microsoft.com/v1.0/...') is often less code than navigating the generated tree. Keep both: SDK for typed CRUD, raw Http for one-offs and webhook-driven work. If you need beta models, there is a separate Microsoft\Graph\Beta\GraphServiceClient — don't mix it into production code without a reason.

Gotchas and Tips

  • Forgetting ->wait(). Every operation returns a Promise. Without wait() nothing happens, and you get a promise object where you expected a model — usually surfacing later as a confusing null or a JSON-encode error. Standardise on ->get()->wait() inside one wrapper class.
  • 403 does not mean "wrong code". It almost always means wrong permission kind (Application vs Delegated) or missing admin consent. User.Read cannot be granted app-only — use User.Read.All or User.ReadBasic.All. App-only Mail.Send additionally requires an ApplicationAccessPolicy in Exchange to scope the mailbox, otherwise it fails with an unhelpful error.
  • /me requires delegated auth, full stop. Using it with ClientCredentialContext returns a 400/403 that reads like a permissions problem.
  • Error inspection. Catch Microsoft\Kiota\Abstractions\ApiException and log getResponseStatusCode(), getError()->getCode(), getError()->getMessage(). OData errors nest; the top message is often generic.
  • Throttling is per-tenant and shared. Bulk loops hit 429 fast. Retry-After is authoritative — respect the header rather than a fixed backoff. Log request-id and client-request-id from response headers; Microsoft support asks for them, and they're your only trace into their side.
  • Date-only fields. Graph returns hireDate, birthday etc. as date-only; the SDK models them as its own Date type, not \DateTime. Passing a Carbon instance where a Date is expected causes serialisation errors that look like schema mismatches.
  • OData filter injection. $filter is a string expression. Never interpolate user input raw; quote and escape single quotes (O'Neil → O''Neil) or use byUserId-style addressable paths instead.
  • Payload size. Default responses are enormous (users, messages, drive items). select() aggressively and avoid expand('manager') inside loops — that's an N+1 against a throttled API.
  • Vendor weight and autoload. The package ships tens of thousands of classes. composer install and dump-autoload -o are slow; keep classmap-authoritative autoloading and OPcache on, and expect CI images to grow. Consider installing it only in the service that needs it if you run a monorepo.
  • Version drift. The regenerated releases are almost entirely "update request builders and models". Minor bumps rename builders, change setters, and add nullability. Pin exactly, read UPGRADING.md on majors, and never upgrade this dependency the afternoon before a release. v1 and v2 APIs are fundamentally different ($graph->createRequest() vs Kiota builders) — don't copy v1 snippets from blog posts into a v2 codebase.
  • Sovereign / national clouds. Base URL and auth endpoint differ (graph.microsoft.us, microsoftgraph.chinacloudapi.cn, etc.). Changelog entries show this has been a repeated bug source; verify the base-URL parameter is actually applied before trusting a multi-cloud deployment.
  • Extension points worth knowing: a custom AccessTokenProvider (for tokens minted elsewhere, e.g. a shared auth service), a custom token cache, and a decorator around your own service classes for logging/metrics. That's where you customise — not by editing or subclassing generated builders.
  • Debugging shortcut: reproduce the exact call in Graph Explorer first. If it works there and fails in PHP, it's auth or a query-param difference, not the SDK. If it fails there too, it's permissions or licence (many Graph features need an Entra ID P1/P2 or M365 licence, e.g. some Teams and identity-protection endpoints).
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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
cgv/localized-resource