PluginProbe ʕ •ᴥ•ʔ
XML Feed for Skroutz & BestPrice for WooCommerce / 1.2.1
XML Feed for Skroutz & BestPrice for WooCommerce v1.2.1
trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.0.4 1.1.0 1.1.1 1.1.2 1.2.0 1.2.1 1.2.3 1.2.4
xml-feed-for-skroutz-for-woocommerce / admin / class-dc-skroutz-feed-creator.php
xml-feed-for-skroutz-for-woocommerce / admin Last commit date
css 1 year ago js 1 year ago class-dc-skroutz-feed-admin.php 8 months ago class-dc-skroutz-feed-creator.php 8 months ago class-dc-skroutz-feed-data-helper.php 8 months ago index.php 1 year ago mod_simplexml.php 1 year ago
class-dc-skroutz-feed-creator.php
1316 lines
1 <?php
2
3 /**
4 * The class that generates the XML feed.
5 */
6 class Dicha_Skroutz_Feed_Creator {
7
8 /**
9 * The plugin options (user settings).
10 * @var array $options
11 */
12 private array $options;
13
14 /**
15 * The feed type.
16 * @var string $feed_type
17 */
18 private string $feed_type;
19
20 /**
21 * A helper class to get product data.
22 * @var Dicha_Skroutz_Feed_Data_Helper $data_helper
23 */
24 private Dicha_Skroutz_Feed_Data_Helper $data_helper;
25
26 /**
27 * An array with mapping between sanitized and unsanitized attribute slugs.
28 * @var array $wc_product_attributes_sanitized
29 */
30 private array $wc_product_attributes_sanitized = [];
31
32 /**
33 * The array with product data that will be exported to XML.
34 * @var array
35 */
36 private array $products_for_export = [];
37
38 /**
39 * The array with problematic products that will be skipped from XML.
40 * @var array
41 */
42 private array $products_with_errors = [];
43
44 /**
45 * The array with errors that occurred during the XML file creation process.
46 * @var string[]
47 */
48 private array $xml_creation_errors = [];
49
50 /**
51 * Script execution start time.
52 *
53 * @var float
54 */
55 private float $start_full_time = 0.0;
56
57 /**
58 * XML generation completed time.
59 *
60 * @var float
61 */
62 private float $xml_generation_time = 0.0;
63
64 /**
65 * The max memory limit of the server.
66 *
67 * @var string The memory limit in bytes.
68 */
69 private string $memory_limit;
70
71 /**
72 * The fields to wrap in CDATA when writing the XML.
73 *
74 * @var array
75 */
76 private array $fields_in_cdata = [];
77
78 /**
79 * If a language switch is active.
80 *
81 * @var bool
82 */
83 private bool $wpml_do_lang_switch = false;
84
85 /**
86 * The original language of the site.
87 *
88 * @var string
89 */
90 private string $original_site_lang = '';
91
92
93 /**
94 * Constructor.
95 *
96 * @param string $feed_type
97 */
98 public function __construct( string $feed_type = '' ) {
99
100 $this->feed_type = empty( $feed_type ) ? 'skroutz' : trim( $feed_type );
101 }
102
103
104 /**
105 * The main function that generates the XML feed.
106 *
107 * @return bool True if XML creation was successful. False otherwise.
108 */
109 public function create_feed(): bool {
110
111 $this->start_full_time = microtime( true );
112
113 // echo '<pre>';
114
115 $this->init_options();
116 $this->init_data_helper();
117 $this->setup_wc_attributes_list();
118 $this->find_max_memory_limit();
119
120 // echo '<pre>'; print_r( $this->options ); echo '</pre>';
121
122 // Add compatibility with RightPress - Pricepep - WooCommerce Dynamic Pricing, Discounts & Fees
123 add_filter( 'rp_wcdpd_request_is_product_feed', '__return_true' );
124
125 // Build product data for export
126 $this->build_product_export_data();
127
128 // Add compatibility with RightPress - Pricepep - WooCommerce Dynamic Pricing, Discounts & Fees
129 remove_filter( 'rp_wcdpd_request_is_product_feed', '__return_true' );
130
131 // Create XML and save
132 $this->saveXML();
133 $this->xml_generation_time = microtime( true );
134
135 // write log data
136 $this->write_logs();
137
138 return empty( $this->xml_creation_errors );
139 }
140
141
142 /**
143 * Gets all products for export and fills an array with their export data.
144 *
145 * @return void
146 */
147 private function build_product_export_data(): void {
148
149 $skroutz_products = $this->collect_products_for_export();
150 // var_dump( $skroutz_products );
151
152 $product_counter = 0;
153
154 // todo move to separate class and add hooks for modifications
155 if ( class_exists( 'SitePress', false ) ) {
156 global $sitepress;
157
158 $switch_language_enabled = apply_filters( 'dicha_skroutz_feed_switch_language_enabled', true );
159
160 if ( $switch_language_enabled && isset( $sitepress ) ) {
161
162 $this->original_site_lang = $sitepress->get_default_language();
163
164 if ( $this->original_site_lang != 'el' ) {
165 $sitepress->switch_lang( 'el', false );
166 $this->wpml_do_lang_switch = true;
167 }
168 }
169 }
170
171 foreach ( $skroutz_products as $product_id ) {
172
173 if ( $this->wpml_do_lang_switch ) {
174 $original_lang_product_id = $product_id;
175 $product_id = apply_filters( 'wpml_object_id', $original_lang_product_id, 'product', true, 'el' );
176 }
177
178 $product_counter ++;
179
180 if ( $product_counter >= 20 ) {
181 $product_counter = 0;
182
183 $this->maybe_flush_runtime_cache();
184 }
185
186 // if ( $product_id != 1186 ) continue; // Skroutz variable with Size + Color
187 // if ( $product_id != 1170 ) continue; // Skroutz Simple
188 // if ( $product_id != 1170 && $product_id != 1186 ) continue;
189 // if ( $product_id != 1175 ) continue; // Skroutz Variable with Size
190 // if ( $product_id != 1180 ) continue; // Skroutz Variable with Dimension
191 // if ( $product_id != 1230 ) continue; // Skroutz Variable with "Any" option
192 // if ( $product_id != 1264 && $product_id != 1268 ) continue; // Skroutz products with greek slug
193 // if ( $product_id != 1220 && $product_id != 1287 ) continue; // Skroutz variable with 2 attrs (no size var) + Skroutz Variable with Color (only) + Size attr (no var)
194
195 $product = wc_get_product( $product_id );
196
197 if ( ! $product instanceof WC_Product ) continue;
198
199 // Skip products from manual filter
200 if ( $this->data_helper->skroutz_exclude_product_from_xml( $product ) ) continue;
201
202 $product_type = $product->get_type();
203
204 if ( 'simple' === $product_type ) {
205 /** @var WC_Product_Simple $product */
206
207 $unique_id = $product->get_id();
208
209 if ( $this->wpml_do_lang_switch ) {
210 // maybe add with filter
211 $unique_id = $original_lang_product_id;
212 }
213
214 // gather simple product data
215 $node_data = array_merge(
216 [ 'id' => apply_filters( 'dicha_skroutz_feed_custom_product_id', $unique_id, $product, $this->feed_type ) ],
217 $this->get_simple_product_data( $product )
218 );
219
220 $data_contain_no_errors = $this->detect_data_errors( $node_data );
221
222 $node_data = apply_filters( 'dicha_skroutz_feed_custom_node_data', $node_data, $product, [], $this->feed_type );
223
224 if ( $data_contain_no_errors ) {
225 $this->products_for_export[ $unique_id ] = $node_data;
226 }
227 }
228 elseif ( 'variable' === $product_type ) {
229 /** @var WC_Product_Variable $product */
230
231 $variations_groups = [];
232
233 // 1. find variation atts and available variations
234 $cat_supports_vars_nodes = empty( array_intersect( $product->get_category_ids(), $this->options['cats_with_no_vars_support'] ) );
235 $size_var_atts_for_product = $cat_supports_vars_nodes ? $this->options['size'] : [];
236 $size_var_atts_for_product = (array) apply_filters( 'dicha_skroutz_feed_size_vars_atts_for_product', $size_var_atts_for_product, $product, $this->options['size'], $this->feed_type );
237 $available_variations = $product->get_available_variations( 'objects' );
238 $variation_attributes = $product->get_variation_attributes();
239 $has_size_variations = $this->has_size_options( $variation_attributes, $size_var_atts_for_product );
240 $has_non_size_variations = $this->has_non_size_options( $variation_attributes, $size_var_atts_for_product );
241 $has_color_variations = $this->has_color_options( $variation_attributes );
242
243 // var_dump( $variation_attributes );
244 // var_dump( $available_variations );
245 // var_dump($has_size_variations);
246 // var_dump($has_non_size_variations);
247 // var_dump($has_color_variations);
248
249 $parent_id = $product->get_id();
250 $parent_name = $product->get_name();
251
252 // Calculate parent level data once for all variation groups
253 $parent_level_data = $this->get_variable_parent_level_data( $product );
254
255 // if color not used for variations, calculate it now and NOT overwrite later
256 if ( ! $has_color_variations ) {
257 $parent_level_data['color'] = $this->data_helper->skroutz_get_color( $product );
258 }
259
260 // if size not used for variations, calculate it now and NOT overwrite later
261 if ( ! $has_size_variations ) {
262 $parent_level_data['size'] = $this->data_helper->skroutz_get_size( $product );
263 }
264
265 /** @var WC_Product_Variation[] $available_variations */
266
267 // split variations to groups, based on variation attributes
268 foreach ( $available_variations as $variation ) {
269
270 // $variation_attributes = $variation->get_attributes();
271 $variation_attributes = $variation->get_variation_attributes( false );
272 $unique_key = $parent_id;
273 $group_name = $parent_name;
274
275 if ( $this->wpml_do_lang_switch ) {
276 // maybe add with filter
277 $unique_key = $original_lang_product_id;
278 }
279
280 // var_dump( $variation->get_id() );
281 // var_dump( $variation_attributes );
282
283
284 // Detect if a variation with "Any" size or "Any" color exists ("Any" === empty string)
285 // If "Any" attribute is color or size, we must skip the variation because of skroutz requirements
286 // If "Any" attribute is something else, it doesn't bother us, and the script will continue as usual
287 foreach ( $variation_attributes as $attribute_slug => $attribute_value ) {
288 if ( empty( $attribute_value ) ) {
289
290 // in this point, the $attribute_slug is already sanitized,
291 // so we need to fetch the original (unsanitized) slug with non english chars,
292 // to check inside $this->options which contains the unsanitized slugs
293 // Checked for greek slugs
294 if ( isset( $this->wc_product_attributes_sanitized[ $attribute_slug ] ) ) {
295 $attribute_slug = $this->wc_product_attributes_sanitized[ $attribute_slug ];
296 }
297
298 if ( in_array( $attribute_slug, $size_var_atts_for_product ) || in_array( $attribute_slug, $this->options['color'] ) ) {
299 continue 2;
300 }
301 }
302 }
303
304 // if exist variations with attributes that are not "size", then split to "variations groups"
305 if ( $has_non_size_variations ) {
306
307 $unique_key_parts = [ $unique_key ];
308 $group_name_parts = [ $group_name ];
309
310 foreach ( $variation_attributes as $attribute_slug => $attribute_value ) {
311
312 // in this point, the $attribute_slug is already sanitized,
313 // so we need to fetch the original (unsanitized) slug with non english chars,
314 // in order to use the functions `taxonomy_exists` and `get_term_by`
315 // Checked for greek slugs
316 if ( isset( $this->wc_product_attributes_sanitized[ $attribute_slug ] ) ) {
317 $attribute_slug = $this->wc_product_attributes_sanitized[ $attribute_slug ];
318 }
319
320 // if "size" attribute or attribute with "Any" value, then no grouping happens
321 if ( in_array( $attribute_slug, $size_var_atts_for_product ) || empty( $attribute_value ) ) continue;
322
323 $attribute_term = taxonomy_exists( $attribute_slug ) ? get_term_by( 'slug', $attribute_value, $attribute_slug ) : false;
324 $attribute_term = ! is_wp_error( $attribute_term ) && $attribute_term ? $attribute_term : false;
325
326 // use term_id if taxonomy exists - use value if custom (no taxonomy) attribute
327 // maybe hash $attribute_value for non taxonomies?
328 $term_id = ! is_wp_error( $attribute_term ) && $attribute_term ? $attribute_term->term_id : $attribute_value;
329
330 if ( $this->wpml_do_lang_switch ) {
331 // maybe add with filter
332 $term_id = apply_filters( 'wpml_object_id', $term_id, $attribute_slug, true, 'el' );
333 }
334
335 // Use attribute term_id to create a unique id for the variation group
336 $unique_key_parts[] = $term_id;
337
338 $term_name = ! is_wp_error( $attribute_term ) && $attribute_term ? $attribute_term->name : $attribute_value;
339 $group_name_parts[] = $term_name;
340 }
341
342 $unique_key = implode( '-', $unique_key_parts );
343 $group_name = implode( ' ', $group_name_parts );
344 }
345
346 // add variation to the correct variation group
347 if ( ! isset( $variations_groups[ $unique_key ] ) ) {
348 $variations_groups[ $unique_key ] = [
349 'unique_id' => $unique_key,
350 'group_name' => $this->data_helper->skroutz_get_name( $product, $group_name, $variation ),
351 'group_variations' => [ $variation ]
352 ];
353 }
354 else {
355 $variations_groups[ $unique_key ]['group_variations'][] = $variation;
356 }
357 }
358
359 // in case of all variations are "Any" size or "Any" color variations -> Skip product but add to array for logging purposes
360 if ( empty( $variations_groups ) ) {
361 $this->products_with_errors[ $parent_id ] = [
362 'id' => $parent_id,
363 'name' => $parent_name,
364 'errors' => [
365 'variations' => new WP_Error( '80-1', 'Product skipped due to "Any" variations' )
366 ]
367 ];
368 }
369 else {
370 // a list of used skus, to prevent "duplicate sku" errors when variations have no own sku, only on parent level
371 $groups_skus_list = [];
372 $groups_count = count( $variations_groups );
373
374 foreach ( $variations_groups as $variations_group ) {
375
376 // gather variable (parent) product data
377 $node_data = array_merge(
378 [
379 'id' => apply_filters( 'dicha_skroutz_feed_custom_product_id', $variations_group['unique_id'], $product, $this->feed_type ), // if no size variation grouping, this will be replaced later with variation ID
380 'name' => $variations_group['group_name']
381 ],
382 $parent_level_data,
383 $this->get_variations_group_data( $product, $variations_group['group_variations'], $has_size_variations, $groups_skus_list, $groups_count ),
384 );
385
386 // check if errors exist
387 $data_contain_no_errors = $this->detect_data_errors( $node_data );
388
389 $node_data = apply_filters( 'dicha_skroutz_feed_custom_node_data', $node_data, $product, $variations_group, $this->feed_type );
390
391 if ( $data_contain_no_errors ) {
392 $this->products_for_export[ $variations_group['unique_id'] ] = $node_data;
393 }
394 }
395 }
396 }
397 }
398
399 // Clear runtime cache in the end to free resources
400 $this->flush_runtime_cache();
401 }
402
403
404 /**
405 * Wrapper function for XML generation when triggered manually from the button inside Tools section.
406 * Redirects automatically to XML settings page after XML generation is completed.
407 *
408 * @return void
409 */
410 public function create_feed_manual_mode() {
411
412 $feed_generation_result = $this->create_feed();
413 $result_param_value = $feed_generation_result ? 1 : 0;
414
415 // enable this after testing
416 wp_redirect( admin_url( 'admin.php?page=' . DICHA_SKROUTZ_FEED_SLUG . '&feed_success=' . $result_param_value ) );
417 exit;
418 }
419
420
421 /**
422 * Initializes options.
423 *
424 * @return void
425 */
426 private function init_options(): void {
427
428 $options = [
429 'manufacturer' => $this->prefix_attributes( get_option( 'dicha_skroutz_feed_manufacturer', [] ) ),
430 'color' => $this->prefix_attributes( get_option( 'dicha_skroutz_feed_color', [] ) ),
431 'size' => $this->prefix_attributes( get_option( 'dicha_skroutz_feed_size', [] ) ),
432 'title_attributes' => $this->prefix_attributes( get_option( 'dicha_skroutz_feed_title_attributes', [] ) ),
433 'xml_availability' => get_option( 'dicha_skroutz_feed_availability' ),
434 'include_backorders' => get_option( 'dicha_skroutz_feed_include_backorders' ),
435 'description' => get_option( 'dicha_skroutz_feed_description', 'short' ),
436 'global_markup' => get_option( 'dicha_skroutz_feed_global_markup', '' ),
437 'flat_rate' => get_option( 'dicha_skroutz_feed_shipping_cost' ),
438 'flat_rate_free' => get_option( 'dicha_skroutz_feed_free_shipping' ),
439 'selected_cats' => get_option( 'dicha_skroutz_feed_filter_categories', [] ),
440 'selected_tags' => get_option( 'dicha_skroutz_feed_filter_tags', [] ),
441 'cats_incl_mode' => get_option( 'dicha_skroutz_incl_excl_mode_categories' ),
442 'tags_incl_mode' => get_option( 'dicha_skroutz_incl_excl_mode_tags' ),
443 'cats_with_no_vars_support' => (array) apply_filters( 'dicha_skroutz_feed_cats_with_no_variations_support', [], $this->feed_type ),
444 ];
445
446 $this->options = apply_filters( 'dicha_skroutz_feed_custom_options', $options, $this->feed_type );
447 }
448
449
450 /**
451 * Initializes data helper class.
452 *
453 * @return void
454 */
455 private function init_data_helper(): void {
456
457 require_once( 'class-dc-skroutz-feed-data-helper.php' );
458
459 $this->data_helper = new Dicha_Skroutz_Feed_Data_Helper( $this->options, $this->feed_type );
460 }
461
462
463 /**
464 * Fetches WooCommerce products for export, depending on options and filters.
465 *
466 * @return array Array of WooCommerce products.
467 */
468 private function collect_products_for_export(): array {
469
470 $query_args = [
471 'return' => 'ids',
472 'limit' => -1,
473 'type' => [ 'simple', 'variable' ],
474 'visibility' => 'catalog',
475 'status' => 'publish',
476 'virtual' => false,
477 'downloadable' => false,
478 'stock_status' => wc_string_to_bool( $this->options['include_backorders'] ) ? [ 'instock', 'onbackorder' ] : 'instock',
479 ];
480
481 // include/exclude products by category
482 if ( ! empty( $selected_product_cat_terms = $this->options['selected_cats'] ) ) {
483
484 if ( wc_string_to_bool( $this->options['cats_incl_mode'] ) ) {
485 $query_args['product_category_id'] = $selected_product_cat_terms;
486 }
487 else {
488 $query_args['dicha_exclude_product_category_id'] = $selected_product_cat_terms;
489 }
490 }
491
492 // include/exclude products by tag
493 if ( ! empty( $selected_product_tag_terms = $this->options['selected_tags'] ) ) {
494
495 if ( wc_string_to_bool( $this->options['tags_incl_mode'] ) ) {
496 $query_args['product_tag_id'] = $selected_product_tag_terms;
497 }
498 else {
499 $query_args['dicha_exclude_product_tag_id'] = $selected_product_tag_terms;
500 }
501 }
502
503 $query_args = apply_filters( 'dicha_skroutz_feed_product_query_args', $query_args, $this->options, $this->feed_type );
504
505 return wc_get_products( $query_args );
506 }
507
508
509 /**
510 * Builds node data for simple products.
511 *
512 * @param $product WC_Product_Simple
513 *
514 * @return array
515 */
516 private function get_simple_product_data( WC_Product_Simple $product ): array {
517 return [
518 'name' => $this->data_helper->skroutz_get_name( $product ),
519 'link' => $this->data_helper->skroutz_get_url( $product ),
520 'image' => $this->data_helper->skroutz_get_main_image_url( $product ),
521 'additional_imageurl' => $this->data_helper->skroutz_get_additional_images( $product ),
522 'category' => $this->data_helper->skroutz_get_category( $product ),
523 'price_with_vat' => $this->data_helper->skroutz_get_price( $product ),
524 'vat' => $this->data_helper->skroutz_get_vat( $product ),
525 'availability' => $this->data_helper->skroutz_get_availability( $product ),
526 'manufacturer' => $this->data_helper->skroutz_get_manufacturer( $product ),
527 'mpn' => $this->data_helper->skroutz_get_mpn( $product ),
528 'ean' => $this->data_helper->skroutz_get_ean( $product ),
529 'size' => $this->data_helper->skroutz_get_size( $product ),
530 'weight' => $this->data_helper->skroutz_get_weight( $product ),
531 'shipping_costs' => $this->data_helper->skroutz_get_shipping( $product ),
532 'color' => $this->data_helper->skroutz_get_color( $product ),
533 'description' => $this->data_helper->skroutz_get_description( $product ),
534 'quantity' => $this->data_helper->skroutz_get_quantity( $product ),
535 ];
536 }
537
538
539 /**
540 * Builds node data for variable products (Only parent-related data).
541 *
542 * @param $parent_product WC_Product_Variable
543 *
544 * @return array
545 */
546 private function get_variable_parent_level_data( WC_Product_Variable $parent_product ): array {
547 return [
548 'link' => $this->data_helper->skroutz_get_url( $parent_product ),
549 'category' => $this->data_helper->skroutz_get_category( $parent_product ),
550 'price_with_vat' => $this->data_helper->skroutz_get_price( $parent_product ),
551 'vat' => $this->data_helper->skroutz_get_vat( $parent_product ),
552 'availability' => $this->data_helper->skroutz_get_availability( $parent_product ),
553 'manufacturer' => $this->data_helper->skroutz_get_manufacturer( $parent_product ),
554 'mpn' => $this->data_helper->skroutz_get_mpn( $parent_product ), // todo check if same mpn in diff groups cause validation error (ID:1186)
555 'ean' => $this->data_helper->skroutz_get_ean( $parent_product ),
556 'shipping_costs' => $this->data_helper->skroutz_get_shipping( $parent_product ),
557 'description' => $this->data_helper->skroutz_get_description( $parent_product ),
558 ];
559 }
560
561
562 /**
563 * Builds node data for variable products (Variation groups data).
564 *
565 * @param WC_Product_Variable $parent_product The parent product
566 * @param WC_Product_Variation[] $group_variations An array with this group's variations
567 * @param bool $has_size_variations True if parent product has "size" variation attributes
568 * @param array $groups_skus_list A list with unique group SKUs
569 * @param int $groups_count The total number of variation groups
570 *
571 * @return array
572 */
573 private function get_variations_group_data( WC_Product_Variable $parent_product, array $group_variations, bool $has_size_variations, array &$groups_skus_list, int $groups_count ): array {
574
575 // Protect against product data error - All groups with no size vars, should have exactly 1 group variation
576 // Usually with variations not showing in product page because their variations attributes were removed
577 if ( ! $has_size_variations && count( $group_variations ) > 1 ) {
578 $variable_group_data['variations'] = new WP_Error( '80-3', 'Product variation data has critical errors. Check your product.' );
579 return $variable_group_data;
580 }
581
582 $group_color = $group_image = $group_link = $group_sku = '';
583 $variable_group_data = $group_sizes = $group_additional_images = $variation_nodes = [];
584
585 // Get parent stock if manage stock happens on parent level
586 // if this happens, stock status field (instock/outofstock/backorder) disappears from variations tabs
587 // To set stock for a single variation, you should enable manage stock and add stock quantity
588 $parent_manages_stock = $parent_product->managing_stock();
589 $parent_stock = $parent_manages_stock ? max( $parent_product->get_stock_quantity(), 0 ) : false;
590 $group_quantity = $parent_stock !== false ? $parent_stock : 0;
591
592 // Get parent weight - If it's empty, try to get weight if exists on any variation
593 $group_weight = $this->data_helper->skroutz_get_weight( $parent_product );
594
595 // parent main product image
596 $parent_main_image = $this->data_helper->skroutz_get_main_image_url( $parent_product );
597
598 // if parent product has only one group (usually just size variations, without non-size attrs), then keep parent mpn for whole group
599 // if multiple groups exist, leave empty so that later gets filled with a variation's sku (avoid duplicate sku in different groups)
600 if ( $groups_count === 1 ) {
601 $group_sku = $this->data_helper->skroutz_get_mpn( $parent_product );
602 }
603
604 // if variations have "skroutz price" (triggers parent price recalculation in the end)
605 $variations_have_skroutz_price = false;
606
607 foreach ( $group_variations as $variation ) {
608
609 // Skip variation from manual filter
610 $exclude_variation = $this->data_helper->skroutz_exclude_variation_from_xml( $variation, $parent_product );
611
612 if ( $exclude_variation ) {
613
614 $exclude_error_data = new WP_Error( '10-4', 'Η παραλλαγή έχει εξαιρεθεί λόγω το�
615 φίλτρο�
616 `dicha_skroutz_feed_exclude_variation_from_xml`' );
617
618 if ( ! $has_size_variations ) {
619 $variable_group_data['exclude_xml'] = $exclude_error_data;
620 }
621 else {
622 $variation_nodes[] = [
623 'variationid' => $variation->get_id(),
624 'exclude_xml' => $exclude_error_data
625 ];
626 }
627
628 // continue foreach loop to next group variation
629 continue;
630 }
631
632
633 // get variation manage stock - Returns true/false or 'parent' if managing stock happens on parent level
634 $variation_manages_stock = $variation->get_manage_stock();
635
636 // if variation not managing stock, but parent does, then variation quantity equals parent quantity
637 // in this rare edge case, the total parent stock will not match with the variations' stock sum, but it is more correct in this way
638 if ( $parent_manages_stock && 'parent' === $variation_manages_stock ) {
639 $variation_quantity = $parent_stock;
640 }
641 else {
642 $variation_quantity = $this->data_helper->skroutz_get_quantity( $variation );
643
644 // Create an error if variation is out of stock
645 if ( ! is_wp_error( $variation_quantity ) && $variation_quantity == 0 ) {
646 $variation_quantity = new WP_Error( '10-1', 'Η κατάσταση αποθέματος της παραλλαγής είναι εξαντλημένη' );
647 }
648
649 // If error exists, add it to the <quantity> node in order to skip product from XML and continue to next variation
650 // Skip if variation is out of stock
651 // Skip if variation is on backorder and backorders are not allowed based on settings
652 if ( is_wp_error( $variation_quantity ) ) {
653
654 if ( ! $has_size_variations ) {
655 $group_quantity = $variation_quantity;
656 }
657 else {
658 $variation_nodes[] = [
659 'variationid' => $variation->get_id(),
660 'quantity' => $variation_quantity
661 ];
662 }
663
664 // continue foreach loop to next group variation
665 continue;
666 }
667 else {
668 $group_quantity += $variation_quantity;
669 }
670 }
671
672
673 if ( empty( $group_sku ) ) {
674 // variation sku (not inheriting parent's)
675 $group_sku = $this->data_helper->skroutz_get_mpn( $variation, 'not_inherit_from_parent' );
676 }
677
678 if ( empty( $group_link ) ) {
679 $group_link = $this->data_helper->skroutz_get_url( $variation, $has_size_variations );
680 }
681
682 // Get variation weight if not set on parent level
683 // (tip: skroutz supports weight on parent level only, not variation level)
684 if ( empty( $group_weight ) ) {
685 $variation_weight = $this->data_helper->skroutz_get_weight( $variation );
686 $group_weight = $variation_weight;
687 }
688
689 // calculate variation size and add it to group sizes
690 $variation_size = $this->data_helper->skroutz_get_size( $variation );
691
692 if ( ! empty( $variation_size ) ) {
693 $group_sizes[] = $variation_size;
694 }
695
696 // calculate variation color and set group color
697 if ( empty( $group_color ) ) {
698 $group_color = $this->data_helper->skroutz_get_color( $variation );
699 }
700
701 // get variation image, if not exists returns parent main image
702 $variation_image = $this->data_helper->skroutz_get_main_image_url( $variation );
703
704 // If variation image is different from main product image, then keep the more specific variation image
705 if ( empty( $group_image ) || $variation_image !== $parent_main_image ) {
706 $group_image = $variation_image;
707 }
708
709 /*
710 * If variations have extra gallery images in some custom field, you can use this filter
711 * to add these images in this array.
712 * If custom images are found, they are shown in the <additional_imageurl> nodes.
713 * If no custom images found, then the parent's (variable) gallery images will be shown in this field.
714 */
715 $variation_additional_images = apply_filters( 'dicha_skroutz_feed_custom_variation_additional_images', [], $variation, $parent_product, $this->feed_type );
716
717 if ( ! empty( $variation_additional_images ) ) {
718 $group_additional_images = array_merge( $group_additional_images, $variation_additional_images );
719 }
720
721 $variation_id = $variation->get_id();
722
723 if ( $this->wpml_do_lang_switch ) {
724 // maybe add with filter
725 $variation_id = apply_filters( 'wpml_object_id', $variation_id, 'product_variation', true, $this->original_site_lang );
726 }
727
728 // if no size variations, then this group has only one variation because no size grouping happens
729 // if no size groups, then don't add size_variations node, but add these data as main nodes
730 if ( ! $has_size_variations ) {
731 $variable_group_data = [
732 'id' => apply_filters( 'dicha_skroutz_feed_custom_variation_id', $variation_id, $variation, $parent_product, $this->feed_type ), // We replace group unique_id with variation ID
733 'availability' => $this->data_helper->skroutz_get_availability( $variation ),
734 'price_with_vat' => $this->data_helper->skroutz_get_price( $variation ),
735 'vat' => $this->data_helper->skroutz_get_vat( $variation ),
736 'ean' => $this->data_helper->skroutz_get_ean( $variation ),
737 ];
738 }
739 else {
740 // detect if "skroutz price" exists at least on one group variation (sufficient to trigger parent price recalculation)
741 if ( ! $variations_have_skroutz_price && max( 0.0, $variation->get_meta( 'dicha_skroutz_feed_price' ) ) > 0 ) {
742 $variations_have_skroutz_price = true;
743 }
744
745 // if size variations exist, then add size_variations nodes
746 $variation_nodes[] = [
747 'variationid' => apply_filters( 'dicha_skroutz_feed_custom_variation_id', $variation_id, $variation, $parent_product, $this->feed_type ),
748 'availability' => $this->data_helper->skroutz_get_availability( $variation ),
749 'size' => $variation_size,
750 'quantity' => apply_filters( 'dicha_skroutz_feed_custom_variation_quantity', $variation_quantity, $variation, $parent_product, $this->feed_type ),
751 'price_with_vat' => $this->data_helper->skroutz_get_price( $variation ),
752 'link' => $this->data_helper->skroutz_get_url( $variation ),
753 'mpn' => $this->data_helper->skroutz_get_mpn( $variation, 'not_inherit_from_parent' ), // if empty, then empty, don't inherit
754 'ean' => $this->data_helper->skroutz_get_ean( $variation ),
755 ];
756 }
757 }
758
759
760 // if group sku empty, then all variations have no sku in their own tab, so get from parent
761 if ( empty( $group_sku ) ) {
762 $group_sku = $this->data_helper->skroutz_get_mpn( $parent_product );
763 }
764
765 // if this sku already exists in another variation group, add a suffix to make it unique
766 if ( ! empty( $group_sku ) && in_array( $group_sku, $groups_skus_list ) ) {
767 $current_groups_count = count( $groups_skus_list );
768 $group_sku .= '-' . $current_groups_count;
769 }
770
771 // add unique sku to variation group data, and in the unique skus list
772 if ( ! empty( $group_sku ) ) {
773 $variable_group_data['mpn'] = $group_sku;
774 $groups_skus_list[] = $group_sku;
775 }
776
777 $variable_group_data['link'] = $group_link;
778 $variable_group_data['weight'] = $group_weight;
779 $variable_group_data['quantity'] = $group_quantity;
780 $variable_group_data['image'] = $group_image;
781
782 // if not additional images found for variations, then use parents gallery images
783 $group_additional_images = array_unique( array_filter( $group_additional_images ) );
784
785 if ( empty( $group_additional_images ) ) {
786 $group_additional_images = $this->data_helper->skroutz_get_additional_images( $parent_product );
787 }
788
789 if ( ! empty( $group_additional_images ) ) {
790 $variable_group_data['additional_imageurl'] = $group_additional_images;
791 }
792
793 // if color empty, then don't add in array, to keep original parent data
794 if ( ! empty( $group_color ) ) {
795 $variable_group_data['color'] = $group_color;
796 }
797
798 // if size empty, then don't add in array, to keep original parent data
799 if ( ! empty( $group_sizes ) ) {
800 $variable_group_data['size'] = implode( ',', $group_sizes );
801 }
802
803 // Add variation nested nodes if size variations exist and have no errors
804 if ( ! empty( $variation_nodes ) ) {
805
806 // clean variation nodes with errors
807 foreach ( $variation_nodes as $node_key => $variation_node_data ) {
808
809 $nodes_with_errors = array_filter( $variation_node_data, 'is_wp_error' );
810
811 if ( ! empty( $nodes_with_errors ) ) {
812
813 $this->products_with_errors[ $variation_node_data['variationid'] ] = [
814 'name' => $parent_product->get_name() . ' - Variation #' . $variation_node_data['variationid'],
815 'errors' => $nodes_with_errors
816 ];
817
818 unset( $variation_nodes[ $node_key ] );
819 }
820 }
821
822 // if variation nodes still exist, add them to XML
823 if ( ! empty( $variation_nodes ) ) {
824 $variable_group_data['variations'] = $variation_nodes;
825
826 // also recalculate group price if variations have "skroutz price"
827 if ( $variations_have_skroutz_price ) {
828 $variable_group_data['price_with_vat'] = min( array_column( $variation_nodes, 'price_with_vat' ) );
829 }
830 }
831 else {
832 // if empty, then all variations have errors -> add a WP_Error to force skipping for the parent product
833 $variable_group_data['variations'] = new WP_Error( '80-2', 'All "size" variations nodes have errors or are hidden from XML' );
834 }
835 }
836
837 return $variable_group_data;
838 }
839
840
841 /**
842 * Checks if a "size" attribute exists.
843 *
844 * @param $variation_attributes array of variation attributes slugs and values
845 *
846 * @return bool True if a "size" attribute exists.
847 */
848 private function has_size_options( array $variation_attributes, array $size_var_atts_for_product ): bool {
849 return ! empty( array_intersect( array_keys( $variation_attributes ), $size_var_atts_for_product ) );
850 }
851
852
853 /**
854 * Checks if any non "size" attribute exists.
855 *
856 * @param $variation_attributes array of variation attributes slugs and values
857 *
858 * @return bool True if any non "size" attribute exists.
859 */
860 private function has_non_size_options( array $variation_attributes, array $size_var_atts_for_product ): bool {
861 return ! empty( array_diff( array_keys( $variation_attributes ), $size_var_atts_for_product ) );
862 }
863
864
865 /**
866 * Checks if a "color" attribute exists.
867 *
868 * @param $variation_attributes array of variation attributes slugs and values
869 *
870 * @return bool True if a "color" attribute exists.
871 */
872 private function has_color_options( array $variation_attributes ): bool {
873 return ! empty( array_intersect( array_keys( $variation_attributes ), $this->options['color'] ) );
874 }
875
876
877 /**
878 * Adds the 'pa_' prefix to attributes slugs, only if missing.
879 *
880 * @param $atts_array string[] Attributes slugs.
881 *
882 * @return string[]
883 */
884 private function prefix_attributes( array $atts_array ): array {
885 return array_map( function( $v ) {
886 if ( empty( $v ) ) return $v;
887 return strpos( $v, 'pa_' ) === 0 ? $v : 'pa_' . $v;
888 }, $atts_array );
889 }
890
891
892 /**
893 * Detect if WP_Errors exist in node data and also adds the problematic node to the errors array.
894 *
895 * @param $node_data array The node data.
896 *
897 * @return bool True if no errors found. False otherwise.
898 */
899 private function detect_data_errors( array $node_data ): bool {
900
901 $data_contain_no_errors = true;
902 $nodes_with_errors = array_filter( $node_data, 'is_wp_error' );
903
904 if ( ! empty( $nodes_with_errors ) ) {
905
906 $this->products_with_errors[ $node_data['id'] ] = [
907 'name' => $node_data['name'],
908 'errors' => $nodes_with_errors
909 ];
910
911 $data_contain_no_errors = false;
912 }
913
914
915 return $data_contain_no_errors;
916 }
917
918
919 /**
920 * Prepares error data to be printed in log files.
921 *
922 * @param $error_data array Original error data containing WP_Errors.
923 *
924 * @return array|false Error data ready for printing in log file, or false if no errors remain after removing unwanted errors.
925 */
926 private function prepare_error_for_printing( array $error_data ) {
927
928 $errors_for_print = [];
929
930 /** @var WP_Error $wp_error */
931 foreach ( $error_data['errors'] as $wp_error ) {
932
933 if ( ! is_wp_error( $wp_error ) ) continue;
934
935 $error_code = $wp_error->get_error_code();
936
937 // Exclude errors which are about stock
938 // Not really errors and not so important to log them
939 if ( in_array( $error_code, [ '10-1', '10-2' ] ) ) continue;
940
941 $errors_for_print[] = [
942 'code' => $error_code,
943 'message' => $wp_error->get_error_message()
944 ];
945 }
946
947 if ( ! empty( $errors_for_print ) ) {
948 $error_data['errors'] = $errors_for_print;
949 return $error_data;
950 }
951
952 return false;
953 }
954
955
956 /**
957 * Creates a new WC log file and prints script info and products with errors.
958 *
959 * @return void
960 */
961 private function write_logs(): void {
962
963 $log_level = get_option( 'dicha_skroutz_feed_log_level', 'minimal' );
964
965 if ( 'disabled' === $log_level ) return;
966
967 if ( 'full' === $log_level ) {
968 $errors_in_readable_form = array_filter( array_map( [ $this, 'prepare_error_for_printing' ], $this->products_with_errors ) );
969 }
970
971 $start_time = gmdate( 'Y-m-d H:i:s', floor( $this->start_full_time ) );
972 $xml_generation_duration = intval( $this->xml_generation_time - $this->start_full_time );
973 $full_script_duration = intval( microtime( true ) - $this->start_full_time );
974
975 $logger = wc_get_logger();
976 $context = [ 'source' => DICHA_SKROUTZ_FEED_SLUG . '-' . get_date_from_gmt( $start_time, 'Hi' ) ];
977
978 if ( ! empty( $this->xml_creation_errors ) ) {
979 $logger->critical( 'XML file generation failed. Errors: ' . implode( ', ', $this->xml_creation_errors ), $context );
980 }
981
982 $logger->info( sprintf( 'XML generation started for feed type: %s', $this->feed_type ), $context );
983 $logger->info( sprintf( 'XML creation start time: %s', get_date_from_gmt( $start_time, 'd-m-Y H:i:s' ) ), $context );
984 $logger->info( sprintf( 'XML generation duration: %d minutes and %s seconds', (int) floor( $xml_generation_duration / 60 ), round( $xml_generation_duration % 60 ) ), $context );
985 $logger->info( sprintf( 'Full script execution duration: %d minutes and %s seconds', (int) floor( $full_script_duration / 60 ), round( $full_script_duration % 60 ) ), $context );
986 $logger->info( sprintf( 'Peak memory usage: %s MB', round( memory_get_peak_usage( true ) / ( 1024 * 1024 ), 2 ) ), $context );
987 $logger->info( sprintf( 'Max memory limit: %s MB', round( $this->memory_limit / ( 1024 * 1024 ), 2 ) ), $context );
988
989 if ( 'full' === $log_level ) {
990 $logger->notice( 'Product nodes with errors:', $context );
991 $logger->notice( wc_print_r( $errors_in_readable_form, true ), $context );
992 }
993 }
994
995
996 /**
997 * Checks if currently used memory is close to max memory limit and in that case flushes the object cache.
998 * Except for object cache, it should be left free memory for PHP internal workings and also a memory size equal to the XML filesize for the file_put_contents() to succeed.
999 *
1000 * After testing, it seems that 75% of RAM is a nice limit for cleaning the OB cache, for max memory limit larger than 512MB.
1001 * Tested OK for 15k products with variations, with 75% limit on max memory size of 512MB.
1002 *
1003 * If the memory limit is very low like 256MB, then use 80% of memory as limit, but there is a limit here on the total exported products because of low memory.
1004 * Tested OK for 5k products with variations, with 80% limit on max memory size of 256MB.
1005 *
1006 * @return void
1007 */
1008 private function maybe_flush_runtime_cache(): void {
1009
1010 // 80% if under 500MB, 75% if over 500MB
1011 $limit_ram_usage_percent = $this->memory_limit < 524288000 ? 0.8 : 0.75;
1012
1013 $max_memory_to_use = min( $limit_ram_usage_percent * $this->memory_limit, $this->memory_limit - 52428800 ); // always leave a minimum of 50MB free
1014
1015 if ( memory_get_usage() > $max_memory_to_use ) {
1016
1017 $this->flush_runtime_cache();
1018 }
1019 }
1020
1021
1022 /**
1023 * Clears the runtime object cache to free memory.
1024 * After each iteration, the object cache is increasing, which makes the used memory really huge for eshops with thousands of products.
1025 *
1026 * @return void
1027 */
1028 private function flush_runtime_cache(): void {
1029
1030 /*
1031 * Calling wp_cache_flush_runtime() lets us clear the runtime cache without invalidating the external object
1032 * cache, so we will always prefer this when it is available (works for WordPress v6.1+).
1033 */
1034 if ( function_exists( 'wp_cache_supports' ) && wp_cache_supports( 'flush_runtime' ) ) {
1035 wp_cache_flush_runtime();
1036 }
1037 else {
1038 wp_cache_flush();
1039 }
1040
1041 $GLOBALS['wpdb']->flush();
1042 }
1043
1044
1045 /**
1046 * Find the max memory limit for this PHP script.
1047 *
1048 * @return void
1049 * @noinspection PhpMissingBreakStatementInspection
1050 */
1051 private function find_max_memory_limit(): void {
1052
1053 $memory_limit = ini_get( 'memory_limit' );
1054
1055 if ( empty( $memory_limit ) ) {
1056 $memory_limit = '256M';
1057 }
1058 elseif ( $memory_limit == -1 ) {
1059 $memory_limit = '1G';
1060 }
1061
1062 $this->memory_limit = preg_replace_callback('/^\s*([\d.]+)\s*(?:([kmgt]?)b?)?\s*$/i', function($matches) {
1063 switch ( strtolower( $matches[2] ) ) {
1064 case 't': $matches[1] *= 1024;
1065 case 'g': $matches[1] *= 1024;
1066 case 'm': $matches[1] *= 1024;
1067 case 'k': $matches[1] *= 1024;
1068 }
1069 return $matches[1];
1070 }, $memory_limit );
1071 }
1072
1073
1074 /**
1075 * Creates a mapping between unserialized and serialized slugs for WC attributes.
1076 *
1077 * @return void
1078 */
1079 private function setup_wc_attributes_list(): void {
1080
1081 global $wc_product_attributes;
1082
1083 foreach ( $wc_product_attributes as $wc_product_attribute_slug => $wc_product_attribute_obj ) {
1084
1085 if ( ! isset( $wc_product_attribute_obj->attribute_id ) || $wc_product_attribute_obj->attribute_id < 1 ) continue;
1086
1087 $this->wc_product_attributes_sanitized[ 'pa_' . sanitize_title( $wc_product_attribute_obj->attribute_name ) ] = $wc_product_attribute_slug;
1088 }
1089 }
1090
1091
1092
1093 /**
1094 *********************************
1095 ***** PRODUCT QUERY FILTERS *****
1096 *********************************
1097 */
1098
1099 /**
1100 * Handle custom params in wc_get_products query.
1101 *
1102 * @param array $query Args for WP_Query.
1103 * @param array $query_vars Query vars from WC_Product_Query.
1104 *
1105 * @return array modified $query
1106 */
1107 function handle_skroutz_products_query_vars( array $query, array $query_vars ): array {
1108
1109 if ( ! empty( $query_vars['dicha_exclude_product_category_id'] ) ) {
1110 $query['tax_query'][] = [
1111 'taxonomy' => 'product_cat',
1112 'field' => 'term_id',
1113 'terms' => $query_vars['dicha_exclude_product_category_id'],
1114 'include_children' => true,
1115 'operator' => 'NOT IN',
1116 ];
1117 }
1118
1119 if ( ! empty( $query_vars['dicha_exclude_product_tag_id'] ) ) {
1120 $query['tax_query'][] = [
1121 'taxonomy' => 'product_tag',
1122 'field' => 'term_id',
1123 'terms' => $query_vars['dicha_exclude_product_tag_id'],
1124 'include_children' => true,
1125 'operator' => 'NOT IN',
1126 ];
1127 }
1128
1129 return $query;
1130 }
1131
1132
1133
1134 /**
1135 *****************************
1136 ***** EXPORT & SAVE XML *****
1137 *****************************
1138 */
1139
1140 /**
1141 * Saves the data for export into an XML file.
1142 *
1143 * @return void
1144 */
1145 private function saveXML(): void {
1146
1147 require_once( 'mod_simplexml.php' );
1148
1149 echo "#========================================================================#\n";
1150 echo "-> Saving Products XML...\n";
1151
1152 $data_for_export = [];
1153
1154 try {
1155 $dt = new DateTime( "now", new DateTimeZone( 'Europe/Athens' ) );
1156 $data_for_export['created_at'] = $dt->format( 'Y-m-d H:i:s' );
1157 }
1158 catch ( Exception $e ) {}
1159
1160 $data_for_export['products'] = $this->products_for_export;
1161
1162 $this->decide_nodes_for_cdata();
1163
1164 // creating object of SimpleXMLElement
1165 $xml_data = new Dicha_SimpleXMLElement_Extension( '<?xml version="1.0" encoding="UTF-8"?><mywebstore></mywebstore>' );
1166
1167 // function call to convert array to xml
1168 $this->xmlProcess( $data_for_export, $xml_data );
1169
1170 $dom = new DOMDocument( "1.0" );
1171 $dom->preserveWhiteSpace = false;
1172 $dom->formatOutput = apply_filters( 'dicha_skroutz_feed_format_output', true, $this->feed_type );
1173 $dom->loadXML( $xml_data->asXML(), LIBXML_PARSEHUGE );
1174
1175 // Include the required files for WP_Filesystem
1176 if ( ! function_exists( 'WP_Filesystem' ) ) {
1177 require_once( ABSPATH . 'wp-admin/includes/file.php' );
1178 }
1179
1180 // Initialize the WP_Filesystem
1181 global $wp_filesystem;
1182 WP_Filesystem();
1183
1184 $xml_location_path = Dicha_Skroutz_Feed_Admin::get_default_feed_filepath();
1185
1186 // create folder inside /uploads/ if not exist
1187 if ( ! $wp_filesystem->exists( $xml_location_path ) ) {
1188 $folder_creation_result = $wp_filesystem->mkdir( $xml_location_path, 0755 );
1189
1190 if ( ! $folder_creation_result ) {
1191 $this->xml_creation_errors[] = 'Could not create folder ' . $xml_location_path . ' inside /uploads/';
1192 return;
1193 }
1194 }
1195
1196 $feed_filename = Dicha_Skroutz_Feed_Admin::get_feed_filename( $this->feed_type ) . '.xml';
1197 $filename_with_path = $xml_location_path . $feed_filename;
1198
1199 // save XML
1200 $xml_datas = $dom->saveXML();
1201
1202 $file_creation_result = $wp_filesystem->put_contents( $filename_with_path, $xml_datas );
1203
1204 if ( ! $file_creation_result ) {
1205 $this->xml_creation_errors[] = 'Could not create the file: ' . $filename_with_path;
1206 return;
1207 }
1208
1209 // Zip XML
1210 // todo always create zip
1211 if ( apply_filters( 'dicha_skroutz_feed_zip_xml', false, $this->feed_type ) && class_exists( 'ZipArchive' ) ) {
1212
1213 $zip = new ZipArchive();
1214 $success_open = $zip->open( $filename_with_path . '.zip', ZipArchive::CREATE );
1215
1216 if ( $success_open === true ) {
1217 $zip->addFile( $filename_with_path, $feed_filename );
1218 $zip->close();
1219 }
1220 }
1221
1222 echo "-> Saving: DONE!\n";
1223 echo "#========================================================================#\n";
1224
1225 // save last successful run time in UTC timestamp
1226 update_option( 'dicha_skroutz_feed_last_run', current_time( 'timestamp', true ), false );
1227 }
1228
1229
1230 /**
1231 * Recursive function to create the XML nodes.
1232 *
1233 * Skroutz validator rules (Jun 2024):
1234 * Manufacturer: Can be missing 100% *** can NOT be all empty *** can NOT be only one node *** can be some filled, some missing, some empty
1235 * Color: Can be missing 100% *** can NOT be all empty *** can NOT be only one node, at least 2 even if one of them empty *** can be some filled, some missing, some empty
1236 * Size: Can be missing 100% *** can be all empty *** can be only one node *** can be some filled, some missing, some empty
1237 * Ean: Can be missing 100% *** can NOT be all empty *** can NOT be only one node, at least 2 even if one of them empty *** can be some filled, some missing, some empty
1238 * Weight: Can be missing 100% *** can NOT be all empty *** can NOT be only one node, at least 2 even if one of them empty *** can be some filled, some missing, some empty
1239 *
1240 *
1241 * @param $data array
1242 * @param $xml_data Dicha_SimpleXMLElement_Extension | SimpleXMLElement
1243 * @param $parent_node string
1244 *
1245 * @return void
1246 */
1247 private function xmlProcess( array $data, &$xml_data, string $parent_node = 'root' ): void {
1248
1249 foreach ( $data as $key => $value ) {
1250
1251 if ( 'products' === $parent_node ) {
1252 $node_name = 'product';
1253 }
1254 elseif ( 'variations' === $parent_node ) {
1255 $node_name = 'variation';
1256 }
1257 else {
1258 $node_name = (string) $key;
1259 }
1260
1261
1262 if ( is_array( $value ) ) {
1263
1264 if ( 'additional_imageurl' === $node_name ) {
1265
1266 // Exception: if additional images, then don't create sub-nodes, but add same-level nodes
1267 if ( in_array( $node_name, $this->fields_in_cdata ) ) {
1268 foreach ( $value as $additional_img_url ) {
1269 $xml_data->addChildCData( $node_name, (string) $additional_img_url );
1270 }
1271 }
1272 else {
1273 foreach ( $value as $additional_img_url ) {
1274 $xml_data->addChild( $node_name, (string) $additional_img_url );
1275 }
1276 }
1277 }
1278 else {
1279 $sub_node = $xml_data->addChild( $node_name );
1280 $this->xmlProcess( $value, $sub_node, $node_name );
1281 }
1282 }
1283 else {
1284
1285 // don't print empty nodes
1286 if ( $value === '' || $value === NULL ) continue;
1287
1288 if ( in_array( $node_name, $this->fields_in_cdata ) ) {
1289 $xml_data->addChildCData( $node_name, (string) $value );
1290 }
1291 else {
1292 $xml_data->addChild( $node_name, (string) $value );
1293 }
1294 }
1295 }
1296 }
1297
1298
1299 /**
1300 * Set which fields will be wrapped in CDATA.
1301 *
1302 * @return void
1303 */
1304 private function decide_nodes_for_cdata(): void {
1305
1306 $this->fields_in_cdata = apply_filters( 'dicha_skroutz_feed_fields_in_cdata', [
1307 'name',
1308 'link',
1309 'category',
1310 'description',
1311 'image',
1312 'additional_imageurl'
1313 ], $this->feed_type );
1314 }
1315
1316 }