Files
adsPRO/autoload/services/class.GoogleAdsApi.php
Jacek Pyziak 651d925b20 Add keyword status toggle functionality and styling
- Introduced a new button to toggle the status of keywords between PAUSED and ENABLED in the keywords table.
- Added corresponding styles for the toggle button to enhance user experience.
- Updated the keywords table rendering logic to display the status and toggle button correctly.
- Created a new migration to add a 'status' column to the 'campaign_keywords' table, defaulting to 'ENABLED'.
2026-02-24 23:31:17 +01:00

3761 lines
119 KiB
PHP

<?php
namespace services;
class GoogleAdsApi
{
private $developer_token;
private $client_id;
private $client_secret;
private $refresh_token;
private $merchant_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';
private static $MERCHANT_BASE_URL = 'https://shoppingcontent.googleapis.com/content/v2.1';
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 -> merchant_refresh_token = self::get_setting( 'google_merchant_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 ] );
}
if ( $key === 'google_ads_last_error' )
{
$error_at = null;
if ( $value !== null && trim( (string) $value ) !== '' )
{
$error_at = date( 'Y-m-d H:i:s' );
}
self::set_setting( 'google_ads_last_error_at', $error_at );
}
}
// --- Konfiguracja ---
public function is_configured()
{
return !empty( $this -> developer_token )
&& !empty( $this -> client_id )
&& !empty( $this -> client_secret )
&& !empty( $this -> refresh_token );
}
public function is_merchant_configured()
{
$merchant_refresh_token = trim( (string) $this -> merchant_refresh_token );
if ( $merchant_refresh_token === '' )
{
$merchant_refresh_token = trim( (string) $this -> refresh_token );
}
return !empty( $this -> client_id )
&& !empty( $this -> client_secret )
&& $merchant_refresh_token !== '';
}
public function get_merchant_product_links_for_offer_ids( $merchant_account_id, $offer_ids )
{
$merchant_account_id = preg_replace( '/\D+/', '', (string) $merchant_account_id );
$offer_ids = array_values( array_unique( array_filter( array_map( function( $item )
{
return trim( (string) $item );
}, (array) $offer_ids ) ) ) );
if ( $merchant_account_id === '' )
{
self::set_setting( 'google_merchant_last_error', 'Brak Merchant Account ID.' );
return false;
}
if ( empty( $offer_ids ) )
{
return [];
}
$access_token = $this -> get_merchant_access_token();
if ( !$access_token )
{
return false;
}
// 1. Proba bezposredniego GET po skonstruowanym productId
$found = [];
$remaining_ids = [];
foreach ( $offer_ids as $oid )
{
$item = $this -> try_direct_merchant_product_get( $merchant_account_id, $oid, $access_token );
if ( $item !== null )
{
$link = trim( (string) ( $item['link'] ?? '' ) );
if ( $this -> is_valid_merchant_product_url( $link ) )
{
$found[ $oid ] = $link;
continue;
}
}
$remaining_ids[] = $oid;
}
if ( empty( $remaining_ids ) )
{
self::set_setting( 'google_merchant_last_error', null );
return $found;
}
// 2. Fallback: listowanie z case-insensitive matching
$remaining_lower = [];
foreach ( $remaining_ids as $oid )
{
$remaining_lower[ strtolower( $oid ) ] = $oid;
}
$page_token = '';
$safety_limit = 500;
while ( $safety_limit-- > 0 )
{
$query = [ 'maxResults' => 250 ];
if ( $page_token !== '' )
{
$query['pageToken'] = $page_token;
}
$url = self::$MERCHANT_BASE_URL . '/' . rawurlencode( $merchant_account_id ) . '/products?' . http_build_query( $query );
$ch = curl_init( $url );
curl_setopt_array( $ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $access_token,
'Accept: application/json'
],
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_merchant_last_error', 'Merchant products.list failed: HTTP ' . $http_code . ' | ' . $error . ' | ' . substr( (string) $response, 0, 1000 ) );
return false;
}
$payload = json_decode( (string) $response, true );
if ( !is_array( $payload ) )
{
self::set_setting( 'google_merchant_last_error', 'Merchant products.list: niepoprawna odpowiedz JSON.' );
return false;
}
$items = isset( $payload['resources'] ) && is_array( $payload['resources'] )
? $payload['resources']
: ( isset( $payload['items'] ) && is_array( $payload['items'] ) ? $payload['items'] : [] );
foreach ( $items as $item )
{
$item_offer_id = trim( (string) ( $item['offerId'] ?? '' ) );
if ( $item_offer_id === '' )
{
continue;
}
$item_offer_id_lower = strtolower( $item_offer_id );
if ( !isset( $remaining_lower[ $item_offer_id_lower ] ) )
{
continue;
}
$link = trim( (string) ( $item['link'] ?? '' ) );
if ( !$this -> is_valid_merchant_product_url( $link ) )
{
continue;
}
$original_key = $remaining_lower[ $item_offer_id_lower ];
$found[ $original_key ] = $link;
unset( $remaining_lower[ $item_offer_id_lower ] );
}
if ( empty( $remaining_lower ) )
{
break;
}
$page_token = trim( (string) ( $payload['nextPageToken'] ?? '' ) );
if ( $page_token === '' )
{
break;
}
}
self::set_setting( 'google_merchant_last_error', null );
return $found;
}
public function get_merchant_products_for_offer_ids( $merchant_account_id, $offer_ids )
{
$merchant_account_id = preg_replace( '/\D+/', '', (string) $merchant_account_id );
$offer_ids = array_values( array_unique( array_filter( array_map( function( $item )
{
return trim( (string) $item );
}, (array) $offer_ids ) ) ) );
if ( $merchant_account_id === '' )
{
self::set_setting( 'google_merchant_last_error', 'Brak Merchant Account ID.' );
return false;
}
if ( empty( $offer_ids ) )
{
return [];
}
$access_token = $this -> get_merchant_access_token();
if ( !$access_token )
{
return false;
}
// 1. Proba bezposredniego GET po skonstruowanym productId (szybkie, pewne)
$found = [];
$remaining_ids = [];
foreach ( $offer_ids as $oid )
{
$item = $this -> try_direct_merchant_product_get( $merchant_account_id, $oid, $access_token );
if ( $item !== null )
{
$found[ $oid ] = $item;
}
else
{
$remaining_ids[] = $oid;
}
}
if ( empty( $remaining_ids ) )
{
self::set_setting( 'google_merchant_last_error', null );
return $found;
}
// 2. Fallback: listowanie produktow z case-insensitive matching
$remaining_lower = [];
foreach ( $remaining_ids as $oid )
{
$remaining_lower[ strtolower( $oid ) ] = $oid;
}
$page_token = '';
$safety_limit = 500;
while ( $safety_limit-- > 0 )
{
$query = [ 'maxResults' => 250 ];
if ( $page_token !== '' )
{
$query['pageToken'] = $page_token;
}
$url = self::$MERCHANT_BASE_URL . '/' . rawurlencode( $merchant_account_id ) . '/products?' . http_build_query( $query );
$ch = curl_init( $url );
curl_setopt_array( $ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $access_token,
'Accept: application/json'
],
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_merchant_last_error', 'Merchant products.list failed: HTTP ' . $http_code . ' | ' . $error . ' | ' . substr( (string) $response, 0, 1000 ) );
return false;
}
$payload = json_decode( (string) $response, true );
if ( !is_array( $payload ) )
{
self::set_setting( 'google_merchant_last_error', 'Merchant products.list: niepoprawna odpowiedz JSON.' );
return false;
}
$items = isset( $payload['resources'] ) && is_array( $payload['resources'] )
? $payload['resources']
: ( isset( $payload['items'] ) && is_array( $payload['items'] ) ? $payload['items'] : [] );
foreach ( $items as $item )
{
$item_offer_id = trim( (string) ( $item['offerId'] ?? '' ) );
if ( $item_offer_id === '' )
{
continue;
}
$item_offer_id_lower = strtolower( $item_offer_id );
if ( !isset( $remaining_lower[ $item_offer_id_lower ] ) )
{
continue;
}
$original_key = $remaining_lower[ $item_offer_id_lower ];
$found[ $original_key ] = $item;
unset( $remaining_lower[ $item_offer_id_lower ] );
}
if ( empty( $remaining_lower ) )
{
break;
}
$page_token = trim( (string) ( $payload['nextPageToken'] ?? '' ) );
if ( $page_token === '' )
{
break;
}
}
self::set_setting( 'google_merchant_last_error', null );
return $found;
}
/**
* Bezposredni GET produktu z Merchant Center po skonstruowanym productId.
* Format: online:{contentLanguage}:{feedLabel}:{offerId}
* Probuje kilka wariantow lang/country wyekstrahowanych z offer_id.
*/
private function try_direct_merchant_product_get( $merchant_account_id, $offer_id, $access_token )
{
$candidates = [];
// Ekstrakcja jezyka/kraju z Shopify-style offer_id: shopify_XX_...
if ( preg_match( '/^shopify_([a-z]{2})_/i', $offer_id, $m ) )
{
$lang = strtolower( $m[1] );
$country = strtoupper( $m[1] );
$candidates[] = 'online:' . $lang . ':' . $country . ':' . $offer_id;
}
// Domyslnie polski rynek
$candidates[] = 'online:pl:PL:' . $offer_id;
$candidates[] = 'online:en:PL:' . $offer_id;
$candidates = array_unique( $candidates );
foreach ( $candidates as $product_id )
{
$url = self::$MERCHANT_BASE_URL . '/' . rawurlencode( $merchant_account_id )
. '/products/' . rawurlencode( $product_id );
$ch = curl_init( $url );
curl_setopt_array( $ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $access_token,
'Accept: application/json'
],
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 30,
] );
$response = curl_exec( $ch );
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
curl_close( $ch );
if ( $http_code === 200 && $response )
{
$item = json_decode( (string) $response, true );
if ( is_array( $item ) && !empty( $item['id'] ) )
{
return $item;
}
}
}
return null;
}
public function update_merchant_product_fields_by_offer_id( $merchant_account_id, $offer_id, $fields )
{
$merchant_account_id = preg_replace( '/\D+/', '', (string) $merchant_account_id );
$offer_id = trim( (string) $offer_id );
$fields = is_array( $fields ) ? $fields : [];
if ( $merchant_account_id === '' || $offer_id === '' )
{
self::set_setting( 'google_merchant_last_error', 'Brak Merchant Account ID lub offer_id.' );
return [ 'success' => false, 'error' => 'Brak Merchant Account ID lub offer_id.' ];
}
$field_map = [
'title' => 'title',
'description' => 'description',
'google_product_category' => 'googleProductCategory',
'custom_label_0' => 'customLabel0',
'custom_label_1' => 'customLabel1',
'custom_label_2' => 'customLabel2',
'custom_label_3' => 'customLabel3',
'custom_label_4' => 'customLabel4'
];
$patch_payload = [];
foreach ( $fields as $key => $value )
{
if ( !isset( $field_map[ $key ] ) )
{
continue;
}
$patch_payload[ $field_map[ $key ] ] = $value;
}
if ( empty( $patch_payload ) )
{
return [ 'success' => true, 'skipped' => true, 'message' => 'Brak wspieranych pol do synchronizacji.' ];
}
$items_map = $this -> get_merchant_products_for_offer_ids( $merchant_account_id, [ $offer_id ] );
if ( $items_map === false || !isset( $items_map[ $offer_id ] ) )
{
$error_message = trim( (string) self::get_setting( 'google_merchant_last_error' ) );
if ( $error_message === '' )
{
$error_message = 'Nie znaleziono produktu w Merchant Center dla offer_id: ' . $offer_id;
}
return [ 'success' => false, 'error' => $error_message ];
}
$merchant_item = $items_map[ $offer_id ];
$merchant_product_id = trim( (string) ( $merchant_item['id'] ?? '' ) );
if ( $merchant_product_id === '' )
{
return [ 'success' => false, 'error' => 'Brak identyfikatora produktu Merchant (id) dla offer_id: ' . $offer_id ];
}
$access_token = $this -> get_merchant_access_token();
if ( !$access_token )
{
return [ 'success' => false, 'error' => (string) self::get_setting( 'google_merchant_last_error' ) ];
}
$url = self::$MERCHANT_BASE_URL
. '/' . rawurlencode( $merchant_account_id )
. '/products/' . rawurlencode( $merchant_product_id );
$ch = curl_init( $url );
curl_setopt_array( $ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . $access_token,
'Content-Type: application/json',
'Accept: application/json'
],
CURLOPT_POSTFIELDS => json_encode( $patch_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 )
{
$error_data = json_decode( (string) $response, true );
$error_message = (string) ( $error_data['error']['message'] ?? '' );
if ( $error_message === '' )
{
$error_message = 'Merchant products.patch failed: HTTP ' . $http_code . ' | ' . $error . ' | ' . substr( (string) $response, 0, 1000 );
}
self::set_setting( 'google_merchant_last_error', $error_message );
return [
'success' => false,
'error' => $error_message,
'merchant_product_id' => $merchant_product_id,
'response' => $response
];
}
$response_data = json_decode( (string) $response, true );
self::set_setting( 'google_merchant_last_error', null );
return [
'success' => true,
'merchant_product_id' => $merchant_product_id,
'response' => $response_data,
'patched_fields' => array_keys( $patch_payload )
];
}
// --- 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;
}
private function get_merchant_access_token()
{
$cached_token = self::get_setting( 'google_merchant_access_token' );
$cached_expires = (int) self::get_setting( 'google_merchant_access_token_expires' );
if ( $cached_token && $cached_expires > time() )
{
return $cached_token;
}
return $this -> refresh_merchant_access_token();
}
private function refresh_merchant_access_token()
{
$merchant_refresh_token = trim( (string) $this -> merchant_refresh_token );
if ( $merchant_refresh_token === '' )
{
$merchant_refresh_token = trim( (string) $this -> refresh_token );
}
if ( $merchant_refresh_token === '' )
{
self::set_setting( 'google_merchant_last_error', 'Brak refresh tokena dla Merchant API.' );
return false;
}
$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' => $merchant_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_merchant_last_error', 'Merchant token refresh failed: HTTP ' . $http_code . ' | ' . $error . ' | ' . $response );
return false;
}
$data = json_decode( (string) $response, true );
if ( !isset( $data['access_token'] ) )
{
self::set_setting( 'google_merchant_last_error', 'Merchant token refresh: brak access_token w odpowiedzi' );
return false;
}
$access_token = (string) $data['access_token'];
$expires_at = time() + ( $data['expires_in'] ?? 3600 ) - 60;
self::set_setting( 'google_merchant_access_token', $access_token );
self::set_setting( 'google_merchant_access_token_expires', $expires_at );
self::set_setting( 'google_merchant_last_error', null );
return $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 . ' | ' . (string) $response );
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 . ' | ' . (string) $response );
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;
}
private function normalize_ads_customer_id( $customer_id )
{
return preg_replace( '/\D+/', '', (string) $customer_id );
}
private function parse_resource_id( $resource_name )
{
$resource_name = trim( (string) $resource_name );
if ( preg_match( '#/(\d+)$#', $resource_name, $matches ) )
{
return (int) $matches[1];
}
return 0;
}
public function create_standard_shopping_campaign( $customer_id, $merchant_account_id, $campaign_name, $daily_budget = 50.0, $sales_country = 'PL' )
{
$customer_id = $this -> normalize_ads_customer_id( $customer_id );
$merchant_account_id = preg_replace( '/\D+/', '', (string) $merchant_account_id );
$campaign_name = trim( (string) $campaign_name );
$sales_country = strtoupper( trim( (string) $sales_country ) );
$daily_budget = (float) $daily_budget;
if ( $customer_id === '' || $merchant_account_id === '' || $campaign_name === '' )
{
self::set_setting( 'google_ads_last_error', 'Brak danych do utworzenia kampanii Standard Shopping.' );
return [ 'success' => false ];
}
if ( $sales_country === '' )
{
$sales_country = 'PL';
}
if ( $daily_budget <= 0 )
{
$daily_budget = 50.0;
}
$budget_micros = max( 1000000, (int) round( $daily_budget * 1000000 ) );
$budget_tmp_resource = 'customers/' . $customer_id . '/campaignBudgets/-1';
$operations = [
[
'campaignBudgetOperation' => [
'create' => [
'resourceName' => $budget_tmp_resource,
'name' => 'adsPRO | Budget | ' . $campaign_name . ' | ' . date( 'Y-m-d H:i:s' ),
'amountMicros' => $budget_micros,
'deliveryMethod' => 'STANDARD',
'explicitlyShared' => false
]
]
],
[
'campaignOperation' => [
'create' => [
'name' => $campaign_name,
'status' => 'PAUSED',
'advertisingChannelType' => 'SHOPPING',
'campaignBudget' => $budget_tmp_resource,
'manualCpc' => (object) [],
'shoppingSetting' => [
'merchantId' => (int) $merchant_account_id,
'salesCountry' => $sales_country,
'campaignPriority' => 0,
'enableLocal' => false
]
]
]
]
];
$result = $this -> mutate( $customer_id, $operations );
if ( $result === false )
{
return [
'success' => false,
'error' => (string) self::get_setting( 'google_ads_last_error' ),
'sent_operations' => $operations
];
}
$campaign_resource_name = '';
foreach ( (array) ( $result['mutateOperationResponses'] ?? [] ) as $row )
{
$campaign_resource_name = (string) ( $row['campaignResult']['resourceName'] ?? '' );
if ( $campaign_resource_name === '' )
{
$campaign_resource_name = (string) ( $row['campaign_result']['resource_name'] ?? '' );
}
if ( $campaign_resource_name !== '' )
{
break;
}
}
$campaign_id = $this -> parse_resource_id( $campaign_resource_name );
if ( $campaign_id <= 0 )
{
self::set_setting( 'google_ads_last_error', 'Nie udało się odczytać ID kampanii z odpowiedzi mutate.' );
return [ 'success' => false, 'response' => $result ];
}
return [
'success' => true,
'campaign_id' => $campaign_id,
'campaign_name' => $campaign_name,
'response' => $result
];
}
private function get_listing_groups_count_for_ad_group( $customer_id, $ad_group_id )
{
$customer_id = $this -> normalize_ads_customer_id( $customer_id );
$ad_group_id = (int) $ad_group_id;
if ( $customer_id === '' || $ad_group_id <= 0 )
{
return false;
}
$gaql = "SELECT "
. "ad_group_criterion.resource_name "
. "FROM ad_group_criterion "
. "WHERE ad_group.id = " . $ad_group_id . " "
. "AND ad_group_criterion.type = 'LISTING_GROUP' "
. "LIMIT 100";
$rows = $this -> search_stream( $customer_id, $gaql );
if ( !is_array( $rows ) )
{
return false;
}
return count( $rows );
}
private function get_listing_group_nodes_for_ad_group( $customer_id, $ad_group_id )
{
$customer_id = $this -> normalize_ads_customer_id( $customer_id );
$ad_group_id = (int) $ad_group_id;
if ( $customer_id === '' || $ad_group_id <= 0 )
{
return false;
}
$gaql = "SELECT "
. "ad_group_criterion.resource_name, "
. "ad_group_criterion.listing_group.parent_ad_group_criterion "
. "FROM ad_group_criterion "
. "WHERE ad_group.id = " . $ad_group_id . " "
. "AND ad_group_criterion.type = 'LISTING_GROUP' "
. "LIMIT 1000";
$rows = $this -> search_stream( $customer_id, $gaql );
if ( !is_array( $rows ) )
{
return false;
}
$nodes = [];
foreach ( $rows as $row )
{
$resource_name = trim( (string) ( $row['adGroupCriterion']['resourceName'] ?? $row['ad_group_criterion']['resource_name'] ?? '' ) );
if ( $resource_name === '' )
{
continue;
}
$parent_resource = trim( (string) ( $row['adGroupCriterion']['listingGroup']['parentAdGroupCriterion'] ?? $row['ad_group_criterion']['listing_group']['parent_ad_group_criterion'] ?? '' ) );
$nodes[] = [
'resource_name' => $resource_name,
'parent_resource_name' => $parent_resource
];
}
return $nodes;
}
private function build_listing_group_removal_order( $nodes )
{
$remaining = [];
foreach ( (array) $nodes as $node )
{
$resource_name = trim( (string) ( $node['resource_name'] ?? '' ) );
if ( $resource_name === '' )
{
continue;
}
$remaining[ $resource_name ] = trim( (string) ( $node['parent_resource_name'] ?? '' ) );
}
$order = [];
while ( !empty( $remaining ) )
{
$parent_set = [];
foreach ( $remaining as $resource_name => $parent_resource )
{
if ( $parent_resource !== '' && isset( $remaining[ $parent_resource ] ) )
{
$parent_set[ $parent_resource ] = true;
}
}
$leaf_resources = [];
foreach ( $remaining as $resource_name => $parent_resource )
{
if ( !isset( $parent_set[ $resource_name ] ) )
{
$leaf_resources[] = $resource_name;
}
}
if ( empty( $leaf_resources ) )
{
foreach ( array_keys( $remaining ) as $resource_name )
{
$order[] = $resource_name;
}
break;
}
foreach ( $leaf_resources as $resource_name )
{
$order[] = $resource_name;
unset( $remaining[ $resource_name ] );
}
}
return array_values( array_unique( $order ) );
}
private function clear_listing_groups_in_ad_group( $customer_id, $ad_group_id )
{
$customer_id = $this -> normalize_ads_customer_id( $customer_id );
$ad_group_id = (int) $ad_group_id;
if ( $customer_id === '' || $ad_group_id <= 0 )
{
return [ 'success' => false, 'removed' => 0 ];
}
$nodes = $this -> get_listing_group_nodes_for_ad_group( $customer_id, $ad_group_id );
if ( $nodes === false )
{
return [
'success' => false,
'removed' => 0,
'error' => (string) self::get_setting( 'google_ads_last_error' )
];
}
if ( empty( $nodes ) )
{
return [ 'success' => true, 'removed' => 0 ];
}
$removal_order = $this -> build_listing_group_removal_order( $nodes );
if ( empty( $removal_order ) )
{
return [ 'success' => true, 'removed' => 0 ];
}
$operations = [];
foreach ( $removal_order as $resource_name )
{
$operations[] = [
'adGroupCriterionOperation' => [
'remove' => $resource_name
]
];
}
$result = $this -> mutate( $customer_id, $operations );
if ( $result === false )
{
return [
'success' => false,
'removed' => 0,
'error' => (string) self::get_setting( 'google_ads_last_error' ),
'sent_operations_count' => count( $operations )
];
}
return [ 'success' => true, 'removed' => count( $operations ) ];
}
private function find_ad_group_id_by_campaign_and_name( $customer_id, $campaign_id, $ad_group_name )
{
$customer_id = $this -> normalize_ads_customer_id( $customer_id );
$campaign_id = (int) $campaign_id;
$ad_group_name = trim( (string) $ad_group_name );
if ( $customer_id === '' || $campaign_id <= 0 || $ad_group_name === '' )
{
return 0;
}
$gaql = "SELECT "
. "ad_group.id "
. "FROM ad_group "
. "WHERE campaign.id = " . $campaign_id . " "
. "AND ad_group.name = '" . $this -> gaql_escape( $ad_group_name ) . "' "
. "AND ad_group.status != 'REMOVED' "
. "LIMIT 1";
$rows = $this -> search_stream( $customer_id, $gaql );
if ( !is_array( $rows ) || empty( $rows ) )
{
return 0;
}
return (int) ( $rows[0]['adGroup']['id'] ?? $rows[0]['ad_group']['id'] ?? 0 );
}
private function get_ad_group_status( $customer_id, $ad_group_id )
{
$customer_id = $this -> normalize_ads_customer_id( $customer_id );
$ad_group_id = (int) $ad_group_id;
if ( $customer_id === '' || $ad_group_id <= 0 )
{
return '';
}
$gaql = "SELECT "
. "ad_group.status "
. "FROM ad_group "
. "WHERE ad_group.id = " . $ad_group_id . " "
. "LIMIT 1";
$rows = $this -> search_stream( $customer_id, $gaql );
if ( !is_array( $rows ) || empty( $rows ) )
{
return '';
}
return strtoupper( trim( (string) ( $rows[0]['adGroup']['status'] ?? $rows[0]['ad_group']['status'] ?? '' ) ) );
}
private function enable_ad_group( $customer_id, $ad_group_id )
{
$customer_id = $this -> normalize_ads_customer_id( $customer_id );
$ad_group_id = (int) $ad_group_id;
if ( $customer_id === '' || $ad_group_id <= 0 )
{
return false;
}
$operation = [
'adGroupOperation' => [
'update' => [
'resourceName' => 'customers/' . $customer_id . '/adGroups/' . $ad_group_id,
'status' => 'ENABLED'
],
'updateMask' => 'status'
]
];
return $this -> mutate( $customer_id, [ $operation ] ) !== false;
}
public function remove_ad_group( $customer_id, $ad_group_id )
{
$customer_id = $this -> normalize_ads_customer_id( $customer_id );
$ad_group_id = (int) $ad_group_id;
if ( $customer_id === '' || $ad_group_id <= 0 )
{
self::set_setting( 'google_ads_last_error', 'Brak danych do usuniecia grupy reklam.' );
return [ 'success' => false, 'removed' => 0 ];
}
$resource_name = 'customers/' . $customer_id . '/adGroups/' . $ad_group_id;
$operation = [
'adGroupOperation' => [
'remove' => $resource_name
]
];
$mutate_result = $this -> mutate( $customer_id, [ $operation ] );
if ( $mutate_result === false )
{
$last_error = (string) self::get_setting( 'google_ads_last_error' );
$is_not_found = stripos( $last_error, 'NOT_FOUND' ) !== false
|| stripos( $last_error, 'RESOURCE_NOT_FOUND' ) !== false;
if ( $is_not_found )
{
return [
'success' => true,
'removed' => 0,
'not_found' => true,
'resource_name' => $resource_name
];
}
return [
'success' => false,
'removed' => 0,
'error' => $last_error,
'resource_name' => $resource_name
];
}
return [
'success' => true,
'removed' => 1,
'response' => $mutate_result,
'resource_name' => $resource_name
];
}
private function get_root_listing_group_resource_name( $customer_id, $ad_group_id )
{
$customer_id = $this -> normalize_ads_customer_id( $customer_id );
$ad_group_id = (int) $ad_group_id;
if ( $customer_id === '' || $ad_group_id <= 0 )
{
return '';
}
$gaql = "SELECT "
. "ad_group_criterion.resource_name, "
. "ad_group_criterion.listing_group.type "
. "FROM ad_group_criterion "
. "WHERE ad_group.id = " . $ad_group_id . " "
. "AND ad_group_criterion.type = 'LISTING_GROUP' "
. "LIMIT 50";
$rows = $this -> search_stream( $customer_id, $gaql );
if ( !is_array( $rows ) || empty( $rows ) )
{
return '';
}
$fallback_resource = '';
foreach ( $rows as $row )
{
$type = strtoupper( (string) ( $row['listingGroup']['type'] ?? $row['listing_group']['type'] ?? '' ) );
$resource_name = trim( (string) ( $row['adGroupCriterion']['resourceName'] ?? $row['ad_group_criterion']['resource_name'] ?? '' ) );
if ( $resource_name === '' )
{
continue;
}
if ( $fallback_resource === '' )
{
$fallback_resource = $resource_name;
}
if ( $type === 'SUBDIVISION' )
{
return $resource_name;
}
}
return $fallback_resource;
}
private function extract_first_ad_group_criterion_resource_name_from_mutate( $mutate_response )
{
foreach ( (array) ( $mutate_response['mutateOperationResponses'] ?? [] ) as $row )
{
$resource_name = trim( (string) ( $row['adGroupCriterionResult']['resourceName'] ?? '' ) );
if ( $resource_name === '' )
{
$resource_name = trim( (string) ( $row['ad_group_criterion_result']['resource_name'] ?? '' ) );
}
if ( $resource_name !== '' )
{
return $resource_name;
}
}
return '';
}
public function ensure_standard_shopping_offer_in_ad_group( $customer_id, $ad_group_id, $offer_id, $cpc_bid = 1.00 )
{
$customer_id = $this -> normalize_ads_customer_id( $customer_id );
$ad_group_id = (int) $ad_group_id;
$offer_id = trim( (string) $offer_id );
$cpc_bid = (float) $cpc_bid;
if ( $customer_id === '' || $ad_group_id <= 0 || $offer_id === '' )
{
self::set_setting( 'google_ads_last_error', 'Brak danych do przypisania produktu do grupy reklam Standard Shopping.' );
return [ 'success' => false ];
}
if ( $cpc_bid <= 0 )
{
$cpc_bid = 1.00;
}
$listing_groups_count = $this -> get_listing_groups_count_for_ad_group( $customer_id, $ad_group_id );
if ( $listing_groups_count === false )
{
return [ 'success' => false, 'error' => (string) self::get_setting( 'google_ads_last_error' ) ];
}
if ( $listing_groups_count > 0 )
{
$clear_result = $this -> clear_listing_groups_in_ad_group( $customer_id, $ad_group_id );
if ( empty( $clear_result['success'] ) )
{
return [
'success' => false,
'error' => 'Nie udało się usunąć istniejącej struktury listing groups: ' . (string) ( $clear_result['error'] ?? '' ),
'listing_groups_count' => $listing_groups_count,
'clear_result' => $clear_result
];
}
$listing_groups_count_after_clear = $this -> get_listing_groups_count_for_ad_group( $customer_id, $ad_group_id );
if ( $listing_groups_count_after_clear === false )
{
return [ 'success' => false, 'error' => (string) self::get_setting( 'google_ads_last_error' ) ];
}
if ( $listing_groups_count_after_clear > 0 )
{
return [
'success' => false,
'error' => 'Po czyszczeniu nadal istnieją listing groups w grupie reklam.',
'listing_groups_count' => $listing_groups_count_after_clear,
'clear_result' => $clear_result
];
}
}
$ad_group_resource = 'customers/' . $customer_id . '/adGroups/' . $ad_group_id;
$root_temp_resource = 'customers/' . $customer_id . '/adGroupCriteria/' . $ad_group_id . '~-1';
$cpc_bid_micros = max( 10000, (int) round( $cpc_bid * 1000000 ) );
$operations = [
[
'adGroupCriterionOperation' => [
'create' => [
'resourceName' => $root_temp_resource,
'adGroup' => $ad_group_resource,
'status' => 'ENABLED',
'listingGroup' => [
'type' => 'SUBDIVISION'
]
]
]
],
[
'adGroupCriterionOperation' => [
'create' => [
'adGroup' => $ad_group_resource,
'status' => 'ENABLED',
'cpcBidMicros' => $cpc_bid_micros,
'listingGroup' => [
'type' => 'UNIT',
'parentAdGroupCriterion' => $root_temp_resource,
'caseValue' => [
'productItemId' => [
'value' => $offer_id
]
]
]
]
]
],
[
'adGroupCriterionOperation' => [
'create' => [
'adGroup' => $ad_group_resource,
'status' => 'ENABLED',
'negative' => true,
'listingGroup' => [
'type' => 'UNIT',
'parentAdGroupCriterion' => $root_temp_resource,
'caseValue' => [
'productItemId' => (object) []
]
]
]
]
]
];
$result = $this -> mutate( $customer_id, $operations );
if ( $result === false )
{
return [
'success' => false,
'error' => (string) self::get_setting( 'google_ads_last_error' ),
'sent_operations' => $operations,
'root_temp_resource' => $root_temp_resource
];
}
return [
'success' => true,
'response' => $result,
'root_temp_resource' => $root_temp_resource
];
}
public function create_standard_shopping_ad_group_with_offer( $customer_id, $campaign_id, $ad_group_name, $offer_id, $cpc_bid = 1.00 )
{
$customer_id = $this -> normalize_ads_customer_id( $customer_id );
$campaign_id = (int) $campaign_id;
$ad_group_name = trim( (string) $ad_group_name );
$offer_id = trim( (string) $offer_id );
$cpc_bid = (float) $cpc_bid;
if ( $customer_id === '' || $campaign_id <= 0 || $ad_group_name === '' || $offer_id === '' )
{
self::set_setting( 'google_ads_last_error', 'Brak danych do utworzenia grupy reklam Standard Shopping.' );
return [ 'success' => false ];
}
if ( $cpc_bid <= 0 )
{
$cpc_bid = 1.00;
}
$ad_group_tmp_resource = 'customers/' . $customer_id . '/adGroups/-1';
$cpc_bid_micros = max( 10000, (int) round( $cpc_bid * 1000000 ) );
$operations = [
[
'adGroupOperation' => [
'create' => [
'resourceName' => $ad_group_tmp_resource,
'campaign' => 'customers/' . $customer_id . '/campaigns/' . $campaign_id,
'name' => $ad_group_name,
'status' => 'PAUSED',
'type' => 'SHOPPING_PRODUCT_ADS',
'cpcBidMicros' => $cpc_bid_micros
]
]
],
[
'adGroupAdOperation' => [
'create' => [
'adGroup' => $ad_group_tmp_resource,
'status' => 'PAUSED',
'ad' => [
'shoppingProductAd' => (object) []
]
]
]
]
];
$result = $this -> mutate( $customer_id, $operations );
if ( $result === false )
{
$last_error = (string) self::get_setting( 'google_ads_last_error' );
$is_duplicate_name = stripos( $last_error, 'DUPLICATE_ADGROUP_NAME' ) !== false
|| stripos( $last_error, 'AdGroup with the same name already exists' ) !== false;
if ( $is_duplicate_name )
{
$existing_ad_group_id = $this -> find_ad_group_id_by_campaign_and_name( $customer_id, $campaign_id, $ad_group_name );
if ( $existing_ad_group_id > 0 )
{
$existing_status = $this -> get_ad_group_status( $customer_id, $existing_ad_group_id );
if ( $existing_status === 'REMOVED' )
{
return [
'success' => false,
'duplicate_name' => true,
'ad_group_id' => $existing_ad_group_id,
'ad_group_name' => $ad_group_name,
'error' => 'Istniejąca grupa reklam o tej nazwie jest usunięta (REMOVED). Wybierz inną nazwę grupy.'
];
}
if ( $existing_status === 'PAUSED' )
{
if ( !$this -> enable_ad_group( $customer_id, $existing_ad_group_id ) )
{
return [
'success' => false,
'duplicate_name' => true,
'ad_group_id' => $existing_ad_group_id,
'ad_group_name' => $ad_group_name,
'error' => 'Nie udało się włączyć istniejącej grupy reklam (PAUSED -> ENABLED).'
];
}
}
$offer_result = $this -> ensure_standard_shopping_offer_in_ad_group( $customer_id, $existing_ad_group_id, $offer_id, $cpc_bid );
if ( !empty( $offer_result['success'] ) )
{
return [
'success' => true,
'duplicate_name' => true,
'ad_group_id' => $existing_ad_group_id,
'ad_group_name' => $ad_group_name,
'offer_result' => $offer_result,
'response' => null
];
}
return [
'success' => false,
'duplicate_name' => true,
'ad_group_id' => $existing_ad_group_id,
'ad_group_name' => $ad_group_name,
'error' => (string) ( $offer_result['error'] ?? 'Nie udało się przypisać produktu do istniejącej grupy reklam.' ),
'offer_result' => $offer_result,
'response' => null
];
}
}
return [
'success' => false,
'error' => $last_error,
'sent_operations' => $operations
];
}
$ad_group_resource_name = '';
foreach ( (array) ( $result['mutateOperationResponses'] ?? [] ) as $row )
{
$ad_group_resource_name = (string) ( $row['adGroupResult']['resourceName'] ?? '' );
if ( $ad_group_resource_name === '' )
{
$ad_group_resource_name = (string) ( $row['ad_group_result']['resource_name'] ?? '' );
}
if ( $ad_group_resource_name !== '' )
{
break;
}
}
$ad_group_id = $this -> parse_resource_id( $ad_group_resource_name );
if ( $ad_group_id <= 0 )
{
self::set_setting( 'google_ads_last_error', 'Nie udało się odczytać ID grupy reklam z odpowiedzi mutate.' );
return [ 'success' => false, 'response' => $result ];
}
$offer_result = $this -> ensure_standard_shopping_offer_in_ad_group( $customer_id, $ad_group_id, $offer_id, $cpc_bid );
if ( empty( $offer_result['success'] ) )
{
return [
'success' => false,
'ad_group_id' => $ad_group_id,
'error' => (string) ( $offer_result['error'] ?? 'Nie udało się ustawić filtra produktu w grupie reklam.' ),
'offer_result' => $offer_result,
'response' => $result
];
}
return [
'success' => true,
'ad_group_id' => $ad_group_id,
'ad_group_name' => $ad_group_name,
'response' => $result,
'offer_result' => $offer_result
];
}
public function generate_keyword_ideas_from_url( $customer_id, $url, $limit = 40 )
{
$access_token = $this -> get_access_token();
if ( !$access_token )
{
return false;
}
$customer_id = preg_replace( '/\D+/', '', (string) $customer_id );
$url = trim( (string) $url );
$limit = max( 1, (int) $limit );
if ( $customer_id === '' || $url === '' )
{
self::set_setting( 'google_ads_last_error', 'Brak customer_id lub URL dla generate_keyword_ideas_from_url.' );
return false;
}
if ( !filter_var( $url, FILTER_VALIDATE_URL ) )
{
self::set_setting( 'google_ads_last_error', 'Nieprawidlowy URL dla Keyword Planner.' );
return false;
}
$endpoint = self::$ADS_BASE_URL . '/' . self::$API_VERSION . '/customers/' . $customer_id . ':generateKeywordIdeas';
$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 = [
'urlSeed' => [
'url' => $url
],
'keywordPlanNetwork' => 'GOOGLE_SEARCH_AND_PARTNERS',
'includeAdultKeywords' => false
];
$ch = curl_init( $endpoint );
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', 'generateKeywordIdeas failed: HTTP ' . $http_code . ' | ' . $error . ' | ' . (string) $response );
return false;
}
$data = json_decode( (string) $response, true );
if ( !is_array( $data ) )
{
self::set_setting( 'google_ads_last_error', 'generateKeywordIdeas failed: niepoprawna odpowiedz JSON' );
return false;
}
$results = isset( $data['results'] ) && is_array( $data['results'] ) ? $data['results'] : [];
$parsed = [];
foreach ( $results as $row )
{
$keyword_text = trim( (string) ( $row['text'] ?? '' ) );
if ( $keyword_text === '' )
{
continue;
}
$metrics = isset( $row['keywordIdeaMetrics'] ) && is_array( $row['keywordIdeaMetrics'] )
? $row['keywordIdeaMetrics']
: [];
$parsed[] = [
'keyword' => $keyword_text,
'avg_monthly_searches' => (int) ( $metrics['avgMonthlySearches'] ?? 0 ),
'competition' => isset( $metrics['competition'] ) ? (string) $metrics['competition'] : null,
'competition_index' => isset( $metrics['competitionIndex'] ) ? (int) $metrics['competitionIndex'] : null,
];
}
usort( $parsed, static function( $left, $right )
{
$left_volume = (int) ( $left['avg_monthly_searches'] ?? 0 );
$right_volume = (int) ( $right['avg_monthly_searches'] ?? 0 );
if ( $left_volume === $right_volume )
{
return strcasecmp( (string) ( $left['keyword'] ?? '' ), (string) ( $right['keyword'] ?? '' ) );
}
return $right_volume <=> $left_volume;
} );
$unique = [];
foreach ( $parsed as $row )
{
$key = mb_strtolower( (string) $row['keyword'], 'UTF-8' );
if ( isset( $unique[ $key ] ) )
{
continue;
}
$unique[ $key ] = $row;
if ( count( $unique ) >= $limit )
{
break;
}
}
self::set_setting( 'google_ads_last_error', null );
return array_values( $unique );
}
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 )
];
}
public function remove_negative_keyword_from_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 usuniecia frazy wykluczajacej.' );
return [ 'success' => false, 'removed' => 0 ];
}
if ( !in_array( $match_type, [ 'PHRASE', 'EXACT', 'BROAD' ], true ) )
{
$match_type = 'PHRASE';
}
$keyword_text_escaped = $this -> gaql_escape( $keyword_text );
$gaql = "SELECT "
. "campaign_criterion.resource_name "
. "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 50";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false )
{
return [ 'success' => false, 'removed' => 0 ];
}
$resource_names = [];
foreach ( (array) $results as $row )
{
$resource_name = (string) ( $row['campaignCriterion']['resourceName'] ?? '' );
if ( $resource_name === '' )
{
$resource_name = (string) ( $row['campaign_criterion']['resource_name'] ?? '' );
}
if ( $resource_name !== '' )
{
$resource_names[] = $resource_name;
}
}
$resource_names = array_values( array_unique( $resource_names ) );
if ( empty( $resource_names ) )
{
return [ 'success' => true, 'removed' => 0, 'not_found' => true ];
}
$operations = [];
foreach ( $resource_names as $resource_name )
{
$operations[] = [
'campaignCriterionOperation' => [
'remove' => $resource_name
]
];
}
$mutate_result = $this -> mutate( $customer_id, $operations );
if ( $mutate_result === false )
{
return [ 'success' => false, 'removed' => 0, 'sent_operations' => $operations ];
}
return [
'success' => true,
'removed' => count( $resource_names ),
'response' => $mutate_result,
'sent_operations' => $operations
];
}
public function remove_negative_keyword_from_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 usuniecia frazy wykluczajacej.' );
return [ 'success' => false, 'removed' => 0 ];
}
if ( !in_array( $match_type, [ 'PHRASE', 'EXACT', 'BROAD' ], true ) )
{
$match_type = 'PHRASE';
}
$keyword_text_escaped = $this -> gaql_escape( $keyword_text );
$gaql = "SELECT "
. "ad_group_criterion.resource_name "
. "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 50";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false )
{
return [ 'success' => false, 'removed' => 0 ];
}
$resource_names = [];
foreach ( (array) $results as $row )
{
$resource_name = (string) ( $row['adGroupCriterion']['resourceName'] ?? '' );
if ( $resource_name === '' )
{
$resource_name = (string) ( $row['ad_group_criterion']['resource_name'] ?? '' );
}
if ( $resource_name !== '' )
{
$resource_names[] = $resource_name;
}
}
$resource_names = array_values( array_unique( $resource_names ) );
if ( empty( $resource_names ) )
{
return [ 'success' => true, 'removed' => 0, 'not_found' => true ];
}
$operations = [];
foreach ( $resource_names as $resource_name )
{
$operations[] = [
'adGroupCriterionOperation' => [
'remove' => $resource_name
]
];
}
$mutate_result = $this -> mutate( $customer_id, $operations );
if ( $mutate_result === false )
{
return [ 'success' => false, 'removed' => 0, 'sent_operations' => $operations ];
}
return [
'success' => true,
'removed' => count( $resource_names ),
'response' => $mutate_result,
'sent_operations' => $operations
];
}
private function gaql_escape( $value )
{
return str_replace( [ '\\', '\'' ], [ '\\\\', '\\\'' ], (string) $value );
}
public function add_keyword_to_ad_group( $customer_id, $ad_group_id, $keyword_text, $match_type = 'BROAD' )
{
$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.' );
return [ 'success' => false, 'duplicate' => false ];
}
if ( !in_array( $match_type, [ 'PHRASE', 'EXACT', 'BROAD' ], true ) )
{
$match_type = 'BROAD';
}
$operation = [
'adGroupCriterionOperation' => [
'create' => [
'adGroup' => 'customers/' . $customer_id . '/adGroups/' . $ad_group_id,
'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 ];
}
return [ 'success' => false, 'duplicate' => false, 'sent_operation' => $operation ];
}
return [
'success' => true,
'duplicate' => false,
'response' => $result,
'sent_operation' => $operation
];
}
public function remove_keyword_from_ad_group( $customer_id, $ad_group_id, $keyword_text, $match_type = 'BROAD' )
{
$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 usuniecia frazy.' );
return [ 'success' => false, 'removed' => 0 ];
}
if ( !in_array( $match_type, [ 'PHRASE', 'EXACT', 'BROAD' ], true ) )
{
$match_type = 'BROAD';
}
$keyword_text_escaped = $this -> gaql_escape( $keyword_text );
$gaql = "SELECT "
. "ad_group_criterion.resource_name "
. "FROM ad_group_criterion "
. "WHERE ad_group.id = " . $ad_group_id . " "
. "AND ad_group_criterion.type = 'KEYWORD' "
. "AND ad_group_criterion.negative = FALSE "
. "AND ad_group_criterion.keyword.text = '" . $keyword_text_escaped . "' "
. "AND ad_group_criterion.keyword.match_type = " . $match_type . " "
. "LIMIT 50";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false )
{
return [ 'success' => false, 'removed' => 0 ];
}
$resource_names = [];
foreach ( (array) $results as $row )
{
$resource_name = (string) ( $row['adGroupCriterion']['resourceName'] ?? '' );
if ( $resource_name === '' )
{
$resource_name = (string) ( $row['ad_group_criterion']['resource_name'] ?? '' );
}
if ( $resource_name !== '' )
{
$resource_names[] = $resource_name;
}
}
$resource_names = array_values( array_unique( $resource_names ) );
if ( empty( $resource_names ) )
{
return [ 'success' => true, 'removed' => 0, 'not_found' => true ];
}
$operations = [];
foreach ( $resource_names as $resource_name )
{
$operations[] = [
'adGroupCriterionOperation' => [
'remove' => $resource_name
]
];
}
$mutate_result = $this -> mutate( $customer_id, $operations );
if ( $mutate_result === false )
{
return [ 'success' => false, 'removed' => 0, 'sent_operations' => $operations ];
}
return [
'success' => true,
'removed' => count( $resource_names ),
'response' => $mutate_result,
'sent_operations' => $operations
];
}
public function update_keyword_match_type( $customer_id, $ad_group_id, $keyword_text, $old_match_type, $new_match_type )
{
$remove_result = $this -> remove_keyword_from_ad_group( $customer_id, $ad_group_id, $keyword_text, $old_match_type );
if ( !( $remove_result['success'] ?? false ) && !( $remove_result['not_found'] ?? false ) )
{
return [ 'success' => false, 'step' => 'remove', 'remove_result' => $remove_result ];
}
$add_result = $this -> add_keyword_to_ad_group( $customer_id, $ad_group_id, $keyword_text, $new_match_type );
if ( !( $add_result['success'] ?? false ) )
{
return [ 'success' => false, 'step' => 'add', 'add_result' => $add_result, 'remove_result' => $remove_result ];
}
return [
'success' => true,
'remove_result' => $remove_result,
'add_result' => $add_result
];
}
/**
* Zmienia status keywordu (ENABLED / PAUSED) w Google Ads.
*/
public function update_keyword_status( $customer_id, $ad_group_id, $keyword_text, $match_type, $new_status )
{
$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 ) );
$new_status = strtoupper( trim( (string) $new_status ) );
if ( $customer_id === '' || $ad_group_id === '' || $keyword_text === '' )
{
self::set_setting( 'google_ads_last_error', 'Brak wymaganych danych do zmiany statusu frazy.' );
return [ 'success' => false ];
}
if ( !in_array( $new_status, [ 'ENABLED', 'PAUSED' ], true ) )
{
self::set_setting( 'google_ads_last_error', 'Nieprawidlowy status: ' . $new_status );
return [ 'success' => false ];
}
if ( !in_array( $match_type, [ 'PHRASE', 'EXACT', 'BROAD' ], true ) )
{
$match_type = 'BROAD';
}
$keyword_text_escaped = $this -> gaql_escape( $keyword_text );
$gaql = "SELECT "
. "ad_group_criterion.resource_name "
. "FROM ad_group_criterion "
. "WHERE ad_group.id = " . $ad_group_id . " "
. "AND ad_group_criterion.type = 'KEYWORD' "
. "AND ad_group_criterion.negative = FALSE "
. "AND ad_group_criterion.keyword.text = '" . $keyword_text_escaped . "' "
. "AND ad_group_criterion.keyword.match_type = " . $match_type . " "
. "LIMIT 1";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false )
{
return [ 'success' => false ];
}
$resource_name = '';
foreach ( (array) $results as $row )
{
$resource_name = (string) ( $row['adGroupCriterion']['resourceName'] ?? '' );
if ( $resource_name === '' )
{
$resource_name = (string) ( $row['ad_group_criterion']['resource_name'] ?? '' );
}
if ( $resource_name !== '' ) break;
}
if ( $resource_name === '' )
{
self::set_setting( 'google_ads_last_error', 'Nie znaleziono frazy w Google Ads.' );
return [ 'success' => false, 'not_found' => true ];
}
$operation = [
'adGroupCriterionOperation' => [
'updateMask' => 'status',
'update' => [
'resourceName' => $resource_name,
'status' => $new_status
]
]
];
$mutate_result = $this -> mutate( $customer_id, [ $operation ] );
if ( $mutate_result === false )
{
return [ 'success' => false, 'sent_operation' => $operation ];
}
return [
'success' => true,
'new_status' => $new_status,
'response' => $mutate_result,
'sent_operation' => $operation
];
}
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_with_url = "SELECT "
. "segments.date, "
. "segments.product_item_id, "
. "segments.product_title, "
. "segments.product_link, "
. "campaign.id, "
. "campaign.name, "
. "campaign.status, "
. "campaign.advertising_channel_type, "
. "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 . "' "
. "AND campaign.status = 'ENABLED' "
. "AND campaign.advertising_channel_type = 'SHOPPING'";
$gaql_with_ad_group = "SELECT "
. "segments.date, "
. "segments.product_item_id, "
. "segments.product_title, "
. "campaign.id, "
. "campaign.name, "
. "campaign.status, "
. "campaign.advertising_channel_type, "
. "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 . "' "
. "AND campaign.status = 'ENABLED' "
. "AND campaign.advertising_channel_type = 'SHOPPING'";
$gaql_pmax_asset_group_with_url = "SELECT "
. "segments.date, "
. "segments.product_item_id, "
. "segments.product_title, "
. "segments.product_link, "
. "campaign.id, "
. "campaign.name, "
. "campaign.status, "
. "campaign.advertising_channel_type, "
. "asset_group.id, "
. "asset_group.name, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM asset_group_product_group_view "
. "WHERE segments.date = '" . $date . "' "
. "AND campaign.status = 'ENABLED' "
. "AND campaign.advertising_channel_type = 'PERFORMANCE_MAX'";
$gaql_pmax_asset_group = "SELECT "
. "segments.date, "
. "segments.product_item_id, "
. "segments.product_title, "
. "campaign.id, "
. "campaign.name, "
. "campaign.status, "
. "campaign.advertising_channel_type, "
. "asset_group.id, "
. "asset_group.name, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM asset_group_product_group_view "
. "WHERE segments.date = '" . $date . "' "
. "AND campaign.status = 'ENABLED' "
. "AND campaign.advertising_channel_type = 'PERFORMANCE_MAX'";
$gaql_pmax_campaign_level_fallback_with_url = "SELECT "
. "segments.date, "
. "segments.product_item_id, "
. "segments.product_title, "
. "segments.product_link, "
. "campaign.id, "
. "campaign.name, "
. "campaign.status, "
. "campaign.advertising_channel_type, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM shopping_performance_view "
. "WHERE segments.date = '" . $date . "' "
. "AND campaign.status = 'ENABLED' "
. "AND campaign.advertising_channel_type = 'PERFORMANCE_MAX'";
$gaql_pmax_campaign_level_fallback = "SELECT "
. "segments.date, "
. "segments.product_item_id, "
. "segments.product_title, "
. "campaign.id, "
. "campaign.name, "
. "campaign.status, "
. "campaign.advertising_channel_type, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM shopping_performance_view "
. "WHERE segments.date = '" . $date . "' "
. "AND campaign.status = 'ENABLED' "
. "AND campaign.advertising_channel_type = 'PERFORMANCE_MAX'";
$search_with_optional_url = function( $query_with_url, $query_without_url ) use ( $customer_id )
{
$result = $this -> search_stream( $customer_id, $query_with_url );
if ( is_array( $result ) )
{
return $result;
}
return $this -> search_stream( $customer_id, $query_without_url );
};
$results_with_ad_group = $search_with_optional_url( $gaql_with_ad_group_with_url, $gaql_with_ad_group );
$results_pmax_asset_group = $search_with_optional_url( $gaql_pmax_asset_group_with_url, $gaql_pmax_asset_group );
$results_pmax_campaign_fallback = [];
$had_success = false;
if ( is_array( $results_with_ad_group ) )
{
$had_success = true;
}
else
{
$results_with_ad_group = [];
}
if ( is_array( $results_pmax_asset_group ) )
{
$had_success = true;
}
else
{
$results_pmax_asset_group = [];
// Fallback dla kont/API, gdzie asset_group_product_group_view moze nie byc dostepny.
$tmp = $search_with_optional_url( $gaql_pmax_campaign_level_fallback_with_url, $gaql_pmax_campaign_level_fallback );
if ( is_array( $tmp ) )
{
$had_success = true;
foreach ( $tmp as $row )
{
$channel = (string) ( $row['campaign']['advertisingChannelType'] ?? '' );
if ( strtoupper( $channel ) === 'PERFORMANCE_MAX' )
{
$results_pmax_campaign_fallback[] = $row;
}
}
}
}
if ( !$had_success )
{
return false;
}
$products = [];
$collect_rows = function( $rows, $group_kind ) use ( &$products )
{
if ( !is_array( $rows ) )
{
return;
}
foreach ( $rows as $row )
{
$channel_type = strtoupper( trim( (string) ( $row['campaign']['advertisingChannelType'] ?? '' ) ) );
if ( $channel_type === 'SEARCH' )
{
continue;
}
$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 ( $group_kind === '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)';
}
}
else if ( $group_kind === 'asset_group' )
{
$ad_group_id = (int) ( $row['assetGroup']['id'] ?? 0 );
$ad_group_name = trim( (string) ( $row['assetGroup']['name'] ?? '' ) );
if ( $ad_group_id > 0 && $ad_group_name === '' )
{
$ad_group_name = 'Asset group #' . $ad_group_id;
}
else if ( $ad_group_id <= 0 )
{
$ad_group_name = 'PMax (bez grup plikow)';
}
}
$scope_key = $offer_id . '|' . $campaign_id . '|' . $ad_group_id;
if ( !isset( $products[ $scope_key ] ) )
{
$initial_product_url = trim( (string) ( $row['segments']['productLink'] ?? '' ) );
if ( $initial_product_url !== '' && $this -> is_likely_image_url( $initial_product_url ) )
{
$initial_product_url = '';
}
$products[ $scope_key ] = [
'OfferId' => $offer_id,
'ProductTitle' => (string) ( $row['segments']['productTitle'] ?? $offer_id ),
'ProductUrl' => $initial_product_url,
'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
];
}
if ( ( $products[ $scope_key ]['ProductUrl'] ?? '' ) === '' )
{
$candidate_url = trim( (string) ( $row['segments']['productLink'] ?? '' ) );
if ( $candidate_url !== '' && !$this -> is_likely_image_url( $candidate_url ) )
{
$products[ $scope_key ]['ProductUrl'] = $candidate_url;
}
}
$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 );
}
};
$collect_rows( $results_with_ad_group, 'ad_group' );
$collect_rows( $results_pmax_asset_group, 'asset_group' );
$collect_rows( $results_pmax_campaign_fallback, 'campaign' );
$product_urls_by_offer_id = $this -> get_product_urls_by_offer_id( $customer_id, $date );
if ( !empty( $product_urls_by_offer_id ) )
{
foreach ( $products as &$product )
{
$offer_id = trim( (string) ( $product['OfferId'] ?? '' ) );
if ( $offer_id === '' )
{
continue;
}
if ( trim( (string) ( $product['ProductUrl'] ?? '' ) ) !== '' )
{
continue;
}
if ( isset( $product_urls_by_offer_id[ $offer_id ] ) )
{
$product['ProductUrl'] = $product_urls_by_offer_id[ $offer_id ];
}
}
unset( $product );
}
return array_values( $products );
}
private function gaql_field_leaf_to_json_key( $leaf )
{
$leaf = trim( (string) $leaf );
if ( $leaf === '' )
{
return '';
}
$parts = explode( '_', strtolower( $leaf ) );
$key = array_shift( $parts );
foreach ( $parts as $part )
{
$key .= ucfirst( $part );
}
return $key;
}
private function is_likely_image_url( $url )
{
$url = trim( (string) $url );
if ( $url === '' )
{
return false;
}
$path = strtolower( (string) parse_url( $url, PHP_URL_PATH ) );
return (bool) preg_match( '/\.(jpg|jpeg|png|gif|webp|bmp|svg|avif)$/i', $path );
}
private function is_valid_merchant_product_url( $url )
{
$url = trim( (string) $url );
if ( $url === '' )
{
return false;
}
if ( !filter_var( $url, FILTER_VALIDATE_URL ) )
{
return false;
}
return !$this -> is_likely_image_url( $url );
}
private function get_product_urls_by_offer_id( $customer_id, $date )
{
$date = date( 'Y-m-d', strtotime( $date ) );
$last_error_before = self::get_setting( 'google_ads_last_error' );
$url_fields = [
'shopping_product.link',
'shopping_product.product_link',
'shopping_product.product_url',
'shopping_product.landing_page'
];
foreach ( $url_fields as $field )
{
$gaql = "SELECT "
. "shopping_product.item_id, "
. $field . " "
. "FROM shopping_product "
. "WHERE segments.date = '" . $date . "'";
$rows = $this -> search_stream( $customer_id, $gaql );
if ( !is_array( $rows ) )
{
continue;
}
$field_parts = explode( '.', $field );
$leaf = end( $field_parts );
$json_key = $this -> gaql_field_leaf_to_json_key( $leaf );
$map = [];
foreach ( $rows as $row )
{
$item_id = trim( (string) ( $row['shoppingProduct']['itemId'] ?? '' ) );
if ( $item_id === '' )
{
continue;
}
$url = trim( (string) ( $row['shoppingProduct'][ $json_key ] ?? '' ) );
if ( $url === '' || !filter_var( $url, FILTER_VALIDATE_URL ) || $this -> is_likely_image_url( $url ) )
{
continue;
}
if ( !isset( $map[ $item_id ] ) )
{
$map[ $item_id ] = $url;
}
}
if ( !empty( $map ) )
{
return $map;
}
}
self::set_setting( 'google_ads_last_error', $last_error_before );
return [];
}
public function get_campaigns_30_days( $customer_id, $as_of_date = null )
{
$as_of_date = $as_of_date ? date( 'Y-m-d', strtotime( $as_of_date ) ) : date( 'Y-m-d' );
$date_from = date( 'Y-m-d', strtotime( '-29 days', strtotime( $as_of_date ) ) );
$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 >= '" . $date_from . "' "
. "AND segments.date <= '" . $as_of_date . "'";
$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, $as_of_date = null )
{
$as_of_date = $as_of_date ? date( 'Y-m-d', strtotime( $as_of_date ) ) : null;
$gaql_base = "SELECT "
. "campaign.id, "
. "metrics.cost_micros, "
. "metrics.conversions_value "
. "FROM campaign "
. "WHERE campaign.status = 'ENABLED'";
$results = false;
if ( $as_of_date )
{
$gaql_with_date = $gaql_base . " AND segments.date <= '" . $as_of_date . "'";
$results = $this -> search_stream( $customer_id, $gaql_with_date );
// Fallback do starego sposobu, gdy filtr daty nie jest akceptowany na danym koncie.
if ( $results === false )
{
$results = $this -> search_stream( $customer_id, $gaql_base );
}
}
else
{
$results = $this -> search_stream( $customer_id, $gaql_base );
}
if ( $results === false ) return false;
$campaigns = [];
foreach ( $results as $row )
{
$cid = $row['campaign']['id'] ?? null;
if ( !$cid ) continue;
if ( !isset( $campaigns[ $cid ] ) )
{
$campaigns[ $cid ] = [
'campaign_id' => $cid,
'cost_micros_total' => 0.0,
'conversion_value_total' => 0.0
];
}
$campaigns[ $cid ]['cost_micros_total'] += (float) ( $row['metrics']['costMicros'] ?? 0 );
$campaigns[ $cid ]['conversion_value_total'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 );
}
foreach ( $campaigns as &$campaign )
{
$cost = $campaign['cost_micros_total'] / 1000000;
$value = $campaign['conversion_value_total'];
$campaign = [
'campaign_id' => $campaign['campaign_id'],
'cost_all_time' => $cost,
'conversion_value_all_time' => $value,
'roas_all_time' => ( $cost > 0 ) ? round( ( $value / $cost ) * 100, 2 ) : 0
];
}
return array_values( $campaigns );
}
public function get_ad_groups_30_days( $customer_id, $as_of_date = null )
{
$as_of_date = $as_of_date ? date( 'Y-m-d', strtotime( $as_of_date ) ) : date( 'Y-m-d' );
$date_from = date( 'Y-m-d', strtotime( '-29 days', strtotime( $as_of_date ) ) );
$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 >= '" . $date_from . "' "
. "AND segments.date <= '" . $as_of_date . "'";
$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, $as_of_date = null )
{
$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 = false;
if ( $as_of_date )
{
$as_of_date = date( 'Y-m-d', strtotime( $as_of_date ) );
$gaql_with_date = $gaql . " AND segments.date <= '" . $as_of_date . "'";
$results = $this -> search_stream( $customer_id, $gaql_with_date );
// Fallback gdy konto nie akceptuje filtra daty na all-time.
if ( $results === false )
{
$results = $this -> search_stream( $customer_id, $gaql );
}
}
else
{
$results = $this -> search_stream( $customer_id, $gaql );
}
if ( $results === false ) return false;
return $this -> aggregate_ad_groups( $results );
}
public function get_shopping_ad_group_offer_ids( $customer_id )
{
$query_variants = [
"SELECT campaign.id, campaign.name, ad_group.id, ad_group.name, ad_group_criterion.listing_group.case_value FROM ad_group_criterion WHERE campaign.status != 'REMOVED' AND ad_group.status != 'REMOVED' AND campaign.advertising_channel_type = 'SHOPPING' AND ad_group_criterion.type = 'LISTING_GROUP'",
"SELECT campaign.id, campaign.name, ad_group.id, ad_group.name, ad_group_criterion.listing_group.case_value.product_item FROM ad_group_criterion WHERE campaign.status != 'REMOVED' AND ad_group.status != 'REMOVED' AND campaign.advertising_channel_type = 'SHOPPING' AND ad_group_criterion.type = 'LISTING_GROUP'",
"SELECT campaign.id, campaign.name, ad_group.id, ad_group.name, ad_group_criterion.listing_group.case_value.product_item.id FROM ad_group_criterion WHERE campaign.status != 'REMOVED' AND ad_group.status != 'REMOVED' AND campaign.advertising_channel_type = 'SHOPPING' AND ad_group_criterion.type = 'LISTING_GROUP'",
"SELECT campaign.id, campaign.name, ad_group.id, ad_group.name, ad_group_criterion.listing_group.case_value.product_item.value FROM ad_group_criterion WHERE campaign.status != 'REMOVED' AND ad_group.status != 'REMOVED' AND campaign.advertising_channel_type = 'SHOPPING' AND ad_group_criterion.type = 'LISTING_GROUP'",
"SELECT campaign.id, campaign.name, ad_group.id, ad_group.name, product_group_view.resource_name, ad_group_criterion.listing_group.case_value FROM product_group_view WHERE campaign.status != 'REMOVED' AND ad_group.status != 'REMOVED' AND campaign.advertising_channel_type = 'SHOPPING'",
"SELECT campaign.id, campaign.name, ad_group.id, ad_group.name, product_group_view.resource_name, ad_group_criterion.listing_group.case_value.product_item FROM product_group_view WHERE campaign.status != 'REMOVED' AND ad_group.status != 'REMOVED' AND campaign.advertising_channel_type = 'SHOPPING'"
];
$results = false;
$last_error = '';
$variant_errors = [];
foreach ( $query_variants as $index => $gaql )
{
$tmp = $this -> search_stream( $customer_id, $gaql );
if ( is_array( $tmp ) )
{
$results = $tmp;
break;
}
$last_error = (string) self::get_setting( 'google_ads_last_error' );
$variant_errors[] = 'V' . ( $index + 1 ) . ': ' . $last_error;
}
if ( !is_array( $results ) )
{
$diag = implode( ' || ', array_filter( $variant_errors ) );
$error_to_save = $diag !== '' ? $diag : $last_error;
if ( $error_to_save !== '' )
{
self::set_setting( 'google_ads_last_error', $error_to_save );
}
return false;
}
if ( !is_array( $results ) )
{
return [];
}
$collect_scalar_values = function( $value ) use ( &$collect_scalar_values )
{
$collected = [];
if ( is_array( $value ) )
{
foreach ( $value as $nested )
{
$collected = array_merge( $collected, $collect_scalar_values( $nested ) );
}
return $collected;
}
if ( is_scalar( $value ) )
{
$tmp = trim( (string) $value );
if ( $tmp !== '' )
{
$collected[] = $tmp;
}
}
return $collected;
};
$extract_offer_ids = function( $row ) use ( $collect_scalar_values )
{
$candidates = [];
$case_value = $row['productGroupView']['caseValue']
?? $row['product_group_view']['case_value']
?? $row['adGroupCriterion']['listingGroup']['caseValue']
?? $row['ad_group_criterion']['listing_group']['case_value']
?? [];
if ( is_array( $case_value ) )
{
if ( isset( $case_value['productItem'] ) )
{
$candidates = array_merge( $candidates, $collect_scalar_values( $case_value['productItem'] ) );
}
if ( isset( $case_value['product_item'] ) )
{
$candidates = array_merge( $candidates, $collect_scalar_values( $case_value['product_item'] ) );
}
if ( isset( $case_value['productItemId'] ) )
{
$candidates[] = trim( (string) $case_value['productItemId'] );
}
if ( isset( $case_value['product_item_id'] ) )
{
$candidates[] = trim( (string) $case_value['product_item_id'] );
}
}
$direct_candidates = [
$row['adGroupCriterion']['listingGroup']['caseValue']['productItem'] ?? null,
$row['ad_group_criterion']['listing_group']['case_value']['product_item'] ?? null,
$row['adGroupCriterion']['listingGroup']['caseValue']['productItemId'] ?? null,
$row['ad_group_criterion']['listing_group']['case_value']['product_item_id'] ?? null,
$row['productGroupView']['caseValue']['productItem'] ?? null,
$row['product_group_view']['case_value']['product_item'] ?? null,
];
foreach ( $direct_candidates as $dc )
{
if ( $dc === null )
{
continue;
}
$candidates = array_merge( $candidates, $collect_scalar_values( $dc ) );
}
$candidates = array_values( array_unique( array_filter( array_map( function( $item )
{
return trim( (string) $item );
}, $candidates ) ) ) );
return $candidates;
};
$scopes = [];
foreach ( $results as $row )
{
$campaign_id = (int) ( $row['campaign']['id'] ?? $row['campaignId'] ?? 0 );
$ad_group_id = (int) ( $row['adGroup']['id'] ?? $row['ad_group']['id'] ?? $row['adGroupId'] ?? 0 );
if ( $campaign_id <= 0 || $ad_group_id <= 0 )
{
continue;
}
$offer_ids = $extract_offer_ids( $row );
if ( empty( $offer_ids ) )
{
continue;
}
$scope_key = $campaign_id . '|' . $ad_group_id;
if ( !isset( $scopes[ $scope_key ] ) )
{
$scopes[ $scope_key ] = [
'campaign_id' => $campaign_id,
'campaign_name' => trim( (string) ( $row['campaign']['name'] ?? '' ) ),
'ad_group_id' => $ad_group_id,
'ad_group_name' => trim( (string) ( $row['adGroup']['name'] ?? $row['ad_group']['name'] ?? '' ) ),
'offer_ids' => []
];
}
foreach ( $offer_ids as $offer_id )
{
$scopes[ $scope_key ]['offer_ids'][ $offer_id ] = true;
}
}
foreach ( $scopes as &$scope )
{
$scope['offer_ids'] = array_values( array_keys( (array) $scope['offer_ids'] ) );
}
unset( $scope );
return array_values( $scopes );
}
public function get_shopping_ad_group_offer_ids_from_shopping_product( $customer_id )
{
$gaql = "SELECT "
. "campaign.id, "
. "campaign.name, "
. "campaign.status, "
. "campaign.advertising_channel_type, "
. "ad_group.id, "
. "ad_group.name, "
. "ad_group.status, "
. "shopping_product.item_id "
. "FROM shopping_product "
. "WHERE campaign.status != 'REMOVED' "
. "AND ad_group.status != 'REMOVED' "
. "AND campaign.advertising_channel_type = 'SHOPPING' "
. "AND segments.date DURING LAST_30_DAYS";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false )
{
return false;
}
if ( !is_array( $results ) )
{
return [];
}
$scopes = [];
foreach ( $results as $row )
{
$campaign_id = (int) ( $row['campaign']['id'] ?? 0 );
$ad_group_id = (int) ( $row['adGroup']['id'] ?? 0 );
$offer_id = trim( (string) ( $row['shoppingProduct']['itemId'] ?? '' ) );
if ( $campaign_id <= 0 || $ad_group_id <= 0 || $offer_id === '' )
{
continue;
}
$scope_key = $campaign_id . '|' . $ad_group_id;
if ( !isset( $scopes[ $scope_key ] ) )
{
$scopes[ $scope_key ] = [
'campaign_id' => $campaign_id,
'campaign_name' => trim( (string) ( $row['campaign']['name'] ?? '' ) ),
'ad_group_id' => $ad_group_id,
'ad_group_name' => trim( (string) ( $row['adGroup']['name'] ?? '' ) ),
'offer_ids' => []
];
}
$scopes[ $scope_key ]['offer_ids'][ $offer_id ] = true;
}
foreach ( $scopes as &$scope )
{
$scope['offer_ids'] = array_values( array_keys( (array) $scope['offer_ids'] ) );
}
unset( $scope );
return array_values( $scopes );
}
public function get_shopping_ad_group_offer_ids_from_performance( $customer_id )
{
$gaql = "SELECT "
. "campaign.id, "
. "campaign.name, "
. "campaign.status, "
. "campaign.advertising_channel_type, "
. "ad_group.id, "
. "ad_group.name, "
. "ad_group.status, "
. "segments.product_item_id, "
. "metrics.impressions "
. "FROM shopping_performance_view "
. "WHERE campaign.status != 'REMOVED' "
. "AND ad_group.status != 'REMOVED' "
. "AND campaign.advertising_channel_type = 'SHOPPING'";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false )
{
return false;
}
if ( !is_array( $results ) )
{
return [];
}
$scopes = [];
foreach ( $results as $row )
{
$campaign_id = (int) ( $row['campaign']['id'] ?? 0 );
$ad_group_id = (int) ( $row['adGroup']['id'] ?? 0 );
$offer_id = trim( (string) ( $row['segments']['productItemId'] ?? '' ) );
if ( $campaign_id <= 0 || $ad_group_id <= 0 || $offer_id === '' )
{
continue;
}
$scope_key = $campaign_id . '|' . $ad_group_id;
if ( !isset( $scopes[ $scope_key ] ) )
{
$scopes[ $scope_key ] = [
'campaign_id' => $campaign_id,
'campaign_name' => trim( (string) ( $row['campaign']['name'] ?? '' ) ),
'ad_group_id' => $ad_group_id,
'ad_group_name' => trim( (string) ( $row['adGroup']['name'] ?? '' ) ),
'offer_ids' => []
];
}
$scopes[ $scope_key ]['offer_ids'][ $offer_id ] = true;
}
foreach ( $scopes as &$scope )
{
$scope['offer_ids'] = array_values( array_keys( (array) $scope['offer_ids'] ) );
}
unset( $scope );
return array_values( $scopes );
}
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;
}
public function get_ad_keywords_30_days( $customer_id )
{
$gaql = "SELECT "
. "campaign.id, "
. "ad_group.id, "
. "ad_group_criterion.keyword.text, "
. "ad_group_criterion.keyword.match_type, "
. "ad_group_criterion.status, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM keyword_view "
. "WHERE campaign.status != 'REMOVED' "
. "AND ad_group.status != 'REMOVED' "
. "AND ad_group_criterion.negative = FALSE "
. "AND ad_group_criterion.status != 'REMOVED' "
. "AND campaign.advertising_channel_type = 'SEARCH' "
. "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_ad_keywords( $results );
}
public function get_ad_keywords_all_time( $customer_id )
{
$gaql = "SELECT "
. "campaign.id, "
. "ad_group.id, "
. "ad_group_criterion.keyword.text, "
. "ad_group_criterion.keyword.match_type, "
. "ad_group_criterion.status, "
. "metrics.impressions, "
. "metrics.clicks, "
. "metrics.cost_micros, "
. "metrics.conversions, "
. "metrics.conversions_value "
. "FROM keyword_view "
. "WHERE campaign.status != 'REMOVED' "
. "AND ad_group.status != 'REMOVED' "
. "AND ad_group_criterion.negative = FALSE "
. "AND ad_group_criterion.status != 'REMOVED' "
. "AND campaign.advertising_channel_type = 'SEARCH' "
. "AND metrics.clicks > 0";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false ) return false;
return $this -> aggregate_ad_keywords( $results );
}
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_search_terms_for_date( $customer_id, $date )
{
$date = date( 'Y-m-d', strtotime( $date ) );
$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 = '{$date}'";
$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_for_date( $customer_id, $date );
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_for_date( $customer_id, $date )
{
$date = date( 'Y-m-d', strtotime( $date ) );
$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 = '{$date}'";
$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 );
}
private function aggregate_ad_keywords( $results )
{
$keywords = [];
foreach ( $results as $row )
{
$campaign_id = $row['campaign']['id'] ?? null;
$ad_group_id = $row['adGroup']['id'] ?? null;
$keyword_text = trim( (string) ( $row['adGroupCriterion']['keyword']['text'] ?? '' ) );
$match_type = trim( (string) ( $row['adGroupCriterion']['keyword']['matchType'] ?? '' ) );
$status = trim( (string) ( $row['adGroupCriterion']['status'] ?? 'ENABLED' ) );
if ( !$campaign_id || !$ad_group_id || $keyword_text === '' )
{
continue;
}
$key = $campaign_id . '|' . $ad_group_id . '|' . strtolower( $keyword_text ) . '|' . strtolower( $match_type );
if ( !isset( $keywords[ $key ] ) )
{
$keywords[ $key ] = [
'campaign_id' => (int) $campaign_id,
'ad_group_id' => (int) $ad_group_id,
'keyword_text' => $keyword_text,
'match_type' => $match_type,
'status' => $status,
'impressions' => 0,
'clicks' => 0,
'cost' => 0.0,
'conversions' => 0.0,
'conversion_value' => 0.0,
'roas' => 0.0,
];
}
$keywords[ $key ]['impressions'] += (int) ( $row['metrics']['impressions'] ?? 0 );
$keywords[ $key ]['clicks'] += (int) ( $row['metrics']['clicks'] ?? 0 );
$keywords[ $key ]['cost'] += (float) ( ( $row['metrics']['costMicros'] ?? 0 ) / 1000000 );
$keywords[ $key ]['conversions'] += (float) ( $row['metrics']['conversions'] ?? 0 );
$keywords[ $key ]['conversion_value'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 );
}
foreach ( $keywords as &$item )
{
$item['roas'] = $item['cost'] > 0
? round( ( $item['conversion_value'] / $item['cost'] ) * 100, 2 )
: 0;
}
unset( $item );
return array_values( $keywords );
}
}