combindma/laravel-facebook-pixel
Easily integrate Facebook Pixel into your Laravel app. Track page views and custom events from PHP or Blade, fire standard events, and manage configuration with a lightweight, developer-friendly API for adding Meta Pixel tracking to any Laravel project.
Install and wire up
composer require <vendor>/laravel-facebook-business-sdk
php artisan vendor:publish --tag=facebook-business-config
Add credentials to .env rather than the config file:
FACEBOOK_APP_ID=
FACEBOOK_APP_SECRET=
FACEBOOK_ACCESS_TOKEN=
FACEBOOK_BUSINESS_ID=
FACEBOOK_API_VERSION=v23.0
The package is a thin Laravel-flavoured wrapper around facebook/php-business-sdk, so the mental model is: the package gives you config + container bindings + a facade; the SDK itself (FacebookAds\Api, FacebookAds\Object\...) does the actual work. Register the SDK's Api instance once (typically in a service provider or a package-provided singleton) and resolve it from the container instead of calling Api::init() in controllers.
Where to look first
config/facebook-business.php — the only file you must understand up front.Api is bound as a singleton and whether a facade alias is registered.FacebookAds\Object\AdAccount, Campaign, AdSet, Ad classes — these are what you'll use 90% of the time.First use case — read something, don't write anything yet:
use FacebookAds\Object\AdAccount;
$account = new AdAccount('act_1234567890');
$campaigns = $account->getCampaigns([
'fields' => ['id', 'name', 'status', 'objective', 'daily_budget'],
'limit' => 25,
]);
Then composer update once on a clean branch and confirm the resolved SDK version is 25.0.1 or lower (see Gotchas — 5.3.6 explicitly pins this).
Wrap the SDK, don't sprinkle it
The SDK objects are stateful and awkward to fake. Put them behind your own service so tests and call sites stay clean:
final class MetaAdsClient
{
public function __construct(private readonly AdAccount $account) {}
public function activeCampaigns(): Collection
{
return collect($this->account->getCampaigns([
'fields' => ['id','name','objective','daily_budget'],
'effective_status' => ['ACTIVE'],
])->getArrayCopy());
}
}
Bind AdAccount in a service provider using the configured ad account ID — then every job and controller gets the right account injected. For multi-account apps, use a factory keyed by account ID and a scoped() binding.
Pagination — always use the cursor, never limit: 500
The SDK exposes a cursor on every edge. The pattern that survives large accounts:
$cursor = $account->getCampaigns(['fields' => ['id','name'], 'limit' => 100]);
while ($cursor->valid()) {
foreach ($cursor as $campaign) {
// persist / dispatch
}
$cursor->fetchAfter(); // follows paging.next
}
Do this inside a queued job with a checkpoint (last processed ID) so a rate-limit exception doesn't restart the whole crawl.
Heavy reads belong in jobs
Sync calls will blow past PHP's max_execution_time and Laravel's request timeout. Standard pattern: controller dispatches SyncCampaignsJob, job uses $this->release($backoff) on FacebookAds\Http\Exception\RequestException with a rate-limit code. Wrap the loop in RateLimiter or Redis::throttle() so a burst of jobs doesn't hit Meta's per-app limits.
Batching for writes
When creating/updating more than a handful of objects, use the SDK's batch API rather than N sequential calls. It's the single biggest performance lever in this package — orders of magnitude fewer round trips, and partial failures come back as per-item results rather than an exception killing the whole loop.
Webhooks / lead ads
If you consume Lead Ads or Messenger webhooks, verify X-Hub-Signature-256 against the app secret before touching the payload, and return 200 fast, then process the payload in a job. Keep the verification in middleware, not the controller — you'll reuse it.
Token handling
Long-lived tokens still expire. Store them in the database (encrypted cast), not .env, if you support multiple businesses or need rotation. Add a scheduled command that exchanges/refreshes and alerts on failure — a silently expired token looks identical to "no campaigns" in your UI.
The 5.3.6 pin is the headline. The package now requires facebook/php-business-sdk ~25.0.0,<25.0.2. Practically:
25.0.2 or later locked in composer.lock, composer update will downgrade it. Check the diff before committing — a silent downgrade in a lockfile is easy to miss in review.>=25.0.2, you'll get an unresolvable conflict and Composer will refuse. That's intentional; don't try to force it with --ignore-platform-reqs or a replace block.25.0.2+ while on this package version. Pin the package ("5.3.6") rather than floating (^5.3) so a future release doesn't move the SDK version under you unexpectedly.composer.json, not a runtime check — nothing will warn you at boot time if you somehow end up on a bad version.Debugging is opaque by default. Attach a logger to see the wire traffic:
Api::instance()->setLogger(new \Monolog\Logger('fb'));
Also inspect Api::instance()->getHttpClient()->getLastRequest() / getLastResponse() after a failure — the SDK throws generic RequestExceptions and the real error detail (error_user_msg, error_subcode) lives in the response body.
Error codes worth branching on. 17/613 = rate limit (back off, don't retry immediately), 190 = invalid/expired token (alert, don't retry), 100 = bad parameter (your bug, fail loudly), 368 = temporarily blocked. Catching RequestException and rethrowing everything is the most common mistake.
Config caching bites. php artisan config:cache is fine, but any code calling env() directly inside the package's service provider will return null in production. If the client silently uses an empty token, check this first.
Api::init() is global state. It's a static singleton in the SDK. If you ever need two businesses with different tokens in one process, you must re-init between calls — which is why per-request apps should use one instance, and multi-tenant apps should isolate by job. Don't assume the package's binding handles this for you; verify in the service provider.
Version field is your friend and your enemy. Meta deprecates API versions on a rolling schedule. If the package's default api_version in config is older than ~2 years, set it explicitly in .env and add a calendar reminder — deprecations arrive as unhelpful 400s.
Extension points. The HttpClient is swappable via Api::instance()->setHttpClient() — useful for injecting retry middleware or logging. If the package doesn't expose a configuration hook for the client, that's a reasonable place to send a PR rather than monkey-patching in a service provider.
Testing. Don't hit the real API in tests. Since the SDK classes are concrete and hard to mock, fake at your wrapper boundary (MetaAdsClient) rather than trying to mock AdAccount. For integration tests, record real responses once and replay them via a custom HttpClient.
How can I help you explore Laravel packages today?