Files
adsPRO/autoload/services/class.GoogleAdsApi.php
Jacek Pyziak afe9d6216d Add migrations for Google Ads settings and demo data
- Create migration for global settings table and add google_ads_customer_id and google_ads_start_date columns to clients table.
- Add migration to include product_url column in products_data table.
- Insert demo data for campaigns, products, and their history for client 'pomysloweprezenty.pl'.
- Implement client management interface with modals for adding and editing clients, including Google Ads Customer ID and data retrieval start date.
2026-02-15 17:46:32 +01:00

275 lines
8.3 KiB
PHP

<?php
namespace services;
class GoogleAdsApi
{
private $developer_token;
private $client_id;
private $client_secret;
private $refresh_token;
private $manager_account_id;
private $access_token;
private static $API_VERSION = 'v23';
private static $TOKEN_URL = 'https://oauth2.googleapis.com/token';
private static $ADS_BASE_URL = 'https://googleads.googleapis.com';
public function __construct()
{
$this -> developer_token = self::get_setting( 'google_ads_developer_token' );
$this -> client_id = self::get_setting( 'google_ads_client_id' );
$this -> client_secret = self::get_setting( 'google_ads_client_secret' );
$this -> refresh_token = self::get_setting( 'google_ads_refresh_token' );
$this -> manager_account_id = self::get_setting( 'google_ads_manager_account_id' );
}
// --- Settings CRUD ---
public static function get_setting( $key )
{
global $mdb;
return $mdb -> get( 'settings', 'setting_value', [ 'setting_key' => $key ] );
}
public static function set_setting( $key, $value )
{
global $mdb;
if ( $mdb -> count( 'settings', [ 'setting_key' => $key ] ) )
{
$mdb -> update( 'settings', [ 'setting_value' => $value ], [ 'setting_key' => $key ] );
}
else
{
$mdb -> insert( 'settings', [ 'setting_key' => $key, 'setting_value' => $value ] );
}
}
// --- Konfiguracja ---
public function is_configured()
{
return !empty( $this -> developer_token )
&& !empty( $this -> client_id )
&& !empty( $this -> client_secret )
&& !empty( $this -> refresh_token );
}
// --- OAuth2 ---
private function get_access_token()
{
$cached_token = self::get_setting( 'google_ads_access_token' );
$cached_expires = (int) self::get_setting( 'google_ads_access_token_expires' );
if ( $cached_token && $cached_expires > time() )
{
$this -> access_token = $cached_token;
return $this -> access_token;
}
return $this -> refresh_access_token();
}
private function refresh_access_token()
{
$ch = curl_init( self::$TOKEN_URL );
curl_setopt_array( $ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query( [
'client_id' => $this -> client_id,
'client_secret' => $this -> client_secret,
'refresh_token' => $this -> refresh_token,
'grant_type' => 'refresh_token'
] ),
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 30,
] );
$response = curl_exec( $ch );
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
$error = curl_error( $ch );
curl_close( $ch );
if ( $http_code !== 200 || !$response )
{
self::set_setting( 'google_ads_last_error', 'Token refresh failed: HTTP ' . $http_code . ' | ' . $error . ' | ' . $response );
return false;
}
$data = json_decode( $response, true );
if ( !isset( $data['access_token'] ) )
{
self::set_setting( 'google_ads_last_error', 'Token refresh: brak access_token w odpowiedzi' );
return false;
}
$this -> access_token = $data['access_token'];
$expires_at = time() + ( $data['expires_in'] ?? 3600 ) - 60;
self::set_setting( 'google_ads_access_token', $this -> access_token );
self::set_setting( 'google_ads_access_token_expires', $expires_at );
self::set_setting( 'google_ads_last_error', null );
return $this -> access_token;
}
// --- Google Ads API ---
public function search_stream( $customer_id, $gaql_query )
{
$access_token = $this -> get_access_token();
if ( !$access_token ) return false;
$customer_id = str_replace( '-', '', $customer_id );
$url = self::$ADS_BASE_URL . '/' . self::$API_VERSION
. '/customers/' . $customer_id . '/googleAds:searchStream';
$headers = [
'Authorization: Bearer ' . $access_token,
'developer-token: ' . $this -> developer_token,
'Content-Type: application/json',
];
if ( !empty( $this -> manager_account_id ) )
{
$headers[] = 'login-customer-id: ' . str_replace( '-', '', $this -> manager_account_id );
}
$ch = curl_init( $url );
curl_setopt_array( $ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => json_encode( [ 'query' => $gaql_query ] ),
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_TIMEOUT => 120,
] );
$response = curl_exec( $ch );
$http_code = curl_getinfo( $ch, CURLINFO_HTTP_CODE );
$error = curl_error( $ch );
curl_close( $ch );
if ( $http_code !== 200 || !$response )
{
self::set_setting( 'google_ads_last_error', 'searchStream failed: HTTP ' . $http_code . ' | ' . $error . ' | ' . substr( $response, 0, 500 ) );
return false;
}
$data = json_decode( $response, true );
// searchStream zwraca tablicę batch'y, każdy z kluczem 'results'
$results = [];
if ( is_array( $data ) )
{
foreach ( $data as $batch )
{
if ( isset( $batch['results'] ) && is_array( $batch['results'] ) )
{
$results = array_merge( $results, $batch['results'] );
}
}
}
self::set_setting( 'google_ads_last_error', null );
return $results;
}
// --- Kampanie: dane 30-dniowe ---
public function get_campaigns_30_days( $customer_id )
{
$gaql = "SELECT "
. "campaign.id, "
. "campaign.name, "
. "campaign.bidding_strategy_type, "
. "campaign.target_roas.target_roas, "
. "campaign_budget.amount_micros, "
. "metrics.cost_micros, "
. "metrics.conversions_value "
. "FROM campaign "
. "WHERE campaign.status = 'ENABLED' "
. "AND segments.date DURING LAST_30_DAYS";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false ) return false;
// Agregacja po campaign.id (API zwraca wiersz per dzień per kampania)
$campaigns = [];
foreach ( $results as $row )
{
$cid = $row['campaign']['id'] ?? null;
if ( !$cid ) continue;
if ( !isset( $campaigns[ $cid ] ) )
{
$campaigns[ $cid ] = [
'campaign_id' => $cid,
'campaign_name' => $row['campaign']['name'] ?? '',
'bidding_strategy' => $row['campaign']['biddingStrategyType'] ?? 'UNKNOWN',
'target_roas' => isset( $row['campaign']['targetRoas']['targetRoas'] )
? (float) $row['campaign']['targetRoas']['targetRoas']
: 0,
'budget' => isset( $row['campaignBudget']['amountMicros'] )
? (float) $row['campaignBudget']['amountMicros'] / 1000000
: 0,
'cost_total' => 0,
'conversion_value' => 0,
];
}
$campaigns[ $cid ]['cost_total'] += (float) ( $row['metrics']['costMicros'] ?? 0 );
$campaigns[ $cid ]['conversion_value'] += (float) ( $row['metrics']['conversionsValue'] ?? 0 );
}
// Przeliczenie micros i ROAS
foreach ( $campaigns as &$c )
{
$c['money_spent'] = $c['cost_total'] / 1000000;
$c['roas_30_days'] = ( $c['money_spent'] > 0 )
? round( ( $c['conversion_value'] / $c['money_spent'] ) * 100, 2 )
: 0;
unset( $c['cost_total'] );
}
return array_values( $campaigns );
}
// --- Kampanie: dane all-time ---
public function get_campaigns_all_time( $customer_id )
{
$gaql = "SELECT "
. "campaign.id, "
. "metrics.cost_micros, "
. "metrics.conversions_value "
. "FROM campaign "
. "WHERE campaign.status = 'ENABLED'";
$results = $this -> search_stream( $customer_id, $gaql );
if ( $results === false ) return false;
$campaigns = [];
foreach ( $results as $row )
{
$cid = $row['campaign']['id'] ?? null;
if ( !$cid ) continue;
$cost = (float) ( $row['metrics']['costMicros'] ?? 0 ) / 1000000;
$value = (float) ( $row['metrics']['conversionsValue'] ?? 0 );
$campaigns[] = [
'campaign_id' => $cid,
'roas_all_time' => ( $cost > 0 ) ? round( ( $value / $cost ) * 100, 2 ) : 0,
];
}
return $campaigns;
}
}