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 ); } }