PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / trunk
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! vtrunk
3.8.0 3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 All 112 releases
templately / modules / full-site-import / Utils / FluentImport.php

FluentImport.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! trunk, at modules/full-site-import/Utils/FluentImport.php

546 lines 15.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Fluent Cart Product Importer
4 *
5 * Imports Fluent Cart products from JSON format with images
6 * Replicates the functionality of Fluent Cart's DummyProductService
7 *
8 * @package Templately\Exporter\Core
9 * @since 1.3.4
10 */
11
12 namespace Templately\Modules\FullSiteImport\Utils;
13
14 use Exception;
15 use FluentCart\App\Models\Product;
16 use FluentCart\App\Services\Async\ImageAttachService;
17 use FluentCart\App\Services\DateTime\DateTime;
18 use FluentCart\Framework\Support\Arr;
19 use FluentCart\Framework\Support\Str;
20 use Templately\Utils\Helper;
21
22 /**
23 * FluentImport Class
24 *
25 * Handles importing Fluent Cart products with their images from JSON format
26 * Uses the same approach as Fluent Cart's DummyProductService
27 *
28 * @since 1.3.4
29 */
30 class FluentImport {
31
32 /**
33 * Import file path
34 *
35 * @var string
36 * @since 1.3.4
37 */
38 private $import_path;
39
40 /**
41 * Images directory path
42 *
43 * @var string
44 * @since 1.3.4
45 */
46 private $images_dir;
47
48 /**
49 * Import statistics
50 *
51 * @var array
52 * @since 1.3.4
53 */
54 private $stats = [
55 'products_imported' => 0,
56 'products_failed' => 0,
57 'variations_imported' => 0,
58 'images_imported' => 0,
59 ];
60
61 /**
62 * Product ID mappings (original ID => new ID)
63 *
64 * @var array
65 * @since 1.3.4
66 */
67 private $product_id_mappings = [
68 'succeed' => [],
69 'failed' => [],
70 ];
71
72 /**
73 * Import errors
74 *
75 * @var array
76 * @since 1.3.4
77 */
78 private $errors = [];
79
80 /**
81 * Products array for action hooks
82 *
83 * @var array
84 * @since 1.3.4
85 */
86 public $products = [];
87
88 /**
89 * Constructor
90 *
91 * @param string $import_path Path to the JSON file to import
92 * @since 1.3.4
93 */
94 public function __construct( $import_path ) {
95 $this->import_path = $import_path;
96 $this->images_dir = dirname( $import_path ) . '/images';
97 $this->require_files();
98 }
99
100 /**
101 * Require necessary WordPress files
102 *
103 * @return void
104 * @since 1.3.4
105 */
106 private function require_files() {
107 if ( ! function_exists( 'media_handle_upload' ) ) {
108 require_once ABSPATH . 'wp-admin/includes/image.php';
109 require_once ABSPATH . 'wp-admin/includes/file.php';
110 require_once ABSPATH . 'wp-admin/includes/media.php';
111 }
112 if ( ! function_exists( 'wp_create_term' ) ) {
113 require_once ABSPATH . 'wp-admin/includes/taxonomy.php';
114 }
115 }
116
117 /**
118 * Import products from JSON
119 *
120 * @return array Structured import result with status, errors, and summary
121 * @since 1.3.4
122 */
123 public function import() {
124 try {
125 // Check if file exists
126 if ( ! file_exists( $this->import_path ) ) {
127 throw new Exception( 'Import file not found: ' . $this->import_path );
128 }
129
130 // Check if images directory exists
131 if ( ! file_exists( $this->images_dir ) ) {
132 throw new Exception( 'Images directory not found: ' . $this->images_dir );
133 }
134
135 // Load JSON
136 $json = file_get_contents( $this->import_path );
137 if ( $json === false ) {
138 throw new Exception( 'Failed to read JSON file' );
139 }
140
141 $products = json_decode( $json, true );
142 if ( json_last_error() !== JSON_ERROR_NONE ) {
143 throw new Exception( 'Failed to parse JSON: ' . json_last_error_msg() );
144 }
145
146 if ( empty( $products ) || ! is_array( $products ) ) {
147 return $this->format_result( 'success', [], [] );
148 }
149
150 // Store products for action hooks (similar to WPImport::$posts)
151 $this->products = $this->prepare_products_for_hooks( $products );
152
153 // Trigger Templately-specific import_start action
154 do_action( 'templately_import_start', $this );
155
156 // Import each product
157 foreach ( $products as $product_data ) {
158 // Prepare result array for this product
159 $result = [
160 'succeed' => $this->product_id_mappings['succeed'],
161 'failed' => $this->product_id_mappings['failed'],
162 ];
163
164 try {
165 $product_id = $this->insert_product( $product_data );
166 $this->stats['products_imported']++;
167
168 // Track product ID mapping if original ID exists
169 if ( isset( $product_data['id'] ) ) {
170 $this->product_id_mappings['succeed'][ $product_data['id'] ] = $product_id;
171 }
172
173 // Update result with new mapping
174 $result['succeed'] = $this->product_id_mappings['succeed'];
175
176 // Prepare post-like data for action hook
177 $post_data = $this->prepare_post_data_for_hook( $product_data, $product_id );
178
179 // Trigger process_post action (similar to WPImport)
180 do_action( 'templately_import.process_post', $post_data, $result, $this );
181
182 } catch ( Exception $e ) {
183 $this->stats['products_failed']++;
184 $error_msg = 'FluentImport: Failed to import product: ' . $e->getMessage();
185 $this->errors[] = $error_msg;
186 Helper::log( $error_msg, 'fsi_fluent', 'error' );
187
188 // Track failed product id if original id exists
189 if ( isset( $product_data['id'] ) ) {
190 $this->product_id_mappings['failed'][] = $product_data['id'];
191 }
192
193 // Update result with failed ID
194 $result['failed'] = $this->product_id_mappings['failed'];
195
196 // Prepare post-like data for action hook (even for failed imports)
197 $post_data = $this->prepare_post_data_for_hook( $product_data, null );
198
199 // Trigger process_post action for failed import
200 do_action( 'templately_import.process_post', $post_data, $result, $this );
201 }
202 }
203
204 return $this->format_result( 'success', $this->errors, $this->product_id_mappings );
205
206 } catch ( Exception $e ) {
207 $error_msg = 'Import failed: ' . $e->getMessage();
208 return $this->format_result( 'error', [ $error_msg ], [] );
209 }
210 }
211
212 /**
213 * Format import result in structured JSON format
214 *
215 * @param string $status Status of import (success or error)
216 * @param array $errors Array of error messages
217 * @param array $product_mappings Product ID mappings
218 * @return array Formatted result
219 * @since 1.3.4
220 */
221 private function format_result( $status, $errors, $product_mappings ) {
222 return [
223 'status' => $status,
224 'errors' => $errors,
225 'summary' => [
226 'terms' => [
227 'succeed' => [],
228 'failed' => [],
229 ],
230 'posts' => [
231 'succeed' => $product_mappings['succeed'] ?? [],
232 'failed' => $product_mappings['failed'] ?? [],
233 ],
234 ],
235 ];
236 }
237
238 /**
239 * Prepare products array for action hooks
240 * Converts products to a format similar to WPImport::$posts
241 *
242 * @param array $products Products array from JSON
243 * @return array Prepared products array
244 * @since 1.3.4
245 */
246 private function prepare_products_for_hooks( $products ) {
247 $prepared = [];
248 foreach ( $products as $product ) {
249 $prepared[] = [
250 'post_id' => $product['id'] ?? 0,
251 'post_type' => 'fluent-products',
252 'post_title' => $product['post_title'] ?? '',
253 ];
254 }
255 return $prepared;
256 }
257
258 /**
259 * Prepare post-like data for action hook
260 * Converts product data to a format similar to WPImport post data
261 *
262 * @param array $product_data Product data from JSON
263 * @param int|null $product_id New product ID (null if failed)
264 * @return array Post-like data array
265 * @since 1.3.4
266 */
267 private function prepare_post_data_for_hook( $product_data, $product_id ) {
268 return [
269 'post_id' => $product_data['id'] ?? 0,
270 'post_type' => 'fluent-products',
271 'post_title' => $product_data['post_title'] ?? '',
272 'new_id' => $product_id,
273 ];
274 }
275
276 /**
277 * Insert a single product
278 * Replicates DummyProductService::insert() method
279 *
280 * @param array $product Product data array
281 * @return int The ID of the created product
282 * @throws Exception If product insertion fails
283 * @since 1.3.4
284 */
285 private function insert_product( $product ) {
286 $product_name = Str::slug( $product['post_title'], '-', null );
287 $now = DateTime::gmtNow();
288 $created_date = $now->format( 'Y-m-d H:i:s' );
289 $product_name_suffix = $now->format( 'd-m-Y-H-i-s' );
290
291 $data = [
292 'post_author' => get_current_user_id(),
293 'post_date' => $created_date,
294 'post_date_gmt' => $created_date,
295 'post_content_filtered' => '',
296 'post_status' => 'publish',
297 'post_type' => 'fluent-products',
298 'comment_status' => 'open',
299 'ping_status' => 'closed',
300 'post_password' => '',
301 'post_name' => $product_name . '-' . $product_name_suffix,
302 'to_ping' => '',
303 'pinged' => '',
304 'post_modified' => $created_date,
305 'post_modified_gmt' => $created_date,
306 'post_parent' => 0,
307 'menu_order' => 0,
308 'post_mime_type' => '',
309 'guid' => get_site_url() . '/?items=' . $product_name . '-' . $product_name_suffix,
310 ];
311
312 $product = array_merge( $product, $data );
313 $detail = Arr::get( $product, 'detail', [] );
314 $variant_data = Arr::get( $product, 'variants', [] );
315 $gallery_images = Arr::get( $product, 'gallery', [] );
316 $categories = Arr::get( $product, 'categories', [] );
317
318 // Create product using Fluent Cart's ORM
319 $product_model = Product::query()->create(
320 Arr::except( $product, [ 'detail', 'variants', 'gallery', 'categories' ] )
321 );
322
323 if ( ! $product_model ) {
324 throw new Exception( 'Failed to create product' );
325 }
326
327 // Attach categories
328 $this->attach_terms( $categories, $product_model->ID );
329
330 // Attach gallery images
331 if ( ! empty( $gallery_images ) ) {
332 $this->attach_images_to_product( $product_model->toArray(), $gallery_images );
333
334 // Set featured image from first gallery image
335 $gallery_image_with_id = get_post_meta( $product_model->ID, 'fluent-products-gallery-image', true );
336 if ( isset( $gallery_image_with_id[0] ) ) {
337 set_post_thumbnail( $product_model->ID, Arr::get( $gallery_image_with_id, '0.id' ) );
338 }
339 }
340
341 // Create variants
342 if ( ! empty( $variant_data ) ) {
343 $variants = $product_model->variants()->createMany( $variant_data );
344 $this->stats['variations_imported'] += $variants->count();
345
346 // Attach images to variants
347 foreach ( $variants as $index => $variant ) {
348 $images = Arr::get( $variant_data, $index . '.images', [] );
349 if ( is_array( $images ) && count( $images ) ) {
350 $this->attach_images_to_variant( $variant->id, $images );
351 }
352 }
353
354 // Create product detail with default variation
355 if ( ! empty( $detail ) ) {
356 $detail['post_id'] = $product_model->ID;
357 $detail['default_variation_id'] = $variants->first()->id;
358 $product_model->detail()->create( $detail );
359 }
360 } else {
361 // Create product detail without default variation
362 if ( ! empty( $detail ) ) {
363 $detail['post_id'] = $product_model->ID;
364 $product_model->detail()->create( $detail );
365 }
366 }
367
368 return $product_model->ID;
369 }
370
371 /**
372 * Attach terms to product
373 * Replicates DummyProductService::attachTerms() method
374 *
375 * @param array|string $categories Categories to attach
376 * @param int $post_id Post ID
377 * @return void
378 * @since 1.3.4
379 */
380 private function attach_terms( $categories, $post_id ) {
381 if ( is_string( $categories ) ) {
382 $categories = explode( ',', $categories );
383 }
384
385 $term_ids = [];
386
387 if ( is_array( $categories ) ) {
388 foreach ( $categories as $category ) {
389 $term = wp_create_term( $category, 'product-categories' );
390 if ( ! is_wp_error( $term ) ) {
391 $term_ids[] = $term['term_id'];
392 }
393 }
394 }
395
396 if ( ! empty( $term_ids ) ) {
397 wp_set_post_terms( $post_id, $term_ids, 'product-categories' );
398 }
399 }
400
401 /**
402 * Attach images to product
403 * Uses ImageAttachService similar to DummyProductService
404 *
405 * @param array $product Product array
406 * @param array $images Array of image paths
407 * @return void
408 * @since 1.3.4
409 */
410 private function attach_images_to_product( array $product, array $images ) {
411 if ( empty( $images ) ) {
412 return;
413 }
414
415 $gallery = [];
416
417 foreach ( $images as $image_path ) {
418 $value = $this->add_image_from_path( $image_path, $product['post_title'] );
419 if ( ! empty( $value ) ) {
420 $gallery[] = $value;
421 $this->stats['images_imported']++;
422 }
423 }
424
425 if ( ! empty( $gallery ) ) {
426 update_post_meta( $product['ID'], 'fluent-products-gallery-image', $gallery );
427 }
428 }
429
430 /**
431 * Attach images to variant
432 * Uses ImageAttachService similar to DummyProductService
433 *
434 * @param int $variant_id Variant ID
435 * @param array $images Array of image paths
436 * @return void
437 * @since 1.3.4
438 */
439 private function attach_images_to_variant( $variant_id, array $images ) {
440 if ( empty( $images ) ) {
441 return;
442 }
443
444 // Use ProductVariation model to access media() relationship
445 $variant = \FluentCart\App\Models\ProductVariation::query()->find( $variant_id );
446 if ( empty( $variant ) ) {
447 return;
448 }
449
450 $meta_value = [];
451 foreach ( $images as $image_path ) {
452 $value = $this->add_image_from_path( $image_path, $variant['variation_title'] );
453 if ( ! empty( $value ) ) {
454 $meta_value[] = $value;
455 $this->stats['images_imported']++;
456 }
457 }
458
459 if ( ! empty( $meta_value ) ) {
460 // Use media() relationship like ImageAttachService does
461 // meta_value should be an array, not serialized
462 $media = [
463 'meta_value' => $meta_value,
464 'object_id' => $variant['id'],
465 'object_type' => 'product_variant_info',
466 'meta_key' => 'product_thumbnail',
467 ];
468 $variant->media()->create( $media );
469 }
470 }
471
472 /**
473 * Add image from local path
474 * Similar to ImageAttachService::addImageFromUrl() but for local files
475 *
476 * @param string $image_path Relative image path (e.g., 'images/photo.jpg')
477 * @param string $title Image title
478 * @return array Image data array with id, title, and url
479 * @since 1.3.4
480 */
481 private function add_image_from_path( $image_path, $title ) {
482 // Convert relative path to absolute
483 $full_path = dirname( $this->import_path ) . '/' . $image_path;
484
485 if ( ! file_exists( $full_path ) ) {
486 Helper::log( 'FluentImport: Image file not found: ' . $full_path, 'fsi_fluent', 'error' );
487 return [];
488 }
489
490 // Check if image already exists in media library
491 $filename = basename( $full_path );
492 global $wpdb;
493 $existing = $wpdb->get_var(
494 $wpdb->prepare(
495 "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'attachment' AND guid LIKE %s",
496 '%' . $wpdb->esc_like( $filename )
497 )
498 );
499
500 if ( $existing ) {
501 $media = wp_prepare_attachment_for_js( $existing );
502 return [
503 'id' => $existing,
504 'title' => $media['title'],
505 'url' => $media['url'],
506 ];
507 }
508
509 // Upload image to WordPress media library
510 $upload_dir = wp_upload_dir();
511 $dest_file = $upload_dir['path'] . '/' . $filename;
512
513 // Copy file to uploads directory
514 if ( ! copy( $full_path, $dest_file ) ) {
515 Helper::log( 'FluentImport: Failed to copy image: ' . $full_path, 'fsi_fluent', 'error' );
516 return [];
517 }
518
519 // Create attachment
520 $attachment_data = [
521 'post_mime_type' => mime_content_type( $dest_file ),
522 'post_title' => sanitize_file_name( pathinfo( $filename, PATHINFO_FILENAME ) ),
523 'post_content' => '',
524 'post_status' => 'inherit',
525 ];
526
527 $attachment_id = wp_insert_attachment( $attachment_data, $dest_file );
528 if ( is_wp_error( $attachment_id ) ) {
529 Helper::log( 'FluentImport: Failed to create attachment: ' . $attachment_id->get_error_message(), 'fsi_fluent', 'error' );
530 return [];
531 }
532
533 // Generate attachment metadata
534 $attachment_metadata = wp_generate_attachment_metadata( $attachment_id, $dest_file );
535 wp_update_attachment_metadata( $attachment_id, $attachment_metadata );
536
537 $media = wp_prepare_attachment_for_js( $attachment_id );
538 return [
539 'id' => $attachment_id,
540 'title' => $media['title'],
541 'url' => $media['url'],
542 ];
543 }
544 }
545
546