- Implemented a new PHP script to retrieve insights for the last N days (default 30). - Supports command-line options for token, account ID, days, API version, and output file. - Fetches data at campaign, adset, and ad levels, with filtering for active statuses. - Handles JSON output and optional file saving, including directory creation if necessary. - Includes error handling for cURL requests and JSON responses.
411 lines
11 KiB
PHP
411 lines
11 KiB
PHP
<?php
|
|
namespace services;
|
|
|
|
class FacebookAdsApi
|
|
{
|
|
private $access_token;
|
|
private $api_version;
|
|
|
|
public function __construct( $access_token = '', $api_version = 'v25.0' )
|
|
{
|
|
$this -> access_token = trim( (string) $access_token );
|
|
$this -> api_version = trim( (string) $api_version ) ?: 'v25.0';
|
|
}
|
|
|
|
public function is_configured()
|
|
{
|
|
return $this -> access_token !== '';
|
|
}
|
|
|
|
public function get_api_version()
|
|
{
|
|
return $this -> api_version;
|
|
}
|
|
|
|
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 === 'facebook_ads_last_error' )
|
|
{
|
|
$error_at = null;
|
|
if ( $value !== null && trim( (string) $value ) !== '' )
|
|
{
|
|
$error_at = date( 'Y-m-d H:i:s' );
|
|
}
|
|
|
|
if ( $mdb -> count( 'settings', [ 'setting_key' => 'facebook_ads_last_error_at' ] ) )
|
|
{
|
|
$mdb -> update( 'settings', [ 'setting_value' => $error_at ], [ 'setting_key' => 'facebook_ads_last_error_at' ] );
|
|
}
|
|
else
|
|
{
|
|
$mdb -> insert( 'settings', [ 'setting_key' => 'facebook_ads_last_error_at', 'setting_value' => $error_at ] );
|
|
}
|
|
}
|
|
}
|
|
|
|
public static function normalize_ad_account_id( $account_id )
|
|
{
|
|
$account_id = trim( (string) $account_id );
|
|
if ( $account_id === '' )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if ( stripos( $account_id, 'act_' ) === 0 )
|
|
{
|
|
$digits = preg_replace( '/\D+/', '', substr( $account_id, 4 ) );
|
|
return $digits !== '' ? 'act_' . $digits : null;
|
|
}
|
|
|
|
$digits = preg_replace( '/\D+/', '', $account_id );
|
|
if ( $digits === '' )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return 'act_' . $digits;
|
|
}
|
|
|
|
public function fetch_active_insights_last_days( $account_id, $days = 30, $active_only = true )
|
|
{
|
|
$days = max( 1, min( 90, (int) $days ) );
|
|
$since = date( 'Y-m-d', strtotime( '-' . ( $days - 1 ) . ' days' ) );
|
|
$until = date( 'Y-m-d' );
|
|
|
|
return $this -> fetch_active_insights_range( $account_id, $since, $until, $active_only, $days );
|
|
}
|
|
|
|
public function fetch_active_insights_for_date( $account_id, $date, $active_only = true )
|
|
{
|
|
$timestamp = strtotime( (string) $date );
|
|
if ( !$timestamp )
|
|
{
|
|
self::set_setting( 'facebook_ads_last_error', 'Niepoprawna data dla synchronizacji Facebook Ads.' );
|
|
return false;
|
|
}
|
|
|
|
$sync_date = date( 'Y-m-d', $timestamp );
|
|
return $this -> fetch_active_insights_range( $account_id, $sync_date, $sync_date, $active_only, 1 );
|
|
}
|
|
|
|
public function fetch_active_insights_for_range( $account_id, $since, $until, $active_only = true )
|
|
{
|
|
$since_ts = strtotime( (string) $since );
|
|
$until_ts = strtotime( (string) $until );
|
|
if ( !$since_ts || !$until_ts )
|
|
{
|
|
self::set_setting( 'facebook_ads_last_error', 'Niepoprawny zakres dat dla synchronizacji Facebook Ads.' );
|
|
return false;
|
|
}
|
|
|
|
$days = (int) floor( ( $until_ts - $since_ts ) / 86400 ) + 1;
|
|
return $this -> fetch_active_insights_range( $account_id, date( 'Y-m-d', $since_ts ), date( 'Y-m-d', $until_ts ), $active_only, $days, true );
|
|
}
|
|
|
|
public function fetch_campaigns_all_time( $account_id, $active_only = true )
|
|
{
|
|
$account_id = self::normalize_ad_account_id( $account_id );
|
|
if ( !$account_id )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( !$this -> is_configured() )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
$base_url = 'https://graph.facebook.com/' . rawurlencode( $this -> api_version ) . '/' . rawurlencode( $account_id ) . '/insights';
|
|
|
|
$params = [
|
|
'access_token' => $this -> access_token,
|
|
'level' => 'campaign',
|
|
'fields' => 'campaign_id,spend,action_values,purchase_roas',
|
|
'date_preset' => 'maximum',
|
|
'limit' => 500
|
|
];
|
|
|
|
if ( $active_only )
|
|
{
|
|
$params['filtering'] = json_encode( [
|
|
[
|
|
'field' => 'campaign.effective_status',
|
|
'operator' => 'IN',
|
|
'value' => [ 'ACTIVE' ]
|
|
]
|
|
] );
|
|
}
|
|
|
|
$rows = $this -> fetch_all_pages( $base_url, $params );
|
|
if ( $rows === false )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
$purchase_action_types = [
|
|
'purchase', 'omni_purchase', 'offsite_conversion.fb_pixel_purchase',
|
|
'web_in_store_purchase', 'onsite_conversion.purchase', 'app_custom_event.fb_mobile_purchase'
|
|
];
|
|
|
|
$campaigns = [];
|
|
foreach ( $rows as $row )
|
|
{
|
|
$campaign_id = (string) ( $row['campaign_id'] ?? '' );
|
|
if ( $campaign_id === '' )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
$spend = (float) ( $row['spend'] ?? 0 );
|
|
|
|
// 1. ROAS z purchase_roas (preferowane)
|
|
$roas = 0.0;
|
|
if ( isset( $row['purchase_roas'] ) && is_array( $row['purchase_roas'] ) )
|
|
{
|
|
foreach ( $row['purchase_roas'] as $pr )
|
|
{
|
|
$at = trim( (string) ( $pr['action_type'] ?? '' ) );
|
|
if ( in_array( $at, $purchase_action_types, true ) )
|
|
{
|
|
$roas = round( (float) ( $pr['value'] ?? 0 ) * 100, 6 );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Fallback: oblicz z action_values / spend
|
|
$conversion_value = 0.0;
|
|
if ( isset( $row['action_values'] ) && is_array( $row['action_values'] ) )
|
|
{
|
|
foreach ( $row['action_values'] as $action )
|
|
{
|
|
$at = trim( (string) ( $action['action_type'] ?? '' ) );
|
|
if ( in_array( $at, $purchase_action_types, true ) )
|
|
{
|
|
$conversion_value += (float) ( $action['value'] ?? 0 );
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( $roas <= 0 && $spend > 0 && $conversion_value > 0 )
|
|
{
|
|
$roas = round( ( $conversion_value / $spend ) * 100, 6 );
|
|
}
|
|
|
|
$campaigns[ $campaign_id ] = [
|
|
'campaign_id' => $campaign_id,
|
|
'spend_all_time' => $spend,
|
|
'conversion_value_all_time' => $conversion_value,
|
|
'roas_all_time' => $roas
|
|
];
|
|
}
|
|
|
|
return $campaigns;
|
|
}
|
|
|
|
private function fetch_active_insights_range( $account_id, $since, $until, $active_only = true, $days = 0, $aggregate = false )
|
|
{
|
|
$account_id = self::normalize_ad_account_id( $account_id );
|
|
if ( !$account_id )
|
|
{
|
|
self::set_setting( 'facebook_ads_last_error', 'Niepoprawne Facebook Ads Account ID.' );
|
|
return false;
|
|
}
|
|
|
|
if ( !$this -> is_configured() )
|
|
{
|
|
self::set_setting( 'facebook_ads_last_error', 'Brak tokena Facebook Ads API.' );
|
|
return false;
|
|
}
|
|
|
|
$since_ts = strtotime( (string) $since );
|
|
$until_ts = strtotime( (string) $until );
|
|
if ( !$since_ts || !$until_ts || $since_ts > $until_ts )
|
|
{
|
|
self::set_setting( 'facebook_ads_last_error', 'Niepoprawny zakres dat Facebook Ads.' );
|
|
return false;
|
|
}
|
|
|
|
$since = date( 'Y-m-d', $since_ts );
|
|
$until = date( 'Y-m-d', $until_ts );
|
|
if ( (int) $days <= 0 )
|
|
{
|
|
$days = (int) floor( ( $until_ts - $since_ts ) / 86400 ) + 1;
|
|
}
|
|
$days = max( 1, min( 90, (int) $days ) );
|
|
|
|
$base_url = 'https://graph.facebook.com/' . rawurlencode( $this -> api_version ) . '/' . rawurlencode( $account_id ) . '/insights';
|
|
|
|
$levels = [
|
|
'campaign' => [
|
|
'fields' => 'account_id,campaign_id,campaign_name,spend,impressions,clicks,ctr,cpc,action_values,purchase_roas,date_start,date_stop',
|
|
'filtering_field' => 'campaign.effective_status'
|
|
],
|
|
'adset' => [
|
|
'fields' => 'account_id,campaign_id,campaign_name,adset_id,adset_name,spend,impressions,clicks,ctr,cpc,action_values,purchase_roas,date_start,date_stop',
|
|
'filtering_field' => 'adset.effective_status'
|
|
],
|
|
'ad' => [
|
|
'fields' => 'account_id,campaign_id,campaign_name,adset_id,adset_name,ad_id,ad_name,spend,impressions,clicks,ctr,cpc,action_values,purchase_roas,date_start,date_stop',
|
|
'filtering_field' => 'ad.effective_status'
|
|
]
|
|
];
|
|
|
|
$result = [
|
|
'meta' => [
|
|
'account_id' => $account_id,
|
|
'api_version' => $this -> api_version,
|
|
'days' => $days,
|
|
'since' => $since,
|
|
'until' => $until,
|
|
'generated_at' => date( 'c' )
|
|
],
|
|
'campaign' => [],
|
|
'adset' => [],
|
|
'ad' => []
|
|
];
|
|
|
|
foreach ( $levels as $level => $cfg )
|
|
{
|
|
$params = [
|
|
'access_token' => $this -> access_token,
|
|
'level' => $level,
|
|
'fields' => $cfg['fields'],
|
|
'time_range' => json_encode( [ 'since' => $since, 'until' => $until ] ),
|
|
'limit' => 500
|
|
];
|
|
|
|
if ( !$aggregate )
|
|
{
|
|
$params['time_increment'] = 1;
|
|
}
|
|
|
|
if ( $active_only )
|
|
{
|
|
$params['filtering'] = json_encode( [
|
|
[
|
|
'field' => $cfg['filtering_field'],
|
|
'operator' => 'IN',
|
|
'value' => [ 'ACTIVE' ]
|
|
]
|
|
] );
|
|
}
|
|
|
|
$rows = $this -> fetch_all_pages( $base_url, $params );
|
|
if ( $rows === false )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
$result[ $level ] = $rows;
|
|
}
|
|
|
|
self::set_setting( 'facebook_ads_last_error', null );
|
|
return $result;
|
|
}
|
|
|
|
private function fetch_all_pages( $url, $params = null )
|
|
{
|
|
$all_rows = [];
|
|
$next_url = $url;
|
|
$next_params = $params;
|
|
|
|
while ( $next_url )
|
|
{
|
|
$payload = $this -> request_json( $next_url, $next_params );
|
|
if ( $payload === false )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ( isset( $payload['data'] ) && is_array( $payload['data'] ) )
|
|
{
|
|
foreach ( $payload['data'] as $row )
|
|
{
|
|
$all_rows[] = $row;
|
|
}
|
|
}
|
|
|
|
$next_url = '';
|
|
$next_params = null;
|
|
|
|
if ( isset( $payload['paging']['next'] ) && is_string( $payload['paging']['next'] ) )
|
|
{
|
|
$next_url = $payload['paging']['next'];
|
|
}
|
|
}
|
|
|
|
return $all_rows;
|
|
}
|
|
|
|
private function request_json( $url, $params = null )
|
|
{
|
|
if ( is_array( $params ) )
|
|
{
|
|
$query = http_build_query( $params );
|
|
$url .= ( strpos( $url, '?' ) === false ? '?' : '&' ) . $query;
|
|
}
|
|
|
|
$ch = curl_init( $url );
|
|
curl_setopt_array( $ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_SSL_VERIFYPEER => true,
|
|
CURLOPT_CONNECTTIMEOUT => 20,
|
|
CURLOPT_TIMEOUT => 120
|
|
] );
|
|
|
|
$response = curl_exec( $ch );
|
|
$http_code = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
|
$curl_error = curl_error( $ch );
|
|
curl_close( $ch );
|
|
|
|
if ( $response === false )
|
|
{
|
|
self::set_setting( 'facebook_ads_last_error', 'cURL error: ' . $curl_error );
|
|
return false;
|
|
}
|
|
|
|
$decoded = json_decode( (string) $response, true );
|
|
if ( !is_array( $decoded ) )
|
|
{
|
|
self::set_setting( 'facebook_ads_last_error', 'Niepoprawny JSON odpowiedzi Meta API. HTTP ' . $http_code );
|
|
return false;
|
|
}
|
|
|
|
if ( isset( $decoded['error'] ) )
|
|
{
|
|
$message = (string) ( $decoded['error']['message'] ?? 'Nieznany blad Meta API' );
|
|
$code = (string) ( $decoded['error']['code'] ?? '' );
|
|
$subcode = (string) ( $decoded['error']['error_subcode'] ?? '' );
|
|
self::set_setting( 'facebook_ads_last_error', 'Meta API: ' . $message . ' (code: ' . $code . ', subcode: ' . $subcode . ')' );
|
|
return false;
|
|
}
|
|
|
|
if ( $http_code >= 400 )
|
|
{
|
|
self::set_setting( 'facebook_ads_last_error', 'Meta API HTTP ' . $http_code . ': ' . substr( (string) $response, 0, 1000 ) );
|
|
return false;
|
|
}
|
|
|
|
return $decoded;
|
|
}
|
|
}
|