<?php
/**
*
* @author fethi.kus@tribal-im.nl
* @since 5-aug-2013 14:38:52
*
* @name AdwordsApiAds
* UTF-8
*
*
*/
class AdwordsApiAds {
private $campaigns = array();
private $default_max_cpc;
private $operationresultperpage;
private $logbook;
private $operationoptions;
private $operations = array("local" => array(), "remote" => array());
private $idata = array();
private $service; // instanceof AdGroupCriterionService;
private $localids = array();
public function __construct(array $input) {
foreach ($input as $k => $v) {
$this->{$k} = $v;
}
}
private function getRemoteIds() {
$remoteIds = array();
$selector = new Selector();
$selector->predicates[] = new Predicate('CampaignId', 'IN', $this->campaigns);
//if there are adGroupIds sent, then filter to it.
//if(count($adGroupIds) > 0){
//$selector->predicates[] = new Predicate('AdGroupId', 'IN', $adGroupIds);
//}
$selector->fields = array('Headline', 'Id', 'CreativeFinalUrls');
//$selector->ordering[] = new OrderBy('Headline', 'ASCENDING');
// By default disabled ads aren't returned by the selector.
$selector->predicates[] = new Predicate('Status', 'IN', array('ENABLED', 'PAUSED')); //, 'DISABLED'
//3c use paging to handle big huge quantities
$selector->paging = new Paging(0, $this->operationresultperpage);
$pages = array();
// AD-267 replace destination url by final url(s)
$urlsToBeUpgraded = array();
//3d get data
$c = 0;
do {
$page = $this->service->get($selector);
if (count($page->entries) == 0) {
break;
}
$c += count($page->entries);
$this->logbook->addMessage("{$c}/{$page->totalNumEntries} remote ads fetched");
foreach ($page->entries as $remoteAd) {
$i = "|" . $remoteAd->adGroupId
. "|" . $remoteAd->ad->headline
. "|" . $remoteAd->ad->description1
. "|" . $remoteAd->ad->description2
. "|" . $remoteAd->ad->displayUrl;
//Note, duplicate ad texts (even the url is different) experienced as false by the user.
//. "|" . $AdGroupAdPage->ad->url;
//Ad texts will use always unique ID,
//for instance when the user changes an AD in adwords interface,
//then there will be an unique ID. Even when there are exect same ad teksts.
$id = $remoteAd->ad->id . $i;
$ad = array();
$ad["campaign_id"] = $this->campaigns[0];
$ad['ad_group_id'] = $remoteAd->adGroupId;
$ad['ad_group'] = null; //will be sent from local
$ad['headline'] = $remoteAd->ad->headline;
$ad['description1'] = $remoteAd->ad->description1;
$ad['description2'] = $remoteAd->ad->description2;
$ad['display_url'] = $remoteAd->ad->displayUrl;
$ad['destination_url'] = $remoteAd->ad->url;
$ad["newstatus"] = null; //will be sent from local
$ad["remotestatus"] = $remoteAd->status;
$ad["manual"] = null;
$ad["operator"] = "SET";
$ad['ad_id'] = $remoteAd->ad->id;
// AD-267 upgrade destination url to final url if exists
if (!empty($ad['destination_url'])) {
// Proceed with URL upgrade.
$upgradeUrl = new AdUrlUpgrade();
$upgradeUrl->adId = $remoteAd->ad->id;
$upgradeUrl->finalUrl = $remoteAd->ad->url;
$urlsToBeUpgraded[] = $upgradeUrl;
} else {
// assume final url is there already
$ad['destination_url'] = implode('|', $remoteAd->ad->finalUrls);
}
if ((count($urlsToBeUpgraded) % \AdWordsConstants::RECOMMENDED_PAGE_SIZE) == 0) {
$this->upgradeUrls($urlsToBeUpgraded);
$urlsToBeUpgraded = [];
}
//4. build an array with remote data.
$remoteIds[$remoteAd->ad->id] = $ad;
}
// Advance the paging index.
$selector->paging->startIndex += $this->operationresultperpage;
} while ($page->totalNumEntries > $selector->paging->startIndex);
if (count($urlsToBeUpgraded) > 0) {
$this->logbook->addMessage(count($urlsToBeUpgraded) . ' destination urls to be upgraded to final urls');
$this->upgradeUrls($urlsToBeUpgraded);
}
$this->logbook->addMessage(count($remoteIds) . " Fetched remote ADs");
return $remoteIds;
}
/**
* Fix when the following is happening:
*
* [RateExceededError
* rateKey=all_request,
* rateScope=ACCOUNT,
* retryAfterSeconds=30>]
*
* @param array $upgrades
*/
protected function upgradeUrls(array $upgrades)
{
if (count($upgrades) < 1) {
return;
}
$this->logbook->addMessage(count($upgrades) . ' destination urls to be upgraded to final urls');
try {
$this->service->upgradeUrl($upgrades);
} catch (\SoapFault $e) {
if (strpos($e->getMessage(), 'RateExceededError') !== false) {
$this->logbook->addMessage('rate exceeded... sleeping for 30 seconds');
sleep(30);
$this->service->upgradeUrl($upgrades);
}
}
}
private function buildUpdates() {
$items = $this->getitems();
$remoteIds = $this->getRemoteIds();
$remoteupdate = array();
$skipremote = array();
$skiplocal = array();
foreach ($remoteIds as $rid => $v) {
//do not override local via remote
$i = "|" . $v['ad_group_id'] . "|" . $v['headline'] . '|' . $v['description1'] . '|' . $v['description2'] . '|' . $v['display_url'] . '|' . $v['destination_url']; //;
$id = $v['ad_id'] . $i;
//with id and without id not found in local db, not belongs to deleted adgroup (the status were set to null) then manual.
// @TODO 201406
//if (isset($macro_ads[$i]) && ($macro_ads[$i]['adgroupremote'] === "DELETED" || $macro_ads[$i]['adgroupremote'] === "REMOVED")) {
// continue;
//}
//skip deleted adgroups, that already recorded in database.
if($this->operationoptions['skipdeleted']['adgroup']
&& isset($this->operationoptions['skipdeleted']['adgroupdata'][$v['ad_group_id']])
//&& $v['remotestatus'] === "DISABLED"
){
continue;
}
//initial import collects data for local
if (empty($this->localids)) {
$this->operations["local"][$rid] = $v;
}
//the remote item with id and without id not found in local db,
//then update local db and skip remote (import the none existing to local).
if (!isset($items[$id]) && !isset($items[$i])) {
//TODO jira 631 issue, Deviation on firsttime sync must be an option.
//$v["manual"] = 1;
#$operations["local"][$rid] = $v;
//initial import collects data for local
//if($countitems === 0){
// $operations["local"][$rid] = $v;
//}
//local ID match, the status different.
if (isset($this->localids[$rid]) && ($this->localids[$rid]["remotestatus"] !== $v["remotestatus"])
) {
$v["manual"] = 1;
}
//skipdeleted
if ($this->operationoptions["skipdeleted"]["adgroup"]) {
// @TODO 201406
if (($v["remotestatus"] !== "DELETED" || $v["remotestatus"] !== "REMOVED") && !isset($this->operationoptions["skipdeleted"]["adgroupdata"][$v['ad_group_id']])) {
$this->operations["local"][$rid] = $v;
}
} else {
//for instance: when an ad changed, then it will create new id, that must be imported.
$this->operations["local"][$rid] = $v;
}
//skip all unkown.
//Notice, when local data skips deleted, all [remotestatus] => DISABLED are skipped too.
$skipremote[$rid] = $v;
continue;
}
//full id is known, update local db
if (isset($items[$id])) {
$v["manual"] = $items[$id]["manual"];
$v["newstatus"] = $items[$id]["newstatus"];
//remotestatus is not equal to local remotestatus = remote change detected (manual).
//Notice, detecting ad text change is not needed. every change will have new id.
//var_dump($items[$id]);
if ($items[$id]["remotestatus"] != "" && $v["remotestatus"] != $items[$id]["remotestatus"]) {
$v["manual"] = 1;
$this->operations["local"][$id] = $v;
$skipremote[$rid] = $v;
continue;
}
//no need to update if the id is known and status is equal
//An AD will have unique ID.
if ($v["remotestatus"] == $items[$id]["newstatus"]) {
$skipremote[$rid] = $v;
continue;
}
//if still not skipped, then add to local as it is
$this->operations["local"][$id] = $v;
}
/**/
//full id is unknown, update local db without id (import remote data)
if (isset($items[$i])) {
//Before skipping remote items update local with remote data that did not existed in local
//manual should always depend on id
$v["manual"] = $items[$i]["manual"];
$v["newstatus"] = $items[$i]["newstatus"];
//!important in Ad text, do not add matching ads.
$skipremote[$i] = $v;
//the remote is not yet in local insert array, then add based on $i into local.
if(!isset($this->operations["local"][$v["ad_id"]])) {
$this->operations["local"][$i] = $v;
}
//local data is not aware of remote,
// remote has the adgroup already with the same name as the local wants to add.
//prevents DUPLICATE
if (!isset($skipremote[$rid]) && $items[$i]["operator"] == "ADD" && $items[$i]["ad_id"] == "" && $items[$i]["remotestatus"] == ""
) {
//already in local update.
if (!isset($this->operations["local"][$i])) {
$skipremote[$rid] = $v;
$this->operations["local"][$rid] = $v;
}
continue;
}
} else {
//with id and not id not match, import data into local.
$this->operations["local"][$rid] = $v;
}
//get the change data from local
if (isset($items[$i])) {
$v = $items[$i];
}
if (isset($items[$id])) {
$v = $items[$id];
}
//AdGroupAdError.CANNOT_OPERATE_ON_DELETED_ADGROUPAD
// @TODO 201406
if ($v['newstatus'] === null && $v["manual"] === 1 || (isset($v['adgroupremote']) && ($v['adgroupremote'] == "DELETED" || $v['adgroupremote'] == "REMOVED"))) {
$skipremote[$rid] = $v;
continue;
}
$remoteupdate[$rid] = $v;
}//remote data
$this->logbook->addMessage(count($skipremote) . " Update remote skip, " . count($remoteupdate) . " Update remote ADs");
//local data, might need to skip.
//$remotefound = array();
foreach ($remoteupdate as $rid => $v) {
//known with id is (should) leaded to local update and not taken to remote update.
if (isset($skipremote[$rid])) {
continue;
}
//TODO skipremote those getting id from remote
//if(isset($v['ad_group_id']) && isset($v["keyword"]) && isset($v['match_type'])){
// $remotefound["|" . $v['ad_group_id'] . "|" . $v["keyword"] . "|" . strtoupper($v['match_type'])] = $v;
//}
$this->operations["remote"][$rid] = $v;
}
//print_r($ops);
//print_r($operations["local"]);
//add remaining items.
foreach ($items as $i => $v) {
//FIXME check intial must be checked together with local update data. E.g. if ($initialexport === true && $v["ad_group_id"] === NULL && isset($operations["local"][$i]))
//if ($initialexport === true && $v["ad_id"] === NULL) {
// continue;
//}
//skip, if there is already an id, it would be double.
if (isset($this->operations["remote"][$v["ad_id"] . $i])) {
continue;
}
//if (isset($remotefound[$i])) {
// continue;
//}
//when an existing ad is changed might have id and must be paused.
if (isset($skipremote[$v['ad_id']])) {
continue;
}
if (isset($this->operations["remote"][$v['ad_id']])) {
continue;
}
if (isset($skipremote[$i])) {
continue;
}
$this->operations["remote"][$i] = $v;
}
//print_r($ops);
$this->logbook->addMessage(count($skiplocal) . " Skip local ADs, " . count($this->operations["local"]) . " Update local ADs");
$this->logbook->addMessage(count($this->operations["remote"]) . " AD operations");
//print_r($ops);
return $this->operations;
}
private function getitems() {
$items = array();
$removed = 0;
foreach ($this->idata as $k => $v) {
// @TODO 201406
if ($v["ad_group_id"] === "0" || ($v["adgroupremote"] === "DELETED" || $v["adgroupremote"] === "REMOVED")) {
$removed++;
continue;
} else {
unset($v["ad_group"], $v["adgroupremote"]);
$items[$k] = $v;
#$adGroupIds[] = $v["ad_group_id"];
if (isset($v["ad_id"]) && $v["ad_id"] != "") {
$this->localids[$v["ad_id"]] = $v;
}
}
}
$this->logbook->addMessage(count($this->operationoptions['skipdeleted']["adgroupdata"])." local DELETED ADGROUP items, ".
count($items) . " local AD items after " .
$removed . " DELETED or ad_group_id=0 AdGroups were removed of the input qty " .
count($this->idata) . "");
return $items;
}
private function buildOperations(array $ops, $default_max_cpc) {
$operations = array();
$operations["local"] = isset($ops["local"]) ? $ops["local"] : array();
if (isset($ops["remote"]) && count($ops["remote"]) > 0)
foreach ($ops["remote"] as $macro_ad) {
if ($macro_ad['operator'] == 'REMOVE') {
$Ad = new Ad();
$Ad->id = $macro_ad['ad_id'];
$adGroupAd = new AdGroupAd();
$adGroupAd->adGroupId = $macro_ad['ad_group_id'];
$adGroupAd->status = $macro_ad['newstatus'];
$adGroupAd->ad = $Ad;
$operation = new AdGroupAdOperation();
$operation->operator = 'REMOVE';
$operation->operand = $adGroupAd;
} else {
/**
* @desc Build Ad operation for google adwords.
* 1. TextAd object
* 2. AdGroupAd object
* 3. AdGroupAdOperation object
*/
$textAd = new TextAd();
$textAd->id = null;
$macro_ad['operator'] = "ADD";
if ($macro_ad['ad_id'] != "" && $macro_ad['ad_id'] != "0") {
$macro_ad['operator'] = "SET";
$textAd->id = $macro_ad['ad_id'];
}
$textAd->headline = $macro_ad['headline'];
$textAd->description1 = $macro_ad['description1'];
$textAd->description2 = $macro_ad['description2'];
$textAd->displayUrl = $macro_ad['display_url'];
// AD 267 --> replace destination_url by final url(s)
$textAd->finalUrls = explode('|', $macro_ad['destination_url']);
$adGroupAd = new AdGroupAd();
$adGroupAd->adGroupId = $macro_ad['ad_group_id'];
//$adGroupAd->status = "ENABLED";
if (isset($macro_ad['remotestatus']) && $macro_ad['remotestatus'] != "" && $macro_ad['remotestatus'] != "0") {
$adGroupAd->status = $macro_ad['remotestatus'];
}
if (isset($macro_ad['newstatus']) && $macro_ad['newstatus'] != "" && $macro_ad['newstatus'] != "0") {
$adGroupAd->status = $macro_ad['newstatus'];
}
$adGroupAd->ad = $textAd;
$operation = new AdGroupAdOperation();
$operation->operator = $macro_ad['operator'];
$operation->operand = $adGroupAd;
if (isset($macro_ad['violatingtext']) && $macro_ad['violatingtext'] != ""
&& ($macro_ad['ad_id'] === null || $macro_ad['ad_id'] == "" || $macro_ad['ad_id'] == "0")
) {
$exkeytxt = explode("|", $macro_ad['violatingtext']);
$exkey = new PolicyViolationKey();
$exkey->policyName = (isset($exkeytxt[0]) && $exkeytxt[0] != "") ? $exkeytxt[0] : "pharma";
$exkey->violatingText = (isset($exkeytxt[1]) && $exkeytxt[1] != "") ? $exkeytxt[1] : null;
$operation->exemptionRequests[] = new ExemptionRequest($exkey);
}
}
$operations["remote"][] = $operation;
}
return $operations;
}
public function output() {
return $this->buildOperations($this->buildUpdates(), $this->default_max_cpc);
}
}