Files
adsPRO/autoload/services/class.GoogleAdsApi.php
Jacek Pyziak 4635cefcbb feat: update font to Roboto across templates and add campaign/ad group filters in product views
- Changed font from Open Sans to Roboto in layout files.
- Added campaign and ad group filters in products main view.
- Enhanced product history to include campaign and ad group IDs.
- Updated migrations to support new campaign and ad group dimensions in product statistics.
- Introduced new migration files for managing campaign types and dropping obsolete columns.
2026-02-18 01:21:22 +01:00

1051 lines
34 KiB
PHP

<?php
namespace services;
class GoogleAdsApi
{
private $developer_token;
private $client_id;
private $client_secret;
private $refresh_token;
private $manager_account_id;
private $access_token;
private static $API_VERSION = 'v23';
private static $TOKEN_URL = 'https://oauth2.googleapis.com/token';
private static $ADS_BASE_URL = 'https://googleads.googleapis.com';
public function __construct()
{
$this -> developer_token = self::get_setting( 'google_ads_developer_token' );
$this -> client_id = self::get_setting( 'google_ads_client_id' );
$this -> client_secret = self::get_setting( 'google_ads_client_secret' );
$this -> refresh_token = self::get_setting( 'google_ads_refresh_token' );
$this -> manager_account_id = self::get_setting( 'google_ads_manager_account_id' );
}
// --- Settings CRUD ---
public static function get_setting( $key )
{
global $mdb;
return $mdb -> get( 'settings', 'setting_value', [ 'setting_key' => $key ] );
}
public static function set_setting( $key, $value )
{
global $mdb;
if ( $mdb -> count( 'settings', [ 'setting_key' => $key ] ) )
{
$mdb -> update( 'settings', [ 'setting_value' => $value ], [ 'setting_key' => $key ] );
}
else
{
$mdb -> insert( 'settings', [ 'setting_key' => $key, 'setting_value' => $value ] );
}
}
// --- Konfiguracja ---
public function is_configured()
{
return !empty( $this -> developer_token )
&& !empty( $this -> client_id )
&& !empty( $this -> client_secret )
&& !empty( $this -> refresh_token );
}
// --- OAuth2 ---
private function get_access_token()
{
$cached_token = self::get_setting( 'google_ads_access_token' );
$cached_expires = (int) self::get_setting( 'google_ads_access_token_expires' );
if ( $cached_token && $cached_expires > time() )
{
$this -> access_token = $cached_token;
return $this -> access_token;
}
return $this -> refresh_access_token();
}
private function refresh_access_token()
{
$ch = curl_init( self::$TOKEN_URL );
curl_setopt_array( $ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query( [
'client_id' => $this -> client_id,
'client_secret' => $this -> client_secret,
'refresh_token' => $this -> refresh_token,
'grant_type' => 'refresh_token'
] ),
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 30,
] );
$response = curl_exec( $ch );
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
$error = curl_error( $ch );
curl_close( $ch );
if ( $http_code !== 200 || !$response )
{
self::set_setting( 'google_ads_last_error', 'Token refresh failed: HTTP ' . $http_code . ' | ' . $error . ' | ' . $response );
return false;
}
$data = json_decode( $response, true );
if ( !isset( $data['access_token'] ) )
{
self::set_setting( 'google_ads_last_error', 'Token refresh: brak access_token w odpowiedzi' );
return false;
}
$this -> access_token = $data['access_token'];
$expires_at = time() + ( $data['expires_in'] ?? 3600 ) - 60;
self::set_setting( 'google_ads_access_token', $this -> access_token );
self::set_setting( 'google_ads_access_token_expires', $expires_at );
self::set_setting( 'google_ads_last_error', null );
return $this -> access_token;
}
// --- Google Ads API ---
public function search_stream( $customer_id, $gaql_query )
{
$access_token = $this -> get_access_token();
if ( !$access_token ) return false;
$customer_id = str_replace( '-', '', $customer_id );
$url = self::$ADS_BASE_URL . '/' . self::$API_VERSION
. '/customers/' . $customer_id . '/googleAds:searchStream';
$headers = [
'Authorization: Bearer ' . $access_token,
'developer-token: ' . $this -> developer_token,
'Content-Type: application/json',
];
if ( !empty( $this -> manager_account_id ) )
{
$headers[] = 'login-customer-id: ' . str_replace( '-', '', $this -> manager_account_id );
}
$ch = curl_init( $url );
curl_setopt_array( $ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => json_encode( [ 'query' => $gaql_query ] ),
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 120,
] );
$response = curl_exec( $ch );
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
$error = curl_error( $ch );
curl_close( $ch );
if ( $http_code !== 200 || !$response )
{
self::set_setting( 'google_ads_last_error', 'searchStream failed: HTTP ' . $http_code . ' | ' . $error . ' | ' . substr( $response, 0, 500 ) );
return false;
}
$data = json_decode( $response, true );
// searchStream zwraca tablicę batch'y, każdy z kluczem 'results'
$results = [];
if ( is_array( $data ) )
{
foreach ( $data as $batch )
{
if ( isset( $batch['results'] ) && is_array( $batch['results'] ) )
{
$results = array_merge( $results, $batch['results'] );
}
}
}
self::set_setting( 'google_ads_last_error', null );
return $results;
}
public function mutate( $customer_id, $mutate_operations, $partial_failure = false )
{
$access_token = $this -> get_access_token();
if ( !$access_token ) return false;
$customer_id = str_replace( '-', '', $customer_id );
$url = self::$ADS_BASE_URL . '/' . self::$API_VERSION
. '/customers/' . $customer_id . '/googleAds:mutate';
$headers = [
'Authorization: Bearer ' . $access_token,
'developer-token: ' . $this -> developer_token,
'Content-Type: application/json',
];
if ( !empty( $this -> manager_account_id ) )
{
$headers[] = 'login-customer-id: ' . str_replace( '-', '', $this -> manager_account_id );
}
$payload = [
'mutateOperations' => array_values( $mutate_operations ),
'partialFailure' => (bool) $partial_failure
];
$ch = curl_init( $url );
curl_setopt_array( $ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => json_encode( $payload ),
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 120,
] );
$response = curl_exec( $ch );
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
$error = curl_error( $ch );
curl_close( $ch );
if ( $http_code !== 200 || !$response )
{
self::set_setting( 'google_ads_last_error', 'mutate failed: HTTP ' . $http_code . ' | ' . $error . ' | ' . substr( (string) $response, 0, 1000 ) );
return false;
}
$data = json_decode( $response, true );
if ( !is_array( $data ) )
{
self::set_setting( 'google_ads_last_error', 'mutate failed: niepoprawna odpowiedz JSON' );
return false;
}
self::set_setting( 'google_ads_last_error', null );
return $data;
}
public function add_negative_keyword_to_ad_group( $customer_id, $ad_group_id, $keyword_text, $match_type = 'PHRASE' )
{
$customer_id = trim( str_replace( '-', '', (string) $customer_id ) );
$ad_group_id = trim( (string) $ad_group_id );
$keyword_text = trim( (string) $keyword_text );
$match_type = strtoupper( trim( (string) $match_type ) );
if ( $customer_id === '' || $ad_group_id === '' || $keyword_text === '' )
{
self::set_setting( 'google_ads_last_error', 'Brak wymaganych danych do dodania frazy wykluczajacej.' );
return [ 'success' => false, 'duplicate' => false ];
}
if ( !in_array( $match_type, [ 'PHRASE', 'EXACT', 'BROAD' ], true ) )
{
$match_type = 'PHRASE';
}
$operation = [
'adGroupCriterionOperation' => [
'create' => [
'adGroup' => 'customers/' . $customer_id . '/adGroups/' . $ad_group_id,
'negative' => true,
'keyword' => [
'text' => $keyword_text,
'matchType' => $match_type
]
]
]
];
$result = $this -> mutate( $customer_id, [ $operation ] );
if ( $result === false )
{
$last_error = (string) self::get_setting( 'google_ads_last_error' );
$is_duplicate = stripos( $last_error, 'DUPLICATE' ) !== false
|| stripos( $last_error, 'already exists' ) !== false;
if ( $is_duplicate )
{
return [
'success' => true,
'duplicate' => true,
'verification' => $this -> verify_negative_keyword_exists( $customer_id, 'ad_group', $keyword_text, $match_type, null, $ad_group_id )
];
}
return [ 'success' => false, 'duplicate' => false, 'sent_operation' => $operation ];
}
return [
'success' => true,
'duplicate' => false,
'response' => $result,
'sent_operation' => $operation,
'verification' => $this -> verify_negative_keyword_exists( $customer_id, 'ad_group', $keyword_text, $match_type, null, $ad_group_id )
];
}
public function add_negative_keyword_to_campaign( $customer_id, $campaign_id, $keyword_text, $match_type = 'PHRASE' )
{
$customer_id = trim( str_replace( '-', '', (string) $customer_id ) );
$campaign_id = trim( (string) $campaign_id );
$keyword_text = trim( (string) $keyword_text );
$match_type = strtoupper( trim( (string) $match_type ) );
if ( $customer_id === '' || $campaign_id === '' || $keyword_text === '' )
{
self::set_setting( 'google_ads_last_error', 'Brak wymaganych danych do dodania frazy wykluczajacej.' );
return [ 'success' => false, 'duplicate' => false ];
}
if ( !in_array( $match_type, [ 'PHRASE', 'EXACT', 'BROAD' ], true ) )
{
$match_type = 'PHRASE';
}
$operation = [
'campaignCriterionOperation' => [
'create' => [
'campaign' => 'customers/' . $customer_id . '/campaigns/' . $campaign_id,
'negative' => true,
'keyword' => [
'text' => $keyword_text,
'matchType' => $match_type
]
]
]
];
$result = $this -> mutate( $customer_id, [ $operation ] );
if ( $result === false )
{
$last_error = (string) self::get_setting( 'google_ads_last_error' );
$is_duplicate = stripos( $last_error, 'DUPLICATE' ) !== false
|| stripos( $last_error, 'already exists' ) !== false;
if ( $is_duplicate )
{
return [
'success' => true,
'duplicate' => true,
'verification' => $this -> verify_negative_keyword_exists( $customer_id, 'campaign', $keyword_text, $match_type, $campaign_id, null )
];
}
return [ 'success' => false, 'duplicate' => false, 'sent_operation' => $operation ];
}
return [
'success' => true,
'duplicate' => false,
'response' => $result,
'sent_operation' => $operation,
'verification' => $this -> verify_negative_keyword_exists( $customer_id, 'campaign', $keyword_text, $match_type, $campaign_id, null )
];
}
private function gaql_escape( $value )
{
return str_replace( [ '\\', '\'' ], [ '\\\\', '\\\'' ], (string) $value );
}
private function verify_negative_keyword_exists( $customer_id, $scope, $keyword_text, $match_type, $campaign_id = null, $ad_group_id = null )
{
$customer_id = trim( str_replace( '-', '', (string) $customer_id ) );
$scope = $scope === 'campaign' ? 'campaign' : 'ad_group';
$match_type = strtoupper( trim( (string) $match_type ) );
$keyword_text_escaped = $this -> gaql_escape( trim( (string) $keyword_text ) );
if ( $scope === 'campaign' )
{
$campaign_id = trim( (string) $campaign_id );
if ( $campaign_id === '' || $keyword_text_escaped === '' )
{
return [ 'found' => false, 'scope' => $scope, 'rows' => [], 'error' => 'Brak danych do weryfikacji.' ];
}
$gaql = "SELECT "
. "campaign_criterion.resource_name, "
. "campaign.id, "
. "campaign_criterion.keyword.text, "
. "campaign_criterion.keyword.match_type "
. "FROM campaign_criterion "
. "WHERE campaign.id = " . $campaign_id . " "
. "AND campaign_criterion.type = 'KEYWORD' "
. "AND campaign_criterion.negative = TRUE "
. "AND campaign_criterion.keyword.text = '" . $keyword_text_escaped . "' "
. "AND campaign_criterion.keyword.match_type = " . $match_type . " "
. "LIMIT 5";
}
else
{
$ad_group_id = trim( (string) $ad_group_id );
if ( $ad_group_id === '' || $keyword_text_escaped === '' )
{
return [ 'found' => false, 'scope' => $scope, 'rows' => [], 'error' => 'Brak danych do weryfikacji.' ];
}
$gaql = "SELECT "
. "ad_group_criterion.resource_name, "
. "campaign.id, "
. "ad_group.id, "
. "ad_group_criterion.keyword.text, "
. "ad_group_criterion.keyword.match_type "
. "FROM ad_group_criterion "
. "WHERE ad_group.id = " . $ad_group_id . " "
. "AND ad_group_criterion.type = 'KEYWORD' "
. "AND ad_group_criterion.negative = TRUE "
. "AND ad_group_criterion.keyword.text = '" . $keyword_text_escaped . "' "
. "AND ad_group_criterion.keyword.match_type = " . $match_type . " "
. "LIMIT 5";
}
$rows = [];
$last_error = null;
for ( $i = 0; $i < 3; $i++ )
{
$result = $this -> search_stream( $customer_id, $gaql );
if ( is_array( $result ) )
{
$rows = $result;
if ( count( $rows ) > 0 )
{
return [ 'found' => true, 'scope' => $scope, 'rows' => $rows ];
}
}
else
{
$last_error = (string) self::get_setting( 'google_ads_last_error' );
}
usleep( 400000 );
}
return [
'found' => count( $rows ) > 0,
'scope' => $scope,
'rows' => $rows,
'error' => $last_error
];
}
// --- Kampanie: dane 30-dniowe ---
public function get_products_for_date( $customer_id, $date )
{
$date = date( 'Y-m-d', strtotime( $date ) );
$gaql_with_ad_group = "SELECT "
. "segments.date, "
. "segments.product_item_id, "
. "segments.product_title, "
. "campaign.id, "
. "campaign.name, "
. "ad_group.id, "
. "ad_group.name, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM shopping_performance_view "
. "WHERE segments.date = '" . $date . "'";
$results = $this -> search_stream( $customer_id, $gaql_with_ad_group );
$fallback_without_ad_group = false;
if ( $results === false )
{
$gaql_without_ad_group = "SELECT "
. "segments.date, "
. "segments.product_item_id, "
. "segments.product_title, "
. "campaign.id, "
. "campaign.name, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM shopping_performance_view "
. "WHERE segments.date = '" . $date . "'";
$results = $this -> search_stream( $customer_id, $gaql_without_ad_group );
if ( $results === false )
{
return false;
}
$fallback_without_ad_group = true;
}
$products = [];
foreach ( $results as $row )
{
$offer_id = trim( (string) ( $row['segments']['productItemId'] ?? '' ) );
if ( $offer_id === '' )
{
continue;
}
$campaign_id = (int) ( $row['campaign']['id'] ?? 0 );
$campaign_name = trim( (string) ( $row['campaign']['name'] ?? '' ) );
if ( $campaign_name === '' && $campaign_id > 0 )
{
$campaign_name = 'Kampania #' . $campaign_id;
}
$ad_group_id = 0;
$ad_group_name = 'PMax (bez grup reklam)';
if ( !$fallback_without_ad_group )
{
$ad_group_id = (int) ( $row['adGroup']['id'] ?? 0 );
$ad_group_name = trim( (string) ( $row['adGroup']['name'] ?? '' ) );
if ( $ad_group_id > 0 && $ad_group_name === '' )
{
$ad_group_name = 'Ad group #' . $ad_group_id;
}
else if ( $ad_group_id <= 0 )
{
$ad_group_name = 'PMax (bez grup reklam)';
}
}
$scope_key = $offer_id . '|' . $campaign_id . '|' . $ad_group_id;
if ( !isset( $products[ $scope_key ] ) )
{
$products[ $scope_key ] = [
'OfferId' => $offer_id,
'ProductTitle' => (string) ( $row['segments']['productTitle'] ?? $offer_id ),
'CampaignId' => $campaign_id,
'CampaignName' => $campaign_name,
'AdGroupId' => $ad_group_id,
'AdGroupName' => $ad_group_name,
'Impressions' => 0,
'Clicks' => 0,
'Cost' => 0.0,
'Conversions' => 0.0,
'ConversionValue' => 0.0
];
}
$products[ $scope_key ]['Impressions'] += (int) ( $row['metrics']['impressions'] ?? 0 );
$products[ $scope_key ]['Clicks'] += (int) ( $row['metrics']['clicks'] ?? 0 );
$products[ $scope_key ]['Cost'] += (float) ( $row['metrics']['costMicros'] ?? 0 ) / 1000000;
$products[ $scope_key ]['Conversions'] += (float) ( $row['metrics']['conversions'] ?? 0 );
$products[ $scope_key ]['ConversionValue'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 );
}
return array_values( $products );
}
public function get_campaigns_30_days( $customer_id )
{
$gaql = "SELECT "
. "campaign.id, "
. "campaign.name, "
. "campaign.advertising_channel_type, "
. "campaign.bidding_strategy_type, "
. "campaign.target_roas.target_roas, "
. "campaign_budget.amount_micros, "
. "metrics.cost_micros, "
. "metrics.conversions_value "
. "FROM campaign "
. "WHERE campaign.status = 'ENABLED' "
. "AND segments.date DURING LAST_30_DAYS";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false ) return false;
// Agregacja po campaign.id (API zwraca wiersz per dzień per kampania)
$campaigns = [];
foreach ( $results as $row )
{
$cid = $row['campaign']['id'] ?? null;
if ( !$cid ) continue;
if ( !isset( $campaigns[ $cid ] ) )
{
$campaigns[ $cid ] = [
'campaign_id' => $cid,
'campaign_name' => $row['campaign']['name'] ?? '',
'advertising_channel_type' => (string) ( $row['campaign']['advertisingChannelType'] ?? '' ),
'bidding_strategy' => $row['campaign']['biddingStrategyType'] ?? 'UNKNOWN',
'target_roas' => isset( $row['campaign']['targetRoas']['targetRoas'] )
? (float) $row['campaign']['targetRoas']['targetRoas']
: 0,
'budget' => isset( $row['campaignBudget']['amountMicros'] )
? (float) $row['campaignBudget']['amountMicros'] / 1000000
: 0,
'cost_total' => 0,
'conversion_value' => 0,
];
}
$campaigns[ $cid ]['cost_total'] += (float) ( $row['metrics']['costMicros'] ?? 0 );
$campaigns[ $cid ]['conversion_value'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 );
}
// Przeliczenie micros i ROAS
foreach ( $campaigns as &$c )
{
$c['money_spent'] = $c['cost_total'] / 1000000;
$c['roas_30_days'] = ( $c['money_spent'] > 0 )
? round( ( $c['conversion_value'] / $c['money_spent'] ) * 100, 2 )
: 0;
unset( $c['cost_total'] );
}
return array_values( $campaigns );
}
// --- Kampanie: dane all-time ---
public function get_campaigns_all_time( $customer_id )
{
$gaql = "SELECT "
. "campaign.id, "
. "metrics.cost_micros, "
. "metrics.conversions_value "
. "FROM campaign "
. "WHERE campaign.status = 'ENABLED'";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false ) return false;
$campaigns = [];
foreach ( $results as $row )
{
$cid = $row['campaign']['id'] ?? null;
if ( !$cid ) continue;
$cost = (float) ( $row['metrics']['costMicros'] ?? 0 ) / 1000000;
$value = (float) ( $row['metrics']['conversionsValue'] ?? 0 );
$campaigns[] = [
'campaign_id' => $cid,
'cost_all_time' => $cost,
'conversion_value_all_time' => $value,
'roas_all_time' => ( $cost > 0 ) ? round( ( $value / $cost ) * 100, 2 ) : 0,
];
}
return $campaigns;
}
public function get_ad_groups_30_days( $customer_id )
{
$gaql = "SELECT "
. "campaign.id, "
. "ad_group.id, "
. "ad_group.name, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM ad_group "
. "WHERE campaign.status = 'ENABLED' "
. "AND ad_group.status = 'ENABLED' "
. "AND segments.date DURING LAST_30_DAYS";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false ) return false;
return $this -> aggregate_ad_groups( $results );
}
public function get_ad_groups_all_time( $customer_id )
{
$gaql = "SELECT "
. "campaign.id, "
. "ad_group.id, "
. "ad_group.name, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM ad_group "
. "WHERE campaign.status = 'ENABLED' "
. "AND ad_group.status = 'ENABLED'";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false ) return false;
return $this -> aggregate_ad_groups( $results );
}
public function get_search_terms_30_days( $customer_id )
{
$gaql = "SELECT "
. "campaign.id, "
. "ad_group.id, "
. "ad_group.name, "
. "search_term_view.search_term, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM search_term_view "
. "WHERE campaign.status != 'REMOVED' "
. "AND ad_group.status != 'REMOVED' "
. "AND metrics.clicks > 0 "
. "AND segments.date DURING LAST_30_DAYS";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false ) return false;
$terms = $this -> aggregate_search_terms( $results );
$pmax_terms = $this -> get_pmax_search_terms_30_days( $customer_id );
if ( $pmax_terms !== false && is_array( $pmax_terms ) && !empty( $pmax_terms ) )
{
$terms = array_merge( $terms, $pmax_terms );
}
return $terms;
}
public function get_search_terms_all_time( $customer_id )
{
$gaql = "SELECT "
. "campaign.id, "
. "ad_group.id, "
. "ad_group.name, "
. "search_term_view.search_term, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM search_term_view "
. "WHERE campaign.status != 'REMOVED' "
. "AND ad_group.status != 'REMOVED' "
. "AND metrics.clicks > 0";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false ) return false;
$terms = $this -> aggregate_search_terms( $results );
$pmax_terms = $this -> get_pmax_search_terms_all_time( $customer_id );
if ( $pmax_terms !== false && is_array( $pmax_terms ) && !empty( $pmax_terms ) )
{
$terms = array_merge( $terms, $pmax_terms );
}
return $terms;
}
private function get_pmax_search_terms_30_days( $customer_id )
{
$gaql = "SELECT "
. "campaign.id, "
. "campaign_search_term_view.search_term, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM campaign_search_term_view "
. "WHERE campaign.status != 'REMOVED' "
. "AND campaign.advertising_channel_type = 'PERFORMANCE_MAX' "
. "AND metrics.clicks > 0 "
. "AND segments.date DURING LAST_30_DAYS";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false ) return false;
return $this -> aggregate_campaign_search_terms( $results );
}
private function get_pmax_search_terms_all_time( $customer_id )
{
$gaql = "SELECT "
. "campaign.id, "
. "campaign_search_term_view.search_term, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM campaign_search_term_view "
. "WHERE campaign.status != 'REMOVED' "
. "AND campaign.advertising_channel_type = 'PERFORMANCE_MAX' "
. "AND metrics.clicks > 0";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false ) return false;
return $this -> aggregate_campaign_search_terms( $results );
}
public function get_negative_keywords( $customer_id )
{
$campaign_gaql = "SELECT "
. "campaign.id, "
. "campaign_criterion.keyword.text, "
. "campaign_criterion.keyword.match_type "
. "FROM campaign_criterion "
. "WHERE campaign.status != 'REMOVED' "
. "AND campaign_criterion.type = 'KEYWORD' "
. "AND campaign_criterion.negative = TRUE";
$ad_group_gaql = "SELECT "
. "campaign.id, "
. "ad_group.id, "
. "ad_group_criterion.keyword.text, "
. "ad_group_criterion.keyword.match_type "
. "FROM ad_group_criterion "
. "WHERE campaign.status != 'REMOVED' "
. "AND ad_group.status != 'REMOVED' "
. "AND ad_group_criterion.type = 'KEYWORD' "
. "AND ad_group_criterion.negative = TRUE";
$campaign_results = $this -> search_stream( $customer_id, $campaign_gaql );
if ( $campaign_results === false ) return false;
$ad_group_results = $this -> search_stream( $customer_id, $ad_group_gaql );
if ( $ad_group_results === false ) return false;
$negatives = [];
$seen = [];
foreach ( $campaign_results as $row )
{
$campaign_id = $row['campaign']['id'] ?? null;
$text = trim( (string) ( $row['campaignCriterion']['keyword']['text'] ?? '' ) );
$match_type = (string) ( $row['campaignCriterion']['keyword']['matchType'] ?? '' );
if ( !$campaign_id || $text === '' )
{
continue;
}
$key = 'campaign|' . $campaign_id . '||' . strtolower( $text ) . '|' . $match_type;
if ( isset( $seen[ $key ] ) )
{
continue;
}
$seen[ $key ] = true;
$negatives[] = [
'scope' => 'campaign',
'campaign_id' => (int) $campaign_id,
'ad_group_id' => null,
'keyword_text' => $text,
'match_type' => $match_type
];
}
foreach ( $ad_group_results as $row )
{
$campaign_id = $row['campaign']['id'] ?? null;
$ad_group_id = $row['adGroup']['id'] ?? null;
$text = trim( (string) ( $row['adGroupCriterion']['keyword']['text'] ?? '' ) );
$match_type = (string) ( $row['adGroupCriterion']['keyword']['matchType'] ?? '' );
if ( !$campaign_id || !$ad_group_id || $text === '' )
{
continue;
}
$key = 'ad_group|' . $campaign_id . '|' . $ad_group_id . '|' . strtolower( $text ) . '|' . $match_type;
if ( isset( $seen[ $key ] ) )
{
continue;
}
$seen[ $key ] = true;
$negatives[] = [
'scope' => 'ad_group',
'campaign_id' => (int) $campaign_id,
'ad_group_id' => (int) $ad_group_id,
'keyword_text' => $text,
'match_type' => $match_type
];
}
return $negatives;
}
private function aggregate_ad_groups( $results )
{
$ad_groups = [];
foreach ( $results as $row )
{
$campaign_id = $row['campaign']['id'] ?? null;
$ad_group_id = $row['adGroup']['id'] ?? null;
if ( !$campaign_id || !$ad_group_id )
{
continue;
}
$key = $campaign_id . '|' . $ad_group_id;
if ( !isset( $ad_groups[ $key ] ) )
{
$ad_groups[ $key ] = [
'campaign_id' => (int) $campaign_id,
'ad_group_id' => (int) $ad_group_id,
'ad_group_name' => (string) ( $row['adGroup']['name'] ?? '' ),
'impressions' => 0,
'clicks' => 0,
'cost' => 0.0,
'conversions' => 0.0,
'conversion_value' => 0.0,
'roas' => 0.0
];
}
$ad_groups[ $key ]['impressions'] += (int) ( $row['metrics']['impressions'] ?? 0 );
$ad_groups[ $key ]['clicks'] += (int) ( $row['metrics']['clicks'] ?? 0 );
$ad_groups[ $key ]['cost'] += (float) ( $row['metrics']['costMicros'] ?? 0 ) / 1000000;
$ad_groups[ $key ]['conversions'] += (float) ( $row['metrics']['conversions'] ?? 0 );
$ad_groups[ $key ]['conversion_value'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 );
}
foreach ( $ad_groups as &$ad_group )
{
$ad_group['roas'] = ( $ad_group['cost'] > 0 )
? round( ( $ad_group['conversion_value'] / $ad_group['cost'] ) * 100, 2 )
: 0;
}
return array_values( $ad_groups );
}
private function aggregate_search_terms( $results )
{
$terms = [];
foreach ( $results as $row )
{
$campaign_id = $row['campaign']['id'] ?? null;
$ad_group_id = $row['adGroup']['id'] ?? null;
$search_term = trim( (string) ( $row['searchTermView']['searchTerm'] ?? '' ) );
if ( !$campaign_id || !$ad_group_id || $search_term === '' )
{
continue;
}
$key = $campaign_id . '|' . $ad_group_id . '|' . strtolower( $search_term );
if ( !isset( $terms[ $key ] ) )
{
$terms[ $key ] = [
'campaign_id' => (int) $campaign_id,
'ad_group_id' => (int) $ad_group_id,
'ad_group_name' => (string) ( $row['adGroup']['name'] ?? '' ),
'search_term' => $search_term,
'impressions' => 0,
'clicks' => 0,
'cost' => 0.0,
'conversions' => 0.0,
'conversion_value' => 0.0,
'roas' => 0.0
];
}
$terms[ $key ]['impressions'] += (int) ( $row['metrics']['impressions'] ?? 0 );
$terms[ $key ]['clicks'] += (int) ( $row['metrics']['clicks'] ?? 0 );
$terms[ $key ]['cost'] += (float) ( $row['metrics']['costMicros'] ?? 0 ) / 1000000;
$terms[ $key ]['conversions'] += (float) ( $row['metrics']['conversions'] ?? 0 );
$terms[ $key ]['conversion_value'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 );
}
foreach ( $terms as $key => &$term )
{
if ( (int) $term['clicks'] <= 0 )
{
unset( $terms[ $key ] );
continue;
}
$term['roas'] = ( $term['cost'] > 0 )
? round( ( $term['conversion_value'] / $term['cost'] ) * 100, 2 )
: 0;
}
return array_values( $terms );
}
private function aggregate_campaign_search_terms( $results )
{
$terms = [];
foreach ( $results as $row )
{
$campaign_id = $row['campaign']['id'] ?? null;
$search_term = trim( (string) ( $row['campaignSearchTermView']['searchTerm'] ?? '' ) );
if ( !$campaign_id || $search_term === '' )
{
continue;
}
$key = $campaign_id . '|0|' . strtolower( $search_term );
if ( !isset( $terms[ $key ] ) )
{
$terms[ $key ] = [
'campaign_id' => (int) $campaign_id,
'ad_group_id' => 0,
'ad_group_name' => 'PMax (bez grup reklam)',
'search_term' => $search_term,
'impressions' => 0,
'clicks' => 0,
'cost' => 0.0,
'conversions' => 0.0,
'conversion_value' => 0.0,
'roas' => 0.0
];
}
$terms[ $key ]['impressions'] += (int) ( $row['metrics']['impressions'] ?? 0 );
$terms[ $key ]['clicks'] += (int) ( $row['metrics']['clicks'] ?? 0 );
$terms[ $key ]['cost'] += (float) ( $row['metrics']['costMicros'] ?? 0 ) / 1000000;
$terms[ $key ]['conversions'] += (float) ( $row['metrics']['conversions'] ?? 0 );
$terms[ $key ]['conversion_value'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 );
}
foreach ( $terms as $key => &$term )
{
if ( (int) $term['clicks'] <= 0 )
{
unset( $terms[ $key ] );
continue;
}
$term['roas'] = ( $term['cost'] > 0 )
? round( ( $term['conversion_value'] / $term['cost'] ) * 100, 2 )
: 0;
}
return array_values( $terms );
}
}