joelbutcher/laravel-facebook-graph
A Laravel integration for the Facebook Graph SDK, making it easy to authenticate, query the Graph API, and manage Facebook resources from your PHP applications with familiar Laravel conventions.
joelbutcher/laravel-facebook-graph is a thin Laravel integration layer over the official facebook/graph-sdk. You get a manager class, a Facebook facade, config-driven app credentials, and Laravel-native session handling for the OAuth helpers — instead of hand-wiring Facebook\Facebook in every project.
Install and publish
composer require joelbutcher/laravel-facebook-graph
php artisan vendor:publish --tag=facebook-config # or --provider="JoelButcher\Facebook\FacebookServiceProvider"
Package discovery registers the service provider and the Facebook facade alias, so no manual config/app.php edits are needed on modern Laravel.
Credentials in .env
FACEBOOK_APP_ID=...
FACEBOOK_APP_SECRET=...
FACEBOOK_DEFAULT_GRAPH_VERSION=v21.0
FACEBOOK_DEFAULT_ACCESS_TOKEN= # optional, leave empty in most apps
First use case — read the authenticated user's profile with a token you already hold:
use JoelButcher\Facebook\Facades\Facebook;
$response = Facebook::get('/me?fields=id,name,email', $user->facebook_token);
return $response->getGraphUser()->asArray();
Where to look first
config/facebook-graph.php — this maps almost 1:1 onto the SDK's config array (app_id, app_secret, default_graph_version, default_access_token, persistent_data_handler, http_client_handler, enable_beta_mode, enable_appsecret_proof). Nearly every "why is it doing that?" question is answered here.JoelButcher\Facebook\Facebook (the class behind the facade) — read its public methods before the SDK docs; it exposes getClient(), getOAuth2Client(), getRedirectLoginHelper(), and extend().Facebook\ SDK classes for anything the wrapper doesn't cover. The package is deliberately thin — the escape hatch is always Facebook::getClient().v1.5.0 only adds Laravel 12 / illuminate/* ^12.0 compatibility; there is no API change, so anything written against v1.4.x carries over unchanged.
Inject the manager, don't reach for the facade. For anything in a service class or job, type-hint JoelButcher\Facebook\Facebook (or the manager binding) in the constructor. The facade is fine in controllers and Blade, but container injection makes the class swappable in tests.
OAuth login flow. Generate the login URL, handle the callback, then immediately upgrade the short-lived token:
$helper = Facebook::getRedirectLoginHelper();
$url = $helper->getLoginUrl(route('facebook.callback'), ['email', 'pages_manage_posts']);
// callback
$token = $helper->getAccessToken(); // ~1-2h token
$long = Facebook::getOAuth2Client()->getLongLivedAccessToken($token); // ~60 days
$user->update(['facebook_token' => encrypt($long->getValue())]);
Store tokens encrypted and record the expiry (getExpiresAt()); treat anything near expiry as needing a refresh rather than discovering it via a 190 error in production.
Page tokens. The common real workflow is: user token → GET /me/accounts → store the page token and page id per tenant. Page tokens don't expire on a schedule but can be invalidated by a password change or revoked permission, so keep the user token around to re-derive them.
Multi-app / multi-tenant setups. Use Facebook::extend('tenant', fn ($app) => new Facebook([...])) and create named drivers per Facebook app rather than mutating the default config at runtime. This is also the cleanest way to separate a "marketing app" from a "login app" with different scopes.
Explicit versions per call. Pass the graph version on calls that must be pinned while you migrate between versions, rather than flipping the global default and breaking every endpoint at once.
Queue the writes. POST /me/feed, /comments, ad mutations and similar should go through jobs with backoff. Facebook rate limits are per-app and per-page, so a single retry loop in a controller will bite you at the first traffic spike.
Webhook handling is yours to write. The package gives you the client; it does not verify X-Hub-Signature-256 or handle the hub.challenge handshake. Use hash_hmac('sha256', $rawBody, config('facebook-graph.app_secret')) with hash_equals, and exclude that route from CSRF.
Testing. Bind a stub Facebook instance (or a preconfigured client with a fake access token) in the container. Because the SDK speaks to Guzzle directly rather than through Laravel's HTTP client, Http::fake() will not intercept these calls — expect to mock the manager or inject a mock Facebook\Client.
Http::fake() doesn't work. The biggest time-waster for newcomers. The SDK owns its own Guzzle instance; either mock at the manager level or supply a custom http_client_handler.default_graph_version in config is a hardcoded string — put a recurring ticket on it, and expect a wave of "Unsupported get request" errors when it lapses. v1.5.0 does not change the pinned default.getLoginUrl()/getAccessToken() will misbehave in stateless API routes, queue workers, and console commands. If you're doing token exchange outside a browser session, use the OAuth2Client directly instead of the redirect helper.Facebook\Exceptions\FacebookSDKException means you configured something wrong (bad credentials, unusable token). Facebook\Exceptions\FacebookResponseException means the Graph API returned an error. Catch them separately — the second carries getStatusCode(), getErrorCode() and getErrorSubcode(), which you need to distinguish a rate limit (codes 4/17/32) from an expired token (code 190) from a permission problem (code 200).enable_appsecret_proof guards against token misuse once an app has an app secret; leaving it off is a finding in most security reviews.env() values are only read at cache time. After changing FACEBOOK_*, run php artisan config:clear — otherwise you'll debug a "wrong app id" that isn't in your .env at all.facebook/graph-sdk version in composer.lock is the one the package expects (it's an abandoned package, so vendor it or pin it deliberately). Run your OAuth callback test and one authenticated get() before shipping — those are the two paths that touch the SDK's config and session handling.GET /debug_token via Facebook::get('/debug_token?input_token=...') (or getOAuth2Client()->debugToken()) returns scopes, expiry, and app id — far more useful than the opaque 190 error when a token silently stops working.Facebook::extend() for named drivers, getClient() for anything the wrapper doesn't proxy, and the http_client_handler config key when you need to swap Guzzle for a client with custom middleware (logging, retries).How can I help you explore Laravel packages today?