Курсор FacebookAds SDK не работает на / reportstats endpoint


Я использую facebook-php-ads-sdk версии 2.2.4 (самая новая на момент написания этой статьи). Я заметил, что курсор, возвращаемый с вызовами $adAccount->getReportStats(), разбивается с помощью неявной опции fetch. Курсор ожидает, что увидит в ответе следующую структуру:

{
    "paging": { "cursor": { "after": "<some_url>" } }
}

Однако конечная точка /reportstats возвращает информацию о подкачке, структурированную следующим образом:

{
    "paging": { "next": "<some_url>" }
}
Я мог бы поклясться, что он работал, как и ожидалось несколько дней назад, так что, возможно, API facebook изменился?

Вот пример:

$adAccount = new AdAccount('some_id');

// cursor is an instance of FacebookAdsCursor.
$cursor = $adAccount->getReportStats($someFields, $someParams);
$cursor->setUseImplicitFetch(true);

foreach ($cursor as $item) {
   // do stuff
}
// cursor is never advanced to next paged result.

Как вы можете видеть в этом вырезанном из FacebookAds Cursor, когда неявная выборка установлена в true, курсор проверяет только paging.cursor.after|before:

<?php

namespace FacebookAds;

use FacebookAdsHttpRequestInterface;
use FacebookAdsHttpResponseInterface;
use FacebookAdsObjectAbstractObject;

class Cursor implements Iterator, Countable, arrayaccess {

      // ...

      /**
       * @return string|null
       */
      protected function getLastRequestBefore() {
        $content = $this->getLastResponse()->getContent();

        return isset($content['paging']['cursors']['before'])
          ? $content['paging']['cursors']['before']
          : null;
      }

      /**
       * @return string|null
       */
      protected function getLastRequestAfter() {
        $content = $this->getLastResponse()->getContent();

        return isset($content['paging']['cursors']['after'])
          ? $content['paging']['cursors']['after']
          : null;
      }

      // ...
}

Фактический запрос curl sdk facebook генерирует:

curl -G 
  -d "data_columns=["time_start","time_stop","spend","impressions","clicks","unique_clicks","social_clicks","unique_social_clicks","cpm","unique_ctr","reach","frequency","cost_per_unique_click","cost_per_action_type","cost_per_total_action","cpp","cpc","ctr","account_id","account_name","campaign_group_id","campaign_group_name","campaign_id","campaign_name"]" 
  -d "date_preset=last_90_days" 
  -d "time_increment=1" 
  -d "access_token=<nice_try_dude>" 
  -d "appsecret_proof=<not_getting_this_either>" 
  https://graph.facebook.com/v2.2/act_<account_id>/reportstats

И вот ответ:

{
  "data": [
    {
      "campaign_id": "<campaign_id>",
      "date_start": "2014-12-18",
      "date_stop": "2014-12-18",
      "time_start": 1418878800,
      "time_stop": 1418965200,
      "spend": 39.39,
      "impressions": 5127,
      "clicks": 65,
      "unique_clicks": 55,
      "social_clicks": 31,
      "unique_social_clicks": 27,
      "cpm": 7.6828554710357,
      "unique_ctr": 1.0880316518299,
      "reach": 5055,
      "frequency": 1.0142433234421,
      "cost_per_unique_click": 0.71618181818182,
      "cost_per_action_type": 0.67913793103448,
      "cost_per_total_action": 0.67913793103448,
      "cpp": 7.7922848664688,
      "cpc": 0.606,
      "ctr": 1.2677979325141,
      "account_id": "<account_id>",
      "account_name": "<account_name>",
      "campaign_group_id": "<campaign_group_id>",
      "campaign_group_name": "<campaign_group_name>",
      "campaign_name": "<campaign_name>"
    },
    {
      "..." : "x49"
    }
  ],
  "limit": 50,
  "offset": 0,
  "paging": {
    "next": "https://graph.facebook.com/v2.2/act_<account_id>/reportstats?data_columns=%5B%22time_start%22%2C%22time_stop%22%2C%22spend%22%2C%22impressions%22%2C%22clicks%22%2C%22unique_clicks%22%2C%22social_clicks%22%2C%22unique_social_clicks%22%2C%22cpm%22%2C%22unique_ctr%22%2C%22reach%22%2C%22frequency%22%2C%22cost_per_unique_click%22%2C%22cost_per_action_type%22%2C%22cost_per_total_action%22%2C%22cpp%22%2C%22cpc%22%2C%22ctr%22%2C%22account_id%22%2C%22account_name%22%2C%22campaign_group_id%22%2C%22campaign_group_name%22%2C%22campaign_id%22%2C%22campaign_name%22%5D&date_preset=last_90_days&time_increment=1&access_token=<access_token>&appsecret_proof=<appsecret_proof>&offset=50"
  }
}
Тем временем я прибегаю к использованию следующей "обертки". Лучшие альтернативы?
<?php namespace PayPerClickMarketFacebookData;

use FacebookAdsCursor;

/**
 * Class MyReportCursor
 *
 * @package PayPerClickMarketFacebookData
 */
class MyReportCursor implements Iterator, Countable, ArrayAccess {

    /**
     * @type int
     */
    protected $position = 0;

    /**
     * @type Cursor[]
     */
    protected $cursors = [];

    /**
     * @param Cursor $cursor
     */
    public function __construct(Cursor $cursor) {
        $cursor->setUseImplicitFetch(false);
        $this->cursors[] = $cursor;
    }

    /**
     * @return Cursor
     */
    public function getCursor() {
        return $this->cursors[ $this->position ];
    }

    public function current() {
        return $this->getCursor()->current()->getData();
    }

    public function next() {
        $this->getCursor()->next();
        if ($this->getCursor()->key() === null) {
            $this->advanceCursors();
        }
    }

    protected function advanceCursors() {
        if ($this->hasCursor($this->position+1)) {
            $this->getCursor()->rewind();
            $this->position++;
        } else if ($this->hasNextPage()) {
            $this->fetchNext();
        }
    }

    /**
     * @return bool
     */
    protected function hasNextPage() {
        return $this->getNextPage() !== null;
    }

    /**
     * @return string|null
     */
    protected function getNextPage() {
        $content = $this->getCursor()->getLastResponse()->getContent();

        return isset($content['paging']['next']) ? $content['paging']['next'] : null;
    }

    /**
     * @param int $offset
     * @return bool
     */
    protected function hasCursor($offset) {
        return isset($this->cursors[ $offset ]);
    }

    protected function fetchNext() {
        parse_str(parse_url($this->getNextPage(), PHP_URL_QUERY), $previousParams);

        $objectPrototype = clone $this->getCursor()->offsetGet($this->getCursor()->getIndexRight());
        $request         = $this->getCursor()->getLastResponse()->getRequest()->createClone();

        $request->getQueryParams()->offsetSet('offset', $previousParams['offset']);

        $this->getCursor()->rewind();

        $this->position++;

        $this->cursors[ $this->position ] = new Cursor($request->execute(), $objectPrototype);
    }

    public function key() {
        return $this->getCursor()->key();
    }

    public function valid() {
        return $this->getCursor()->valid();
    }

    public function rewind() {
        $this->position = 0;
    }

    public function offsetExists($offset) {
        return $this->getCursor()->offsetExists($offset);
    }

    public function offsetGet($offset) {
        return $this->getCursor()->offsetGet($offset);
    }

    public function offsetSet($offset, $value) {
        $this->getCursor()->offsetSet($offset, $value);
    }

    public function offsetUnset($offset) {
        $this->getCursor()->offsetUnset($offset);
    }

    public function count() {
        return array_reduce($this->cursors, function ($a, $cursor) {
            return $a + $cursor->count();
        }, 0);
    }
}
3 2

3 ответа:

То, что вы видите, - это ответ, когда это "временная пагинация" https://developers.facebook.com/docs/graph-api/using-graph-api/v2.2#paging

Я отправил ошибку на их github.

Скорее всего, это ошибка в facebook-php-ads-sdk. Даже примеры API Facebook показывают это как:

use FacebookAds\Object\AdAccount;

$account = new AdAccount('act_<AD_ACCOUNT_ID>');

$params = array(
    'date_preset'=>'last_28_days',
    'data_columns'=>"['adgroup_id','actions','spend']",
);

$stats = $account->getReportsStats(null, $params);

foreach($stats as $stat) {
    echo $stat->impressions;
    echo $stat->actions;
}

К сожалению - это, действительно, временная подкачка, поэтому данные курсора не будут возвращены из него. Он уже заполнен как выпуск на GitHub - https://github.com/facebook/facebook-php-ads-sdk/issues/76

EDIT: О, это вы заполнили этот баг :)