- 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.
1051 lines
34 KiB
PHP
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 );
|
|
}
|
|
}
|