- 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.
275 lines
8.3 KiB
PHP
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;
|
|
}
|
|
}
|