Использование HTTP-клиента Laravel с Facebook Business SDK

Недавно мне пришлось интегрировать Facebook Marketing API в один из проектов на работе. Приложение должно было не только иметь возможность входа в Facebook — это было решено с помощью Laravel Socialite — но и делать запросы к различным конечным точкам Facebook API для получения данных о страницах бизнеса.

Facebook предоставляет PHP SDK для своего «Marketing API» под названием facebook/php-business-sdk. 1. (Называть что-то последовательно — это то, с чем Facebook, похоже, не справляется. SDK также доступен под названием facebook/php-ads-sdk).

Так или иначе, после установки Business SDK в ваш проект вы передаете ID приложения, секрет приложения и действительный токен доступа в Api::init() и все готово.

use FacebookAdsApi;
use FacebookAdsObjectAdAccount;
use FacebookAdsObjectFieldsAdAccountFields;

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

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

$account = (new AdAccount($account_id))->getSelf($fields);
Вход в полноэкранный режим Выход из полноэкранного режима

Но как начать писать тесты для кода, взаимодействующего с Facebook API? SDK не использует HTTP-клиент Laravel. Вы не можете просто вызвать Http::fake() и начать писать тесты. Запросы все равно попадут на серверы Facebook.

Покопавшись в исходном коде пакета, я заметил, что Facebook следует шаблону Adapter и позволяет нам использовать собственный класс для выполнения HTTP-запросов к их API. Это означает, что мы можем создать собственный адаптер, который будет выполнять запросы с помощью HTTP-клиента Laravel и при этом позволит нам использовать Http::fake() в наших тестах.

Я создал новый класс LaravelHttpAdapter в AppDomainFacebook, который расширяет FacebookAdsHttpAdapterAbstractAdapter и реализует FacebookAdsHttpAdapterAdapterInterface.

Код выглядит следующим образом:

namespace AppDomainFacebook;

use FacebookAdsHttpAdapterAbstractAdapter;
use FacebookAdsHttpAdapterAdapterInterface;
use FacebookAdsHttpHeaders;
use FacebookAdsHttpRequestInterface;
use FacebookAdsHttpResponse;
use FacebookAdsHttpResponseInterface;
use IlluminateSupportFacadesHttp;

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;
    }
}

Вход в полноэкранный режим Выход из полноэкранного режима

Метод sendRequest — вот где происходит волшебство. Из объекта RequestInterface мы извлекаем URL, заголовки, метод HTTP и тело и передаем их HTTP-клиенту Laravel.2

Ответ от HTTP-клиента затем превращается в объект Response, который реализует интерфейс Facebook ResponseInterface.

Хорошо. Далее мы должны указать Facebook SDK на использование нашего класса Adapter. Для этого я создал InteractsWithFacebookApi-трейт. В нашем приложении есть несколько мест, где делаются запросы к Facebook, поэтому имеет смысл извлечь этот фрагмент кода.

Вот как выглядит этот признак.

namespace AppDomainFacebookConcerns;

use AppDomainFacebookLaravelHttpAdapter;
use FacebookAdsApi;
use FacebookAdsHttpClient;
use IlluminateSupportFacadesHttp;

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);
    }
}

Вход в полноэкранный режим Выход из полноэкранного режима

Метод setupFacebookApi принимает токен доступа и инициирует API, как описано в SDK в README.

Поскольку SDK не использует никаких IOC-контейнеров, мы должны вручную создать новый экземпляр внутреннего класса HTTP-клиента Facebook. Затем мы вызываем метод setAdapter() на объекте Client и передаем ему экземпляр нашего класса LaravelHttpAdapter.

Далее мы создаем новый экземпляр API, используя обновленный HTTP-клиент и существующие учетные данные. В завершение мы вызываем setInstance() для существующего экземпляра API и указываем SDK использовать наш новый экземпляр API.

Написание тестов#

Написание тестов для кода, взаимодействующего с Facebook API, теперь стало проще простого.

Тест ниже утверждает, что класс ActivateAdCampaignAction делает запрос к конечной точке Facebook API, отправляет ['status' => 'ACTIVE'] и возвращает ['success' => true]. И все это без обращения к реальному серверу или написания макетов для 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';
    });
}

Вход в полноэкранный режим Выход из полноэкранного режима

Конечно, вы можете пойти еще дальше.

Вызов Http::fake() в настоящее время содержит проверку на подстановочный знак (https://graph.facebook.com/*), а не проверку на конкретную конечную точку API (https://graph.facebook.com/v13.0/{adCampaignId}).

Или ответ, передаваемый в Http::response(), жестко закодирован в тестовом примере. Если вам нужен ответ несколько раз, вы можете извлечь его и создать класс AdCampaignResponses. В нем может быть метод successfullUpdate(), который возвращает данные.


Я презираю работу с Facebook и их API. Их документация до смешного плоха, их портал для разработчиков регулярно показывает устаревшую информацию или просто не работает вообще. И, конечно же, сама компания является сомнительной.

Несмотря на все это, Facebook (по-прежнему) доминирует на рынке рекламы в моем регионе, и предприятия используют его для привлечения клиентов.

По крайней мере, я смог найти чистый способ использования их SDK и написания тестов, знакомых разработчикам Laravel.


  1. Забавный факт: их PHP SDK для взаимодействия с их graph API был устаревшим и не совместим с PHP 8. И это говорит компания с миллиардным капиталом, тысячами инженеров, которая была построена на PHP.

  2. Проект, над которым я работаю, выполняет только GET и POST запросы к API Facebook. Поэтому эта реализация может сломаться, если вам придется делать запросы PATCH, PUT или DELETE. Не стесняйтесь присылать мне свои исправления.

Оцените статью
Procodings.ru
Добавить комментарий