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

Ms Graph Laravel Package

smorken/ms-graph

A Laravel package for integrating Microsoft Graph, making it easy to access Microsoft 365 data and services such as users, mail, calendars, and files through a simple, fluent PHP API.

View on GitHub
Deep Wiki
Context7

Getting Started

Before you write any PHP, register the app in Azure AD. This is where most of the day is spent, not in the package.

  1. Azure Portal → Entra ID → App registrations → New registration.
  2. Choose the flow:
    • Application permissions (client credentials, no user) for daemons, scheduled syncs, admin tooling.
    • Delegated permissions (needs a signed-in user) for anything acting on behalf of a person.
  3. Add API permissions → Microsoft Graph → pick the narrowest scopes you can (e.g. Mail.Send, Calendars.Read, User.Read.All).
  4. Click Grant admin consent. Skip this and every call fails with 403 while the token itself looks perfectly valid — see Gotchas.
  5. Create a client secret under Certificates & secrets and note the expiry date. Put it in your calendar now.

Install. The package is not on Packagist, so add a VCS repository first:

"repositories": [
  { "type": "vcs", "url": "https://bitbucket.org/scott_morken/msgraph.git" }
]
composer require smorken/ms-graph

Pin to a commit, not dev-master — nothing upstream is going to warn you about a breaking push.

Publish and configure. The service provider name follows the usual convention, but verify it from the package's composer.json extra.laravel.providers before assuming:

php artisan vendor:publish --provider="Smorken\MsGraph\MsGraphServiceProvider"

Expect config/msgraph.php with tenant/client/secret keys. Fill them from environment variables — never inline them:

MSGRAPH_TENANT_ID=
MSGRAPH_CLIENT_ID=
MSGRAPH_CLIENT_SECRET=

Where a developer should look, in order:

  1. config/msgraph.php — the whole surface area is basically this file plus one client class.
  2. The service provider — it tells you whether you get a singleton binding, a facade, or both, and which class is the entry point.
  3. The client class itself — read it end to end. It is small; if it takes more than fifteen minutes, that is a signal about code quality.

First use case to prove it works — the cheapest possible end-to-end path:

// app/Console/Commands/GraphSmokeTest.php
public function handle()
{
    $graph = app(\Smorken\MsGraph\MsGraph::class); // confirm class name from the provider

    $me = $graph->get('/me');                       // or $graph->users()->find(...)
    dump($me);
}

If you get a 200 with a JSON body back, auth, tenant, scopes, and consent are all correct. If not, the error body tells you which of those four is wrong — read the error.code and error.message, not the HTTP status.

Do this against a dev tenant with a throwaway app registration. Do not start with production credentials and real mailboxes.

Implementation Patterns

Wrap it before you use it. The single most valuable thing you can do on day one is refuse to call the package directly from controllers and jobs. Define the interface your application actually needs:

interface MailSender
{
    public function send(string $to, string $subject, string $body): void;
}

class GraphMailSender implements MailSender
{
    public function __construct(private $graph) {}

    public function send(string $to, string $subject, string $body): void
    {
        $this->graph->post("/users/{$this->from}/sendMail", [ /* payload */ ]);
    }
}

This costs an hour and buys you a one-class migration to the official microsoft/microsoft-graph SDK later. Given the maintenance profile, assume that migration is coming.

Application-permission pattern (the one this package is best at). Daemon jobs that read a shared calendar, send notification mail, or sync directory users. No user interaction, no token refresh dance — the client-credentials grant gives you a token, you use it, it expires in an hour. Schedule it:

// app/Console/Kernel.php
$schedule->job(new SyncDirectoryUsers)->hourly()->withoutOverlapping();

Keep the job idempotent and batched; Graph is a remote service and hourly syncs will eventually overlap or retry.

Delegated-permission pattern (where it gets awkward). If your feature acts as the user — "show my Outlook calendar in our dashboard" — you need an OAuth authorization-code flow with redirect handling, which is Laravel Socialite territory or a dedicated Azure provider. Check whether the package supports the authorization-code grant or only client credentials; if it only does the latter, the delegated case is entirely on you and the package is doing little more than holding a token. In that case, skip to Socialite plus a small Graph client.

Token caching — verify it yourself. The day-to-day cost of a Graph integration is token acquisition. If the package fetches a fresh token on every call, you are doubling your request count and hitting the token endpoint's own rate limits under any volume. Check the client class for a Cache::remember around the token call. If it is absent, add it in your wrapper rather than patching the vendor file:

$token = Cache::remember('graph.token', now()->addMinutes(50), fn () => $this->fetchToken());

Cache by tenant + scope-set, and always leave a margin under the real 60-minute expiry.

Keep Graph calls out of request/response cycles. Mail sends, calendar writes, and directory syncs belong in queued jobs. A Graph call in a web request adds 200–800 ms of latency you cannot control and turns a Microsoft outage into your outage.

Test against a fake, not a tenant. Because the package is a thin HTTP wrapper over a Guzzle/Illuminate HTTP client, you can intercept at that layer. In tests, bind a fake MailSender for domain tests; for the Graph-facing adapter, inject a client with a mocked handler returning fixture JSON. Never let CI touch a real tenant — credentials in CI are a leak waiting to happen and a dev tenant is not a test environment.

Integration checkpoint per endpoint. Every time you add an endpoint, ask whether the wrapper is still earning its place. The first three calls are cheap. The fourth one that needs @odata.nextLink paging or a 429 retry is usually the moment the calculation flips.

Gotchas and Tips

403 with a valid token is almost always admin consent. The token decodes fine, scopes look right, and Graph still refuses. Decode the JWT at jwt.ms and inspect the roles claim (application permissions) or scp claim (delegated). If your scope is not in there, consent was not granted — an app registration alone does nothing.

Application vs delegated permissions are not interchangeable, and the package may only support one. Do not discover this after writing the integration. Read the auth code first, before you build anything on top of it.

Token expiry in long-running workers. Queue workers and Octane processes are long-lived. A singleton client that captured a token at boot will be serving an expired token hours later. Cache the token with a TTL and re-resolve per job, or explicitly refresh on 401.

config:cache will bite you. If the package reads env() directly inside the client instead of from config/msgraph.php, deploys that run php artisan config:cache will see null. env() outside config files is a classic Laravel smell — grep for it and move those reads into the config file.

429 / Retry-After is probably not handled. Microsoft throttles aggressively, especially on mail and directory endpoints. Graph returns 429 with a Retry-After header in seconds; you must sleep and retry. Wrap calls in a retry helper:

retry(3, fn () => $this->graph->get($path), function ($e) {
    return $e->getCode() === 429 ? $e->getHeader('Retry-After')[0] * 1000 : false;
});

Exponential backoff without honoring Retry-After makes throttling worse, not better.

Paging is manual. Collections come back with @odata.nextLink. If the wrapper does not follow it, a "list all users" call silently returns 100 of several thousand. Always check whether the result is a full page or a complete collection, and never build a sync job that assumes the first response is everything.

Use v1.0, not beta. Some samples and helper methods default to beta, which has no compatibility promise and can change under you. Check the base URL in config and pin it.

Secret expiry is a scheduled outage. An Azure AD client secret expiring silently is the most common failure mode of a working Graph integration — everything is fine for a year and then it is not. Monitor the expiry in your alerting, and prefer certificates over secrets where the compliance bar allows it.

Logging will leak your bearer token. If the package or your own code logs full Guzzle request/response bodies at debug level, the Authorization: Bearer … header goes into your log aggregator. Check before you deploy; redact Authorization in a middleware or handler.

Bitbucket availability is your build's availability. A repo that can be made private without notice is a build-time dependency. Mirror it to your own VCS on day one and point Composer at the mirror. This is not paranoia at zero stars.

No license metadata is surfaced. Confirm the license in the repo before using this in anything commercial. If there is no license file, your legal position is "all rights reserved" by default — get that resolved or write your own implementation.

Laravel version drift. Check the illuminate/support constraint in composer.json against your target version. The package was almost certainly built against whatever Laravel was current, and nothing upstream will widen that constraint for you. Re-check at every major upgrade.

Missing test suite means you are the test suite. No CI config, no PHPUnit, no fixtures — assume untested. Write contract tests against a recorded HTTP fixture before relying on any endpoint.

Extension points to use instead of forking. Bind your own interface implementations in a service provider, decorate the package's client with your retry/caching layer, and route every call site through your interface. If you must patch the package, fork it under your own namespace rather than editing vendor/ — the next composer install erases your work.

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
shitoudev/permissionmanager