Use Laravel's HTTP Client with Facebook's Business SDK

_stefanzweifel

Stefan Zweifel

Posted on May 30, 2022

Use Laravel's HTTP Client with Facebook's Business SDK

Recently, I had to integrate the Facebook Marketing API into a project at work. The app not only needed to be able to login with Facebook – solved by using Laravel Socialite – but also make requests to various Facebook API endpoints to fetch data about pages of a business.

Facebook provides a PHP SDK for their "Marketing API" called facebook/php-business-sdk 1. (Naming something consistently is a thing Facebook seems to struggle with. The SDK is also available as facebook/php-ads-sdk)

Anyhow, once the Business SDK is installed in your project, you pass your app ID, app secret and a valid access token to Api::init() and you're good to go.

use FacebookAds\Api;
use FacebookAds\Object\AdAccount;
use FacebookAds\Object\Fields\AdAccountFields;

Api::init($app_id, $app_secret, $access_token);

$fields = [
  AdAccountFields::ID,
  AdAccountFields::NAME,
];

$account = (new AdAccount($account_id))->getSelf($fields);
Enter fullscreen mode Exit fullscreen mode

But how would you start writing tests for your code that interacts with the Facebook API? The SDK doesn't use Laravel's HTTP client. You can't just call Http::fake() and start writing your tests. The requests would still hit Facebook's live servers.

After digging through the source code of the package, I noticed that Facebook follows the Adapter-pattern and allows us to use our own class to make the HTTP requests to their API. Meaning we can create our own Adapter that makes the requests with Laravel's HTTP client and in doing so, allows us to use Http::fake() in our tests.

I've create a new LaravelHttpAdapter-class under App\Domain\Facebook that extends FacebookAds\Http\Adapter\AbstractAdapter and implements FacebookAds\Http\Adapter\AdapterInterface.

The code looks like this:

namespace App\Domain\Facebook;

use FacebookAds\Http\Adapter\AbstractAdapter;
use FacebookAds\Http\Adapter\AdapterInterface;
use FacebookAds\Http\Headers;
use FacebookAds\Http\RequestInterface;
use FacebookAds\Http\Response;
use FacebookAds\Http\ResponseInterface;
use Illuminate\Support\Facades\Http;

class LaravelHttpAdapter extends AbstractAdapter implements AdapterInterface
{
    protected \ArrayObject $opts;

    public function getOpts()
    {
        return $this->opts;
    }

    public function setOpts(\ArrayObject $opts)
    {
        $this->opts = $opts;
    }

    public function sendRequest(RequestInterface $request): ResponseInterface
    {
        $body = $request->getBodyParams()->export();

        $httpClient = Http::withHeaders($request->getHeaders()->getArrayCopy());

        if ($request->getMethod() === 'POST') {
            $response = $httpClient->post($request->getUrl(), $body);
        } else {
            $response = $httpClient->send($request->getMethod(), $request->getUrl());
        }

        $facebookResponse = new Response();
        $headers = new Headers($response->headers());
        $facebookResponse->setBody($response->body());
        $facebookResponse->setHeaders($headers);

        return $facebookResponse;
    }
}

Enter fullscreen mode Exit fullscreen mode

The sendRequest-method is where the magic happens. From the RequestInterface object we extract the URL, headers, HTTP method and body and pass it on to Laravel's HTTP client.2

The response from the HTTP client is then turned into a Response-object that implements Facebook's ResponseInterface.

Alright. Next we have to tell the Facebook SDK to use our Adapter class. For this I've created an InteractsWithFacebookApi-trait. There are multiple places in our app where requests to Facebook are made, so it made sense to extract this bit of code.

Here's how this trait looks like.

namespace App\Domain\Facebook\Concerns;

use App\Domain\Facebook\LaravelHttpAdapter;
use FacebookAds\Api;
use FacebookAds\Http\Client;
use Illuminate\Support\Facades\Http;

trait InteractsWithFacebookApi
{
    public function setupFacebookApi(string $accessToken)
    {
        // Default Init method to create new Facebook API Instance
        $api = Api::init(
            config('services.facebook.client_id'),
            config('services.facebook.client_secret'),
            $accessToken
        );

        // Build new API instance where Laravel's HTTP client
        // is used to make HTTP requests to Facebooks API.
        // Allows us to use Http::fake() in tests.
        $httpClient = new Client();
        $httpClient->setAdapter(new LaravelHttpAdapter($httpClient));
        $newApiInstance = new Api($httpClient, $api->getSession());

        $api::setInstance($newApiInstance);
    }
}

Enter fullscreen mode Exit fullscreen mode

The setupFacebookApi-method accepts an access token and inits the API as documented in the SDKs README.

As the SDK doesn't use any sort of IOC-container, we have to manually build a new instance of Facebook's internal HTTP client class. We then call the setAdapter()-method on the Client-object and pass an instance of our LaravelHttpAdapter-class to it.

Next we create a new API instance using the updated HTTP client and the existing credentials. To finish things off, we call setInstance() on the existing API instance and tell the SDK to use our new API instance.

Writing Tests#

Writing tests for code that interacts with the Facebook API is now a piece of cake.

The test below asserts that an ActivateAdCampaignAction-class makes a request to the Facebook API endpoint, sends ['status' => 'ACTIVE'] and returns ['success' => true]. All without ever hitting a real server or writing mocks for the Facebook SDK.

/** @test */
public function it_makes_http_request_to_facebook_to_activate_given_ad_campaign()
{
    Http::fake([
        'https://graph.facebook.com/*' => Http::response(['success' => true]),
    ]);

    $adCampaign = AdCampaign::factory()->create([
        'active' => 0,
    ]);

    app(ActivateAdCampaignAction::class)->execute($adCampaign);

    Http::assertSent(function (Request $request) {
        return $request['status'] === 'ACTIVE';
    });
}

Enter fullscreen mode Exit fullscreen mode

Of course you could go even further.

The Http::fake()-call currently contains a wildcard check (https://graph.facebook.com/*) and not a check for a specific API endpoint (https://graph.facebook.com/v13.0/{adCampaignId}).

Or the response passed to Http::response() is hard coded in the test case. If you need the response multiple times you could extract that and create an AdCampaignResponses-class. It could have a successfullUpdate()-method on it that returns the data.


I despise working with Facebook and their API. Their documentation is ridiculously bad, their developer portal regularly shows out of date information or just doesn't work at all. And of course the company itself is shady AF.

Despite all that, Facebook (still) dominates the ad market in my region and businesses use it to attract customers.

At least I could figure out a clean way to use their SDK and writing tests familiar to Laravel developers.


  1. Fun Fact: Their PHP SDK to interact with their graph API has been deprecated is not compatible with PHP 8. That's coming from a company worth billions of USD, thousands of engineers and which was built on PHP.

  2. The project I'm working on only makes GET and POST requests to Facebook's API. So this implementation might break if you have to make PATCH, PUT or DELETE requests. Feel free to send me your patches.

💖 💪 🙅 🚩
_stefanzweifel
Stefan Zweifel

Posted on May 30, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related