update
This commit is contained in:
@@ -104,6 +104,7 @@ class Clients
|
||||
$google_ads_customer_id = trim( \S::get( 'google_ads_customer_id' ) );
|
||||
$google_merchant_account_id = trim( \S::get( 'google_merchant_account_id' ) );
|
||||
$facebook_ads_account_id = self::normalize_facebook_ads_account_id( \S::get( 'facebook_ads_account_id' ) );
|
||||
$xml_feed_url = trim( \S::get( 'xml_feed_url' ) );
|
||||
$active_raw = \S::get( 'active' );
|
||||
$active = (string) $active_raw === '0' ? 0 : 1;
|
||||
|
||||
@@ -114,6 +115,13 @@ class Clients
|
||||
exit;
|
||||
}
|
||||
|
||||
if ( $xml_feed_url !== '' && !filter_var( $xml_feed_url, FILTER_VALIDATE_URL ) )
|
||||
{
|
||||
\S::alert( 'Niepoprawny URL feedu XML.' );
|
||||
header( 'Location: /clients' );
|
||||
exit;
|
||||
}
|
||||
|
||||
$google_ads_start_date = trim( \S::get( 'google_ads_start_date' ) );
|
||||
|
||||
$data = [
|
||||
@@ -121,6 +129,7 @@ class Clients
|
||||
'google_ads_customer_id' => $google_ads_customer_id ?: null,
|
||||
'google_merchant_account_id' => $google_merchant_account_id ?: null,
|
||||
'facebook_ads_account_id' => $facebook_ads_account_id,
|
||||
'xml_feed_url' => $xml_feed_url ?: null,
|
||||
'google_ads_start_date' => $google_ads_start_date ?: null,
|
||||
'active' => $active,
|
||||
];
|
||||
|
||||
@@ -132,6 +132,23 @@ class Cron
|
||||
$products_temp_rows_total += (int) self::rebuild_products_temp_for_client( (int) $client['id'] );
|
||||
}
|
||||
|
||||
$xml_feed_report = null;
|
||||
if ( !empty( $client['xml_feed_url'] ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$xml_feed_report = \services\XmlFeedImporter::import_for_client( (int) $client['id'] );
|
||||
if ( !empty( $xml_feed_report['errors'] ) )
|
||||
{
|
||||
$products_errors = array_merge( $products_errors, (array) $xml_feed_report['errors'] );
|
||||
}
|
||||
}
|
||||
catch ( \Throwable $e )
|
||||
{
|
||||
$products_errors[] = 'XML feed import: ' . $e -> getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
$errors = array_merge( $campaign_errors, $products_errors );
|
||||
|
||||
self::output_cron_response( [
|
||||
@@ -152,6 +169,7 @@ class Cron
|
||||
'products_fetch_skipped_reasons' => array_keys( $products_fetch_skipped_reasons ),
|
||||
'history_30_products' => $history_30_products_total,
|
||||
'products_temp_rows' => $products_temp_rows_total,
|
||||
'xml_feed' => $xml_feed_report,
|
||||
'errors' => $errors
|
||||
] );
|
||||
}
|
||||
@@ -934,7 +952,7 @@ class Cron
|
||||
}
|
||||
|
||||
$not_found_rows = $mdb -> query(
|
||||
"SELECT id AS product_id, offer_id, name, title
|
||||
"SELECT id AS product_id, offer_id, title, title_gmc
|
||||
FROM products
|
||||
WHERE client_id = :client_id
|
||||
AND TRIM( COALESCE( offer_id, '' ) ) <> ''
|
||||
@@ -950,10 +968,10 @@ class Cron
|
||||
{
|
||||
$offer_id = trim( (string) ( $row['offer_id'] ?? '' ) );
|
||||
$product_id = (int) ( $row['product_id'] ?? 0 );
|
||||
$product_name = trim( (string) ( $row['title'] ?? '' ) );
|
||||
$product_name = trim( (string) ( $row['title_gmc'] ?? '' ) );
|
||||
if ( $product_name === '' )
|
||||
{
|
||||
$product_name = trim( (string) ( $row['name'] ?? '' ) );
|
||||
$product_name = trim( (string) ( $row['title'] ?? '' ) );
|
||||
}
|
||||
if ( $product_name === '' )
|
||||
{
|
||||
@@ -1109,7 +1127,7 @@ class Cron
|
||||
}
|
||||
|
||||
$existing_products_rows = $mdb -> query(
|
||||
'SELECT id, offer_id, name, title, product_url
|
||||
'SELECT id, offer_id, title, title_gmc, product_url
|
||||
FROM products
|
||||
WHERE client_id = :client_id
|
||||
ORDER BY id ASC',
|
||||
@@ -1127,8 +1145,8 @@ class Cron
|
||||
|
||||
$products_by_offer_id[ $offer_id ] = [
|
||||
'id' => (int) ( $row['id'] ?? 0 ),
|
||||
'name' => (string) ( $row['name'] ?? '' ),
|
||||
'title' => (string) ( $row['title'] ?? '' ),
|
||||
'title_gmc' => (string) ( $row['title_gmc'] ?? '' ),
|
||||
'product_url' => (string) ( $row['product_url'] ?? '' )
|
||||
];
|
||||
}
|
||||
@@ -1366,14 +1384,14 @@ class Cron
|
||||
$mdb -> insert( 'products', [
|
||||
'client_id' => $client_id,
|
||||
'offer_id' => $offer_external_id,
|
||||
'name' => $product_title
|
||||
'title' => $product_title
|
||||
] );
|
||||
|
||||
$product_id = $mdb -> id();
|
||||
|
||||
$products_by_offer_id[ $offer_external_id ] = [
|
||||
'id' => (int) $product_id,
|
||||
'name' => $product_title,
|
||||
'title' => $product_title,
|
||||
'product_url' => ''
|
||||
];
|
||||
}
|
||||
|
||||
@@ -319,10 +319,10 @@ class Products
|
||||
}
|
||||
|
||||
$offer_id = trim( (string) ( $row['offer_id'] ?? '' ) );
|
||||
$product_name = trim( (string) ( $row['title'] ?? '' ) );
|
||||
$product_name = trim( (string) ( $row['title_gmc'] ?? '' ) );
|
||||
if ( $product_name === '' )
|
||||
{
|
||||
$product_name = trim( (string) ( $row['name'] ?? '' ) );
|
||||
$product_name = trim( (string) ( $row['title'] ?? '' ) );
|
||||
}
|
||||
if ( $product_name === '' )
|
||||
{
|
||||
@@ -697,8 +697,8 @@ class Products
|
||||
$product_id = \S::get( 'product_id' );
|
||||
|
||||
$product_name = \factory\Products::get_product_name( $product_id );
|
||||
$product_title = \factory\Products::get_product_data( $product_id, 'title' );
|
||||
$product_description = \factory\Products::get_product_data( $product_id, 'description' );
|
||||
$product_title = \factory\Products::get_product_data( $product_id, 'title_gmc' );
|
||||
$product_description = \factory\Products::get_product_data( $product_id, 'description_gmc' );
|
||||
$google_product_category = \factory\Products::get_product_data( $product_id, 'google_product_category' );
|
||||
$product_url = \factory\Products::get_product_data( $product_id, 'product_url' );
|
||||
|
||||
@@ -854,9 +854,9 @@ class Products
|
||||
}
|
||||
|
||||
$context = [
|
||||
'original_name' => $product['name'],
|
||||
'current_title' => \factory\Products::get_product_data( $product_id, 'title' ),
|
||||
'current_description' => \factory\Products::get_product_data( $product_id, 'description' ),
|
||||
'original_name' => $product['title'],
|
||||
'current_title' => \factory\Products::get_product_data( $product_id, 'title_gmc' ),
|
||||
'current_description' => \factory\Products::get_product_data( $product_id, 'description_gmc' ),
|
||||
'current_category' => \factory\Products::get_product_data( $product_id, 'google_product_category' ),
|
||||
'offer_id' => $product['offer_id'],
|
||||
'impressions_30' => $product['impressions_30'] ?? 0,
|
||||
@@ -986,7 +986,7 @@ class Products
|
||||
$custom_class = '';
|
||||
$custom_label_4 = \factory\Products::get_product_data( $product_id, 'custom_label_4' );
|
||||
$custom_label_1 = \factory\Products::get_product_data( $product_id, 'custom_label_1' );
|
||||
$custom_name = \factory\Products::get_product_data( $product_id, 'title' );
|
||||
$custom_name = \factory\Products::get_product_data( $product_id, 'title_gmc' );
|
||||
$product_url = trim( (string) \factory\Products::get_product_data( $product_id, 'product_url' ) );
|
||||
|
||||
if ( $custom_name )
|
||||
@@ -1095,6 +1095,9 @@ class Products
|
||||
foreach ( $breakdown_rows as $breakdown_row )
|
||||
{
|
||||
$breakdown_for_view[] = [
|
||||
'product_id' => (int) ( $breakdown_row['product_id'] ?? $product_id ),
|
||||
'campaign_id' => (int) ( $breakdown_row['campaign_id'] ?? 0 ),
|
||||
'ad_group_id' => (int) ( $breakdown_row['ad_group_id'] ?? 0 ),
|
||||
'campaign_name' => (string) ( $breakdown_row['campaign_name'] ?? '' ),
|
||||
'ad_group_name' => (string) ( $breakdown_row['ad_group_name'] ?? '' ),
|
||||
'impressions' => (int) ( $breakdown_row['impressions'] ?? 0 ),
|
||||
@@ -1185,6 +1188,29 @@ class Products
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function delete_product_scope_history()
|
||||
{
|
||||
$product_id = (int) \S::get( 'product_id' );
|
||||
$campaign_id = (int) \S::get( 'campaign_id' );
|
||||
$ad_group_id = (int) \S::get( 'ad_group_id' );
|
||||
|
||||
if ( $product_id <= 0 )
|
||||
{
|
||||
echo json_encode( [ 'status' => 'error', 'message' => 'Brak identyfikatora produktu.' ] );
|
||||
exit;
|
||||
}
|
||||
|
||||
if ( \factory\Products::delete_product_scope_history( $product_id, $campaign_id, $ad_group_id ) )
|
||||
{
|
||||
echo json_encode( [ 'status' => 'ok' ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
echo json_encode( [ 'status' => 'error', 'message' => 'Nie udalo sie usunac wpisow historii dla tego zakresu.' ] );
|
||||
}
|
||||
exit;
|
||||
}
|
||||
|
||||
static public function save_min_roas()
|
||||
{
|
||||
$product_id = \S::get( 'product_id' );
|
||||
@@ -1390,8 +1416,8 @@ class Products
|
||||
$google_product_category = \S::get( 'google_product_category' );
|
||||
$product_url = \S::get( 'product_url' );
|
||||
|
||||
$old_title = (string) \factory\Products::get_product_data( $product_id, 'title' );
|
||||
$old_description = (string) \factory\Products::get_product_data( $product_id, 'description' );
|
||||
$old_title = (string) \factory\Products::get_product_data( $product_id, 'title_gmc' );
|
||||
$old_description = (string) \factory\Products::get_product_data( $product_id, 'description_gmc' );
|
||||
$old_category = (string) \factory\Products::get_product_data( $product_id, 'google_product_category' );
|
||||
|
||||
$changed_for_merchant = [];
|
||||
@@ -1400,13 +1426,13 @@ class Products
|
||||
{
|
||||
if ( $custom_title )
|
||||
{
|
||||
\factory\Products::set_product_data( $product_id, 'title', $custom_title );
|
||||
\factory\Products::set_product_data( $product_id, 'title_gmc', $custom_title );
|
||||
$changed_for_merchant['title'] = [ 'old' => $old_title, 'new' => (string) $custom_title ];
|
||||
}
|
||||
|
||||
if ( $custom_description )
|
||||
{
|
||||
\factory\Products::set_product_data( $product_id, 'description', $custom_description );
|
||||
\factory\Products::set_product_data( $product_id, 'description_gmc', $custom_description );
|
||||
$changed_for_merchant['description'] = [ 'old' => $old_description, 'new' => (string) $custom_description ];
|
||||
}
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ class Products
|
||||
'custom_label_4',
|
||||
'custom_label_3',
|
||||
'custom_label_1',
|
||||
'title',
|
||||
'description',
|
||||
'title_gmc',
|
||||
'description_gmc',
|
||||
'google_product_category',
|
||||
'product_url'
|
||||
], true );
|
||||
@@ -32,6 +32,57 @@ class Products
|
||||
return true;
|
||||
}
|
||||
|
||||
static public function delete_product_scope_history( $product_id, $campaign_id, $ad_group_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$product_id = (int) $product_id;
|
||||
$campaign_id = (int) $campaign_id;
|
||||
$ad_group_id = (int) $ad_group_id;
|
||||
|
||||
if ( $product_id <= 0 )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
$where = [
|
||||
'product_id' => $product_id,
|
||||
'campaign_id' => $campaign_id,
|
||||
'ad_group_id' => $ad_group_id
|
||||
];
|
||||
|
||||
$pdo = $mdb -> pdo;
|
||||
$started_tx = false;
|
||||
|
||||
try
|
||||
{
|
||||
if ( !$pdo -> inTransaction() )
|
||||
{
|
||||
$pdo -> beginTransaction();
|
||||
$started_tx = true;
|
||||
}
|
||||
|
||||
$mdb -> delete( 'products_aggregate', $where );
|
||||
$mdb -> delete( 'products_history', $where );
|
||||
$mdb -> delete( 'products_history_30', $where );
|
||||
|
||||
if ( $started_tx )
|
||||
{
|
||||
$pdo -> commit();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch ( \Throwable $e )
|
||||
{
|
||||
if ( $started_tx && $pdo -> inTransaction() )
|
||||
{
|
||||
$pdo -> rollBack();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static public function get_product_comments( $product_id )
|
||||
{
|
||||
global $mdb;
|
||||
@@ -154,8 +205,8 @@ class Products
|
||||
$sql = 'SELECT
|
||||
p.id AS product_id,
|
||||
p.offer_id,
|
||||
p.name,
|
||||
p.title,
|
||||
p.title_gmc,
|
||||
SUM( pa.impressions_30 ) AS impressions_30
|
||||
FROM products_aggregate AS pa
|
||||
INNER JOIN products AS p ON p.id = pa.product_id
|
||||
@@ -171,9 +222,9 @@ class Products
|
||||
}
|
||||
|
||||
$sql .= '
|
||||
GROUP BY p.id, p.offer_id, p.name, p.title
|
||||
GROUP BY p.id, p.offer_id, p.title, p.title_gmc
|
||||
HAVING COALESCE( SUM( pa.impressions_30 ), 0 ) = 0
|
||||
ORDER BY COALESCE( NULLIF( TRIM( p.title ), \'\' ), NULLIF( TRIM( p.name ), \'\' ), p.offer_id ) ASC, p.id ASC
|
||||
ORDER BY COALESCE( NULLIF( TRIM( p.title_gmc ), \'\' ), NULLIF( TRIM( p.title ), \'\' ), p.offer_id ) ASC, p.id ASC
|
||||
LIMIT ' . $limit;
|
||||
|
||||
try
|
||||
@@ -496,8 +547,8 @@ class Products
|
||||
if ( $search !== '' )
|
||||
{
|
||||
$sql .= ' AND (
|
||||
p.name LIKE :search
|
||||
OR p.title LIKE :search
|
||||
p.title LIKE :search
|
||||
OR p.title_gmc LIKE :search
|
||||
OR p.offer_id LIKE :search
|
||||
OR p.custom_label_4 LIKE :search
|
||||
OR p.custom_label_1 LIKE :search
|
||||
@@ -566,7 +617,7 @@ class Products
|
||||
END AS ad_group_name,
|
||||
MIN( pa.campaign_id ) AS history_campaign_id,
|
||||
MIN( pa.ad_group_id ) AS history_ad_group_id,
|
||||
COALESCE( NULLIF( TRIM( p.title ), \'\' ), NULLIF( TRIM( p.name ), \'\' ), p.offer_id ) AS name,
|
||||
COALESCE( NULLIF( TRIM( p.title_gmc ), \'\' ), NULLIF( TRIM( p.title ), \'\' ), p.offer_id ) AS name,
|
||||
SUM( pa.impressions_all_time ) AS impressions,
|
||||
SUM( pa.impressions_30 ) AS impressions_30,
|
||||
SUM( pa.clicks_all_time ) AS clicks,
|
||||
@@ -595,7 +646,7 @@ class Products
|
||||
self::build_scope_filters( $sql, $params, $campaign_id, $ad_group_id );
|
||||
self::build_products_filters( $sql, $params, $search, $custom_label_4, $custom_label_1 );
|
||||
|
||||
$sql .= ' GROUP BY p.id, p.offer_id, p.min_roas, p.custom_label_1, p.name, p.title';
|
||||
$sql .= ' GROUP BY p.id, p.offer_id, p.min_roas, p.custom_label_1, p.title, p.title_gmc';
|
||||
$sql .= ' ORDER BY ' . $order_sql . ' ' . $order_dir . ', product_id DESC LIMIT ' . $start . ', ' . $limit;
|
||||
|
||||
return $mdb -> query( $sql, $params ) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
@@ -773,7 +824,7 @@ class Products
|
||||
'SELECT
|
||||
p.id,
|
||||
p.offer_id,
|
||||
p.name,
|
||||
p.title,
|
||||
p.min_roas,
|
||||
COALESCE( SUM( pa.impressions_all_time ), 0 ) AS impressions,
|
||||
COALESCE( SUM( pa.impressions_30 ), 0 ) AS impressions_30,
|
||||
@@ -800,7 +851,7 @@ class Products
|
||||
FROM products AS p
|
||||
LEFT JOIN products_aggregate AS pa ON pa.product_id = p.id
|
||||
WHERE p.id = :pid
|
||||
GROUP BY p.id, p.offer_id, p.name, p.min_roas',
|
||||
GROUP BY p.id, p.offer_id, p.title, p.min_roas',
|
||||
[ ':pid' => $product_id ]
|
||||
) -> fetch( \PDO::FETCH_ASSOC );
|
||||
}
|
||||
@@ -883,7 +934,7 @@ class Products
|
||||
return null;
|
||||
}
|
||||
|
||||
return $mdb -> get( 'products', 'name', [ 'id' => $product_id ] );
|
||||
return $mdb -> get( 'products', 'title', [ 'id' => $product_id ] );
|
||||
}
|
||||
|
||||
static public function get_product_merchant_context( $product_id )
|
||||
@@ -1269,7 +1320,7 @@ class Products
|
||||
p.id,
|
||||
p.client_id,
|
||||
p.offer_id,
|
||||
p.name,
|
||||
p.title,
|
||||
cl.google_ads_customer_id,
|
||||
cl.google_merchant_account_id
|
||||
FROM products AS p
|
||||
|
||||
@@ -163,12 +163,12 @@ class SupplementalFeed
|
||||
$labels_updated = self::refresh_bestseller_labels_for_client( $client_id );
|
||||
|
||||
$products = $mdb -> query(
|
||||
"SELECT p.offer_id, p.title, p.description, p.google_product_category, p.custom_label_1, p.custom_label_3, p.custom_label_4
|
||||
"SELECT p.offer_id, p.title_gmc AS title, p.description_gmc AS description, p.google_product_category, p.custom_label_1, p.custom_label_3, p.custom_label_4
|
||||
FROM products p
|
||||
WHERE p.client_id = :client_id
|
||||
AND p.offer_id IS NOT NULL
|
||||
AND p.offer_id <> ''
|
||||
AND ( p.title IS NOT NULL OR p.description IS NOT NULL OR p.google_product_category IS NOT NULL OR p.custom_label_1 IS NOT NULL OR p.custom_label_3 IS NOT NULL OR p.custom_label_4 IS NOT NULL )",
|
||||
AND ( p.title_gmc IS NOT NULL OR p.description_gmc IS NOT NULL OR p.google_product_category IS NOT NULL OR p.custom_label_1 IS NOT NULL OR p.custom_label_3 IS NOT NULL OR p.custom_label_4 IS NOT NULL )",
|
||||
[ ':client_id' => $client_id ]
|
||||
) -> fetchAll( \PDO::FETCH_ASSOC );
|
||||
|
||||
|
||||
398
autoload/services/class.XmlFeedImporter.php
Normal file
398
autoload/services/class.XmlFeedImporter.php
Normal file
@@ -0,0 +1,398 @@
|
||||
<?php
|
||||
namespace services;
|
||||
|
||||
class XmlFeedImporter
|
||||
{
|
||||
const BATCH_SIZE = 200;
|
||||
const HTTP_TIMEOUT = 300;
|
||||
const GMC_NS = 'http://base.google.com/ns/1.0';
|
||||
|
||||
/**
|
||||
* Importuje feed XML (Google Merchant) dla klienta i wzbogaca tabele products.
|
||||
* Strumieniowy parser (XMLReader) odporny na feedy z kilkoma tysiacami pozycji.
|
||||
*
|
||||
* Aktualizuje pola: title, description, custom_label_1, price (zrodlowe dane z feedu).
|
||||
* NIE nadpisuje pol edytowanych przez skrypty/AI: title_gmc, description_gmc (sluza do supplemental feed -> GMC).
|
||||
* Dla pozycji nieobecnej w products tworzy nowy rekord.
|
||||
*
|
||||
* @param int $client_id
|
||||
* @return array raport z polami: feed_url, fetched, updated, inserted, skipped, errors, peak_memory_mb, duration_ms
|
||||
*/
|
||||
static public function import_for_client( $client_id )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
$client_id = (int) $client_id;
|
||||
$report = [
|
||||
'feed_url' => '',
|
||||
'fetched' => 0,
|
||||
'updated' => 0,
|
||||
'inserted' => 0,
|
||||
'skipped' => 0,
|
||||
'errors' => [],
|
||||
'peak_memory_mb' => 0,
|
||||
'duration_ms' => 0,
|
||||
];
|
||||
|
||||
if ( $client_id <= 0 )
|
||||
{
|
||||
$report['errors'][] = 'Nieprawidlowy client_id';
|
||||
return $report;
|
||||
}
|
||||
|
||||
$client = $mdb -> get( 'clients', [ 'id', 'xml_feed_url' ], [ 'id' => $client_id ] );
|
||||
$feed_url = trim( (string) ( $client['xml_feed_url'] ?? '' ) );
|
||||
if ( $feed_url === '' )
|
||||
{
|
||||
$report['skipped_reason'] = 'no_feed';
|
||||
return $report;
|
||||
}
|
||||
$report['feed_url'] = $feed_url;
|
||||
|
||||
$started_at = microtime( true );
|
||||
@set_time_limit( 600 );
|
||||
@ini_set( 'memory_limit', '512M' );
|
||||
|
||||
$tmp_file = tempnam( sys_get_temp_dir(), 'xmlfeed_' );
|
||||
if ( $tmp_file === false )
|
||||
{
|
||||
$report['errors'][] = 'Nie mozna utworzyc pliku tymczasowego';
|
||||
return $report;
|
||||
}
|
||||
|
||||
$download_ok = self::download_feed( $feed_url, $tmp_file, $report );
|
||||
if ( !$download_ok )
|
||||
{
|
||||
@unlink( $tmp_file );
|
||||
return $report;
|
||||
}
|
||||
|
||||
$reader = new \XMLReader();
|
||||
if ( !$reader -> open( $tmp_file ) )
|
||||
{
|
||||
$report['errors'][] = 'Nie mozna otworzyc feedu XML do parsowania';
|
||||
@unlink( $tmp_file );
|
||||
return $report;
|
||||
}
|
||||
|
||||
$batch = [];
|
||||
while ( $reader -> read() )
|
||||
{
|
||||
if ( $reader -> nodeType !== \XMLReader::ELEMENT )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$local = $reader -> localName;
|
||||
if ( $local !== 'item' && $local !== 'entry' )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$node = $reader -> expand();
|
||||
if ( !$node )
|
||||
{
|
||||
$report['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$item = self::extract_item_fields( $node );
|
||||
if ( $item === null || $item['offer_id'] === '' )
|
||||
{
|
||||
$report['skipped']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$report['fetched']++;
|
||||
$batch[] = $item;
|
||||
|
||||
if ( count( $batch ) >= self::BATCH_SIZE )
|
||||
{
|
||||
self::flush_batch( $client_id, $batch, $report );
|
||||
$batch = [];
|
||||
gc_collect_cycles();
|
||||
}
|
||||
}
|
||||
catch ( \Throwable $e )
|
||||
{
|
||||
$report['skipped']++;
|
||||
if ( count( $report['errors'] ) < 20 )
|
||||
{
|
||||
$report['errors'][] = 'Item parse error: ' . $e -> getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( !empty( $batch ) )
|
||||
{
|
||||
self::flush_batch( $client_id, $batch, $report );
|
||||
$batch = [];
|
||||
}
|
||||
|
||||
$reader -> close();
|
||||
@unlink( $tmp_file );
|
||||
|
||||
$mdb -> update( 'clients', [ 'xml_feed_last_sync_at' => date( 'Y-m-d H:i:s' ) ], [ 'id' => $client_id ] );
|
||||
|
||||
$report['peak_memory_mb'] = round( memory_get_peak_usage( true ) / 1024 / 1024, 1 );
|
||||
$report['duration_ms'] = (int) round( ( microtime( true ) - $started_at ) * 1000 );
|
||||
|
||||
return $report;
|
||||
}
|
||||
|
||||
static private function download_feed( $url, $tmp_file, &$report )
|
||||
{
|
||||
$fp = fopen( $tmp_file, 'wb' );
|
||||
if ( $fp === false )
|
||||
{
|
||||
$report['errors'][] = 'Nie mozna otworzyc pliku tymczasowego do zapisu';
|
||||
return false;
|
||||
}
|
||||
|
||||
$ch = curl_init( $url );
|
||||
curl_setopt_array( $ch, [
|
||||
CURLOPT_FILE => $fp,
|
||||
CURLOPT_FOLLOWLOCATION => true,
|
||||
CURLOPT_MAXREDIRS => 5,
|
||||
CURLOPT_TIMEOUT => self::HTTP_TIMEOUT,
|
||||
CURLOPT_CONNECTTIMEOUT => 30,
|
||||
CURLOPT_FAILONERROR => true,
|
||||
CURLOPT_USERAGENT => 'adsPRO XML Feed Importer/1.0',
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_ENCODING => '',
|
||||
] );
|
||||
|
||||
$ok = curl_exec( $ch );
|
||||
$http_code = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
||||
$err = curl_error( $ch );
|
||||
curl_close( $ch );
|
||||
fclose( $fp );
|
||||
|
||||
if ( !$ok || ( $http_code >= 400 ) )
|
||||
{
|
||||
$report['errors'][] = 'Pobieranie feedu nieudane (HTTP ' . $http_code . '): ' . $err;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( filesize( $tmp_file ) === 0 )
|
||||
{
|
||||
$report['errors'][] = 'Feed jest pusty';
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static private function extract_item_fields( \DOMNode $node )
|
||||
{
|
||||
$doc = new \DOMDocument();
|
||||
$imported = $doc -> importNode( $node, true );
|
||||
$doc -> appendChild( $imported );
|
||||
|
||||
$sxe = simplexml_import_dom( $doc -> documentElement );
|
||||
if ( $sxe === false )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
$g = $sxe -> children( self::GMC_NS );
|
||||
|
||||
$offer_id = '';
|
||||
if ( isset( $g -> id ) )
|
||||
{
|
||||
$offer_id = trim( (string) $g -> id );
|
||||
}
|
||||
if ( $offer_id === '' && isset( $sxe -> id ) )
|
||||
{
|
||||
$offer_id = trim( (string) $sxe -> id );
|
||||
}
|
||||
if ( $offer_id === '' )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
$title = '';
|
||||
if ( isset( $g -> title ) )
|
||||
{
|
||||
$title = trim( (string) $g -> title );
|
||||
}
|
||||
if ( $title === '' && isset( $sxe -> title ) )
|
||||
{
|
||||
$title = trim( (string) $sxe -> title );
|
||||
}
|
||||
|
||||
$description = '';
|
||||
if ( isset( $g -> description ) )
|
||||
{
|
||||
$description = trim( (string) $g -> description );
|
||||
}
|
||||
if ( $description === '' && isset( $sxe -> description ) )
|
||||
{
|
||||
$description = trim( (string) $sxe -> description );
|
||||
}
|
||||
|
||||
$custom_label_1 = isset( $g -> custom_label_1 ) ? trim( (string) $g -> custom_label_1 ) : '';
|
||||
|
||||
$price = null;
|
||||
if ( isset( $g -> price ) )
|
||||
{
|
||||
$price_raw = trim( (string) $g -> price );
|
||||
$price = self::parse_price( $price_raw );
|
||||
}
|
||||
if ( $price === null && isset( $g -> sale_price ) )
|
||||
{
|
||||
$price = self::parse_price( trim( (string) $g -> sale_price ) );
|
||||
}
|
||||
|
||||
return [
|
||||
'offer_id' => self::truncate( $offer_id, 255 ),
|
||||
'title' => self::truncate( $title, 255 ),
|
||||
'description' => $description,
|
||||
'custom_label_1' => self::truncate( $custom_label_1, 255 ),
|
||||
'price' => $price,
|
||||
];
|
||||
}
|
||||
|
||||
static private function parse_price( $raw )
|
||||
{
|
||||
if ( $raw === '' )
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if ( preg_match( '/([0-9]+(?:[.,][0-9]+)?)/', $raw, $m ) )
|
||||
{
|
||||
$value = (float) str_replace( ',', '.', $m[1] );
|
||||
if ( $value > 0 )
|
||||
{
|
||||
return round( $value, 2 );
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static private function truncate( $value, $max )
|
||||
{
|
||||
if ( function_exists( 'mb_substr' ) )
|
||||
{
|
||||
return mb_substr( (string) $value, 0, $max, 'UTF-8' );
|
||||
}
|
||||
return substr( (string) $value, 0, $max );
|
||||
}
|
||||
|
||||
static private function flush_batch( $client_id, array $batch, array &$report )
|
||||
{
|
||||
global $mdb;
|
||||
|
||||
if ( empty( $batch ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$pdo = $mdb -> pdo;
|
||||
|
||||
try
|
||||
{
|
||||
$pdo -> beginTransaction();
|
||||
|
||||
$offer_ids = [];
|
||||
foreach ( $batch as $item )
|
||||
{
|
||||
$offer_ids[ $item['offer_id'] ] = true;
|
||||
}
|
||||
$offer_ids = array_keys( $offer_ids );
|
||||
|
||||
$existing = [];
|
||||
if ( !empty( $offer_ids ) )
|
||||
{
|
||||
$placeholders = [];
|
||||
$select_params = [ ':client_id' => $client_id ];
|
||||
foreach ( $offer_ids as $idx => $oid )
|
||||
{
|
||||
$key = ':oid_' . $idx;
|
||||
$placeholders[] = $key;
|
||||
$select_params[ $key ] = $oid;
|
||||
}
|
||||
$sel_sql = 'SELECT id, offer_id FROM products WHERE client_id = :client_id AND offer_id IN (' . implode( ', ', $placeholders ) . ')';
|
||||
$sel_stmt = $pdo -> prepare( $sel_sql );
|
||||
$sel_stmt -> execute( $select_params );
|
||||
while ( $row = $sel_stmt -> fetch( \PDO::FETCH_ASSOC ) )
|
||||
{
|
||||
// jeden offer_id moze miec wiele wierszy (legacy duplikaty) - zbieramy wszystkie id
|
||||
$existing[ (string) $row['offer_id'] ][] = (int) $row['id'];
|
||||
}
|
||||
}
|
||||
|
||||
$update_stmt = $pdo -> prepare(
|
||||
'UPDATE products SET
|
||||
title = :title,
|
||||
description = :description,
|
||||
custom_label_1 = COALESCE(:custom_label_1, custom_label_1),
|
||||
price = COALESCE(:price, price)
|
||||
WHERE id = :id'
|
||||
);
|
||||
|
||||
$insert_stmt = $pdo -> prepare(
|
||||
'INSERT INTO products (client_id, offer_id, title, description, custom_label_1, price)
|
||||
VALUES (:client_id, :offer_id, :title, :description, :custom_label_1, :price)'
|
||||
);
|
||||
|
||||
$updated_count = 0;
|
||||
$inserted_count = 0;
|
||||
|
||||
foreach ( $batch as $item )
|
||||
{
|
||||
$title = $item['title'] !== '' ? $item['title'] : null;
|
||||
$desc = $item['description'] !== '' ? $item['description'] : null;
|
||||
$cl1 = $item['custom_label_1'] !== '' ? $item['custom_label_1'] : null;
|
||||
$price = $item['price'];
|
||||
|
||||
if ( !empty( $existing[ $item['offer_id'] ] ) )
|
||||
{
|
||||
// aktualizujemy WSZYSTKIE legacy duplikaty (utrzymujemy spojnosc danych)
|
||||
foreach ( $existing[ $item['offer_id'] ] as $row_id )
|
||||
{
|
||||
$update_stmt -> execute( [
|
||||
':title' => $title,
|
||||
':description' => $desc,
|
||||
':custom_label_1' => $cl1,
|
||||
':price' => $price,
|
||||
':id' => $row_id,
|
||||
] );
|
||||
$updated_count++;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$insert_stmt -> execute( [
|
||||
':client_id' => $client_id,
|
||||
':offer_id' => $item['offer_id'],
|
||||
':title' => $title,
|
||||
':description' => $desc,
|
||||
':custom_label_1' => $cl1,
|
||||
':price' => $price,
|
||||
] );
|
||||
$inserted_count++;
|
||||
}
|
||||
}
|
||||
|
||||
$pdo -> commit();
|
||||
|
||||
$report['updated'] += $updated_count;
|
||||
$report['inserted'] += $inserted_count;
|
||||
}
|
||||
catch ( \Throwable $e )
|
||||
{
|
||||
if ( $pdo -> inTransaction() )
|
||||
{
|
||||
$pdo -> rollBack();
|
||||
}
|
||||
if ( count( $report['errors'] ) < 20 )
|
||||
{
|
||||
$report['errors'][] = 'Batch flush error: ' . $e -> getMessage();
|
||||
}
|
||||
$report['skipped'] += count( $batch );
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user