Pull to refresh

PHP и Яндекс Директ: наш опыт использования

Reading time6 min
Views17K
18 июня Яндекс анонсировал публичный доступ к API Яндекс Директ. Мы, как рекламное агентство, получили этот доступ немного раньше и сейчас используем его для управления ставками в рекламных кампаниях. Хотелось бы поделиться нашим опытом.



Первоначальные наши впечатления от API были достаточно противоречивые, все таки заметно, что это первая версия. Местами присутствуют несуразности в именовании элементов API, зачастую избыточна, на наш взгляд, система типов параметров методов.

Нашей целью было предоставить разработчикам проектов относительно простой механизм описания правил автоматического формирования ставок, при этом разработчик должен легко описывать тривиальные ситуации и иметь возможность реализовать произвольный алгоритм, если это потребуется отделам рекламы или продвижения интернет-проектов.

Использование чистого API в этой связи мы сочли малоперспективным :) и решили написать над ним более удобный враппер.

Мы сразу же столкнулись с одним неприятным моментом. API использует протокол SOAP, на сервере используется SOAP::Lite. SOAP::Lite имеет некие специфические особенности при формировании представления массивов объектов, возвращаемых SOAP-методами, в результате которых на клиенте при использовании стандартного PHP-модуля SOAP Client cтановится невозможным использовать опцию classmap.

Эта опция позволяет связать типы данных SOAP с классами объектов, представляющими эти типы, и без нее, вообще-то, плохо. С другими реализациями SOAP-сервера, например на Java, такой проблемы не возникает, проверено на сервисах Google и разных других. Честно говоря, наших знаний о SOAP недостаточно, чтобы предложить вменяемый выход из этой ситуации, проблема скорее всего на стороне PHP, а не SOAP::Lite. Яндекс в свою очередь рекомендует использовать NuSOAP.

В общем, протокол вроде бы и стандартный, а щастья нет :(

Кстати говоря, для нормальной работы, скажем, с Google Adwords API, запрос, формируемый стандартным PHP Soap Client, приходится нещадно патчить, примеры можно посмотреть в исходниках PHP-клиента для этого сервиса.

Использовать NuSOAP мы убоялись и решили обойтись без classmap, работаем с простыми stdClass-объектами, которые возвращает API, добавляя по необходимости к ним поведение при помощи объектов-врапперов. Кроме того, врапперы выполняют трансляцию имен свойств из нашего стандарта (с подчеркиваниями) в CamelCase — хорошо, когда все единообразно.

Подавляющее большинство методов API возвращает списки объектов. В некоторых случаях удобно не просто использовать их как контейнеры, но и привязать к ним какое-то поведение, поэтому мы сделали базовый класс коллекций Service.Yandex.Direct.Collection, который:
  1. хранит возвращенные данные в виде обычных структур вида stdclass
  2. при обращении по индексу создает враппер над соответствующим элементов коллекции, таким образом, данные хранятся внутри, а врапперы создаются по необходимости
  3. позволяет выбирать отдельные элементы не только по порядковому индексу, но и по значению идентификатора хранимой сущности;
  4. позволяет порождать дочерние коллекции, содержащие только элементы, удовлетворяющие определенному условию;
  5. позволяет вызывать методы API, оперирующие данными коллекции (если таковые поддерживаются).

Собственно доступ к API разместили в классе Service.Yandex.Direct.APIMapper, который соединяется с сервисом, диспетчеризует SOAP вызовы с использованием __call, и старается быть вежливым, делая при необходимости паузы между запросами (по крайней мере на время тестирования присутствовали ограничения на частоту вызовов).

Таким образом, разработчик может вызывать методы API в стандартной нотации нашей библиотеки, с логированием вызовов, выдерживанием временных промежутков между ними и т.д.

Основываясь на этом, добавляем поведение в сущности и коллекции. Использование собственных коллекций нам очень пригодилось, поскольку позволяет, например, писать так:

Service_Yandex_Direct::api()->
  campaigns_for($login)->
    by_id($campaign_id)->
      all_banners()->
         by_id($banner_id)->
           all_phrases();


В настоящее время для большинства выборок в API отсутствует возможность применять правила фильтрации (некоторые методы позволяют, но не все). Поскольку выборка по различным условиями нам нужна, пришлось реализовывать ее на клиенте опять же в базовом классе коллекции.

Выглядит это так:

$phrases->
  select()->
    where(array('phrase ~' => array('{^рекламная фотосъемка}', '{^фотосъемка рекламы}')));


В данном случае из набора фраз, который был возвращен веб-сервисом, мы отбираем фразы, начинающиеся на «рекламная фотосъемка» и «фотосъемка рекламы».

Where-условие представляет собой массив, ключи которого содержат имя поля, на которое накладывается условие, и операцию, которая используется для сопоставления, а значения — образец или список образцов, с которыми производится сопоставление. Результат выполнения — коллекция того же класса, что и исходная, но содержащая отфильтрованные элементы.

Пока нам хватает таких операций:
  • ~ — сопоставление с регулярным выражением;
  • = — проверка на равенство;
  • in — проверка на вхождение в множество значений.
  • !in — проверка на отсутствие в множестве значений.

Ну и методы поведения у коллекций и сущностей тоже помогают:

Service_Yandex_Direct::api()->
  campaigns_for($client)->
    all_banner()->
      where(array('id in', $ids))->
        stop();


Теперь о том, как мы приспособили все это для разработчиков проектов.

Для каждого проекта создается сценарий управления рекламной кампанией. Он представляет собой PHP-скрипт, например следующего вида:

<?php
$this->
  stay_special(5.0,
    $campaigns->by_id(2059530)->
      all_banners()->
        where(array('BannerID in' => array(6989002, 6989179, 6989186, 6989202, 6989206, 6989209, 6989212))), 0.01)->
  try_special(2.0,
    $campaigns->by_id(2059530)->
      all_banners()->
        where(array('BannerID !in' => array(6989002, 6989179, 6989186, 6989202, 6989206, 6989209, 6989212))), 0.01);
?>


Сценарии запускаются приложением командной строки, реализованным в виде модуля Service.Yandex.Direct.Manager, приложение, в свою очередь, запускается кроном.

Переменная $campaigns автоматически создается классом задачи и содержит список кампаний для текущего клиента, который определяется по имени файла сценария.

В каждом вызове мы определяем список объявление, для которых нужно изменить цены. Это делается путем последовательной выборки «кампании → объявления → фразы».

В первом случае:
  • $campaigns — все кампании клиента;
  • $campaigns->by_id(2059530) — кампания с id = 2059530;
  • $campaigns->by_id(2059530)->all_banners() — все объявления кампании с id = 2059530;
  • $campaigns->by_id(2059530)->all_banners()->where(array('BannerID in' => array(...))) — все объявления кампании с id = 2059530, с идентификаторами, указанными в списке

Таким образом, на словах приведенное выше выражение можно описать так: все объявления компании 2059530 клиента studylab, попадающие в указанный список id.

На самом деле, нас интересуют цены, а цены привязаны к фразам. Однако приложение умеет само достраивать недостающие звенья цепочки. Полное выражение в нашем случае выглядело бы так:

$campaigns->
  by_id(2059530)->
    all_banners()->
      where(array('BannerId in' => array(...)))->
        all_phrases();


В принципе, ничего не мешает писать в файле сценария код, выполняющий те или иные действия над ценами и вызывающий затем стандартный метод update_prices(). Например, можно было бы написать так:

$phrases = $campaigns->by_id(2059530)->all_banners()->all_phrases();
$prices =   $phrases->prices;

foreach ($phrases as $phrase)
  $prices->by_id($phrase->id)->price =
    $phrase->premium_min < $limit ?
      (($phrase->premium_min + $delta < $phrase->premium_max) ?  
        $phrase->premium_min + $delta : $phrase->premium_min + 0.01) : $limit;

$this->api->update_prices($prices);


Приведенный выше код проходит по всем фразам, проверяет, обеспечивает ли текущая ставка показ в спецразмещениях, и соответствующим образом корректирует ее.

Если проект предъявляет какие-то особые требования к управлению кампанией, так и нужно делать, однако в большинстве проектов задачи по управлению ценами как правило сходные и очень простые, поэтому можно выделить эти стандартные действия в готовые методы, реализующие ту или иную стратегию изменения ставок.

Нам сейчас хватает трех стратегий, но наверное понадобятся еще:
  • stay_special — удерживать объявление в спецразмещениях (но не обязательно на первом месте)
  • stay_visible — удерживать объявление в показах;
  • try_special — удерживать объявление в показах, стараясь по возможности попадать в спецразмещения.
  • Каждая из этих стратегий параметризируется максимальным значением ставки и дельтой, на которую ее можно увеличивать. При этом по умолчанию дельта равна 0.01.

Таким образом, вместо приведенного выше кода достаточно написать (например, для всех фраз данного клиента)

$this->stay_special($campaigns->by_id(2059530));


Программист тестирует сценарий, после чего сценарий добавляется в приложение, управляющее кампаниями. Пока нам хватает одного процесса, но вообще можно запускать несколько экземпляров менеджера кампаний с отдельными очередями для разных клиентов.

Что мы получили в итоге? Написание объектного враппера над SOAP API позволило нам минимизировать количество кода для решения рутинных задач и в то же время сделать этот код достаточно читаемым для того, чтобы при необходимости даже непрограммист мог понять, по каким правилам формируются ставки. При этом мы оставляем для программиста возможность реализации произвольного алгоритма манипулирования ставками.

Код модуля доступен на github, используется наш внутренний PHP-фреймворк.
Tags:
Hubs:
+21
Comments16

Articles

Information

Website
www.techart.ru
Registered
Founded
Employees
101–200 employees
Location
Россия