Files
adsPRO/autoload/services/class.FacebookAdsApi.php
Jacek Pyziak b54a9a71b1 Add CLI script to fetch active Meta Ads insights for campaigns, adsets, and ads
- 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.
2026-02-20 23:45:36 +01:00

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