PluginProbe
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments / 4.4.1
SureCart – Ecommerce Made Easy For Selling Physical Products, Digital Downloads, Subscriptions, Donations, & Payments v4.4.1
4.7.2 4.7.1 4.7.0 4.6.6 4.6.5 4.6.4 4.6.3 4.6.2 4.6.1 4.6.0 4.5.1 4.5.0 4.4.2 4.4.1 4.4.0 4.3.3 4.3.2 4.3.1 4.3.0 4.2.3 4.2.2 4.2.1 1.0.3 1.0.4 1.0.5 All 281 releases
surecart / app / src / Sync / PostSyncService.php
PostSyncService.php
456 lines 12.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SureCart\Sync;
4
5 use SureCart\Models\Concerns\Facade;
6
7 use SureCart\Models\VariantOptionValue;
8 use SureCart\Support\Currency;
9 use SureCart\Sync\Concerns\HasLockProcess;
10
11 /**
12 * This class syncs product records to WordPress posts.
13 */
14 class PostSyncService {
15 use Facade;
16 use HasLockProcess;
17
18 /**
19 * The product model.
20 *
21 * @var \SureCart\Models\Product
22 */
23 protected $product;
24
25 /**
26 * The post.
27 *
28 * @var \WP_Post
29 */
30 protected $post;
31
32 /**
33 * The post type.
34 *
35 * @var string
36 */
37 protected $post_type = 'sc_product';
38
39 /**
40 * The excluded meta keys.
41 *
42 * @var array
43 */
44 protected $excluded_meta_keys = [
45 'gallery',
46 'gallery_ids',
47 'meta_description',
48 'page_title',
49 'wp_buy_link_coupon_field_disabled',
50 'wp_buy_link_custom_thankyou_page',
51 'wp_buy_link_custom_thankyou_page_url',
52 'wp_buy_link_enabled',
53 'wp_buy_link_logo_disabled',
54 'wp_buy_link_product_description_disabled',
55 'wp_buy_link_product_image_disabled',
56 'wp_buy_link_success_page_enabled',
57 'wp_buy_link_success_page_url',
58 'wp_buy_link_terms_disabled',
59 'wp_buy_link_test_mode_enabled',
60 'wp_created_by',
61 'wp_template_part_id',
62 'wp_template_id',
63 ];
64
65 /**
66 * Find the post from the model.
67 *
68 * @param string $model_id The model id.
69 *
70 * @return \WP_Post|\WP_Error|null
71 */
72 protected function findByModelId( $model_id ) {
73 // if we don't have a model id, return null.
74 if ( empty( $model_id ) ) {
75 return null;
76 }
77
78 // query the post.
79 $query = new \WP_Query(
80 array(
81 'post_type' => $this->post_type,
82 'post_status' => array( 'auto-draft', 'draft', 'publish', 'trash', 'sc_archived', 'future' ),
83 'posts_per_page' => 1,
84 'no_found_rows' => true,
85 'suppress_filters' => true,
86 'meta_query' => array(
87 array(
88 'key' => 'sc_id',
89 'value' => $model_id, // query by model id.
90 ),
91 ),
92 )
93 );
94
95 // handle error.
96 if ( is_wp_error( $query ) ) {
97 return $query;
98 }
99
100 // get the post.
101 $post = $query->posts[0] ?? null;
102
103 // return the post.
104 return apply_filters( "surecart_get_{$this->post_type}_post", $post, $model_id, $this );
105 }
106
107 /**
108 * Sync the model with the post.
109 *
110 * @param \SureCart\Models\Model $model The model.
111 *
112 * @return \WP_Post|\WP_Error
113 */
114 protected function sync( \SureCart\Models\Model $model ) {
115 $this->post = $this->findByModelId( $model->id );
116
117 if ( is_wp_error( $this->post ) ) {
118 return $this->post;
119 }
120
121 return empty( $this->post->ID ) ? $this->create( $model ) : $this->update( $model );
122 }
123
124 /**
125 * Delete the synced post.
126 *
127 * @param string $id The model id.
128 *
129 * @return \WP_Post|\WP_Error|false|null
130 */
131 protected function delete( string $id ) {
132 $this->post = $this->findByModelId( $id );
133
134 if ( is_wp_error( $this->post ) ) {
135 return $this->post;
136 }
137
138 // force delete post.
139 $deleted = wp_delete_post( $this->post->ID, true );
140 if ( is_wp_error( $deleted ) ) {
141 return $deleted;
142 }
143
144 // delete variant option values where post_id is the post id.
145 $deleted_option = VariantOptionValue::where( 'product_id', $id )->delete();
146 if ( is_wp_error( $deleted_option ) ) {
147 return $deleted_option;
148 }
149
150 return $deleted;
151 }
152
153 /**
154 * Get the schema map.
155 *
156 * @param \SureCart\Models\Model $model The model.
157 *
158 * @return array
159 */
160 protected function getSchemaMap( \SureCart\Models\Model $model ) {
161 $base_amount = ! empty( $model->prices->data[0]->amount ) ? $model->prices->data[0]->amount : 0;
162
163 // Filter metadata using array_filter with a callback.
164 $metadata = array_filter(
165 (array) $model->metadata,
166 function ( $key ) {
167 $excluded = apply_filters( 'surecart_excluded_post_meta_keys', $this->excluded_meta_keys );
168 return ! in_array( $key, $excluded, true );
169 },
170 ARRAY_FILTER_USE_KEY // pass just the key to the callback.
171 );
172
173 return array(
174 'post_title' => $model->name,
175 'post_type' => $this->post_type,
176 'post_name' => $model->slug,
177 'post_excerpt' => $model->description ?? '',
178 'post_date' => ( new \DateTime() )->setTimestamp( $model->cataloged_at ?? $model->created_at ?? time() )->setTimezone( new \DateTimeZone( wp_timezone_string() ) )->format( 'Y-m-d H:i:s' ),
179 'post_date_gmt' => date_i18n( 'Y-m-d H:i:s', $model->cataloged_at ?? $model->created_at ?? time(), true ),
180 'post_modified' => ( new \DateTime() )->setTimestamp( $model->updated_at ?? $model->created_at ?? time() )->setTimezone( new \DateTimeZone( wp_timezone_string() ) )->format( 'Y-m-d H:i:s' ),
181 'post_modified_gmt' => date_i18n( 'Y-m-d H:i:s', $model->updated_at ?? $model->created_at ?? time(), true ),
182 'post_status' => $this->getPostStatusFromModel( $model ),
183 'meta_input' => array_merge(
184 $metadata,
185 array(
186 'sc_id' => $model->id,
187 'product' => $model->toArray(),
188 'min_price_amount' => ! empty( $model->metrics->min_price_amount ) ? Currency::maybeConvertAmount( $model->metrics->min_price_amount ?? 0, $model->initial_price->currency ?? null ) : $base_amount,
189 'max_price_amount' => ! empty( $model->metrics->max_price_amount ) ? Currency::maybeConvertAmount( $model->metrics->max_price_amount ?? 0, $model->initial_price->currency ?? null ) : $base_amount,
190 'available_stock' => $model->available_stock,
191 'stock_enabled' => $model->stock_enabled,
192 'allow_out_of_stock_purchases' => $model->allow_out_of_stock_purchases,
193 'featured' => $model->featured,
194 'recurring' => $model->recurring,
195 'shipping_enabled' => $model->shipping_enabled,
196 'purchase_limit' => $model->purchase_limit,
197 'sku' => $model->sku,
198 'weight' => $model->weight,
199 'weight_unit' => $model->weight_unit,
200 'display_amount' => $model->display_amount,
201 'scratch_display_amount' => $model->scratch_display_amount,
202 'range_display_amount' => $model->range_display_amount,
203 'is_on_sale' => $model->is_on_sale,
204 'total_reviews' => $model->total_reviews,
205 'average_stars' => $model->average_stars,
206 'reviews_enabled' => $model->reviews_enabled ?? false,
207 )
208 ),
209 );
210 }
211
212 /**
213 * Convert the template id.
214 *
215 * This removes the template theme prefix since this is not needed.
216 *
217 * @param string $id The template id.
218 *
219 * @return string
220 */
221 protected function convertTemplateId( $id ) {
222 $parts = explode( '//', $id, 2 );
223
224 // not a FSE template.
225 if ( count( $parts ) < 2 ) {
226 return $id;
227 }
228
229 return $parts[1] ?? null;
230 }
231
232 /**
233 * Create the post.
234 *
235 * @param \SureCart\Models\Model $model The model.
236 *
237 * @return \WP_Post|\WP_Error
238 */
239 protected function create( \SureCart\Models\Model $model ) {
240 // Another process is already syncing this product.
241 if ( $this->hasLock( $model ) ) {
242 return $this->findByModelId( $model->id );
243 }
244
245 try {
246 // Acquire lock.
247 $this->acquireLock( $model );
248
249 // don't do these actions as they can slow down the sync.
250 foreach ( array( 'do_pings', 'transition_post_status', 'save_post', 'pre_post_update', 'add_attachment', 'edit_attachment', 'edit_post', 'post_updated', 'wp_insert_post', 'save_post_' . $this->post_type ) as $action ) {
251 remove_all_actions( $action );
252 }
253
254 // we are importing.
255 if ( ! defined( 'WP_IMPORTING' ) ) {
256 define( 'WP_IMPORTING', true );
257 }
258
259 // insert post.
260 $props = $this->getSchemaMap( $model );
261 $post_id = wp_insert_post( wp_slash( apply_filters( 'surecart/product/sync/created/props', $props, $model ) ), true, false );
262
263 // handle errors.
264 if ( is_wp_error( $post_id ) ) {
265 return $post_id;
266 }
267
268 // If there is a page template, use that, otherwise use the default.
269 update_post_meta( $post_id, '_wp_page_template', $this->convertTemplateId( $model->template_id ?? '' ) );
270 update_post_meta( $post_id, '_wp_page_template_part', $this->convertTemplateId( $model->template_part_id ?? '' ) );
271
272 $this->syncCollections( $post_id, $model );
273
274 // We need to use wp_set_post_terms() because tax_input performs its own permission checks.
275 wp_set_post_terms( $post_id, \SureCart::account()->id, 'sc_account' );
276
277 $values = $this->syncVariantValues( $model, $post_id );
278 if ( is_wp_error( $values ) ) {
279 return $values;
280 }
281
282 // set the post on the model.
283 $this->post = get_post( $post_id );
284
285 // fire action.
286 do_action( 'surecart/product/sync/created', $this->post, $model );
287
288 return $this->post;
289 } finally {
290 // Always release the lock, if any error occurs.
291 $this->releaseLock( $model );
292 }
293 }
294
295 /**
296 * Update the post.
297 *
298 * @param \SureCart\Models\Model $model The model.
299 *
300 * @return \WP_Post|\WP_Error
301 */
302 protected function update( $model ) {
303 // find the post.
304 if ( empty( $this->post ) ) {
305 $this->post = $this->findByModelId( $model->id );
306 }
307
308 $props = $this->getSchemaMap( $model );
309
310 // if we already have a post template, don't sync. This is to prevent overwriting the template.
311 if ( ! empty( $this->post->page_template ) ) {
312 unset( $props['page_template'] );
313 }
314
315 // update the post by id.
316 $post_id = wp_update_post(
317 apply_filters(
318 'surecart/product/sync/updated/props',
319 array_merge(
320 $props,
321 array(
322 'ID' => $this->post->ID,
323 )
324 ),
325 $model
326 ),
327 );
328
329 if ( is_wp_error( $post_id ) ) {
330 return $post_id;
331 }
332
333 // update page template.
334 update_post_meta( $post_id, '_wp_page_template', $this->convertTemplateId( $model->template_id ?? '' ) );
335 update_post_meta( $post_id, '_wp_page_template_part', $this->convertTemplateId( $model->template_part_id ?? '' ) );
336
337 // set the collection terms.
338 $this->syncCollections( $post_id, $model );
339
340 // We need to use wp_set_post_terms() because tax_input performs its own permission checks.
341 wp_set_post_terms( $post_id, \SureCart::account()->id, 'sc_account' );
342
343 $values = $this->syncVariantValues( $model, $post_id );
344 if ( is_wp_error( $values ) ) {
345 return $values;
346 }
347
348 $this->post = get_post( $post_id );
349
350 // fire action.
351 do_action( 'surecart/product/sync/updated', $this->post, $model );
352
353 return $this->post;
354 }
355
356 /**
357 * Sync the variant values.
358 *
359 * @param int $model_id The model id.
360 * @param int $post_id The post id.
361 *
362 * @return bool|\WP_Error
363 */
364 protected function syncVariantValues( $model, $post_id ) {
365 // delete existing.
366 VariantOptionValue::where( 'product_id', $model->id )->delete();
367
368 // create new.
369 foreach ( ( $model->variant_options->data ?? array() ) as $option ) {
370 foreach ( $option->values as $value ) {
371 $created = VariantOptionValue::create(
372 array(
373 'value' => $value,
374 'name' => $option->name,
375 'post_id' => $post_id,
376 'product_id' => $model->id,
377 )
378 );
379 if ( is_wp_error( $created ) ) {
380 return $created;
381 }
382 }
383 }
384
385 return true;
386 }
387
388 /**
389 * Sync the collections.
390 *
391 * @param int $post_id The post id.
392 * @param \SureCart\Models\Model $model The model.
393 *
394 * @return void
395 */
396 protected function syncCollections( $post_id, $model ) {
397 // sanity check.
398 if ( ! isset( $model->product_collections ) || ! isset( $model->product_collections->data ) || ! is_array( $model->product_collections->data ) ) {
399 return;
400 }
401
402 // store the terms for the post.
403 $terms = array();
404
405 // Loop through the collections.
406 foreach ( $model->product_collections->data as $collection ) {
407 // sync the collection.
408 $collection->sync();
409
410 // get term by sc_id.
411 $term = $collection->term;
412
413 // error handling.
414 if ( is_wp_error( $term ) ) {
415 error_log( $term->get_error_message() );
416 continue;
417 }
418
419 // if we don't have a term id, skip.
420 if ( ! isset( $term->term_id ) || empty( $term->term_id ) ) {
421 error_log( 'Could not sync term for collection: ' . $collection->name );
422 error_log( print_r( $term, true ) );
423 continue;
424 }
425
426 // add to terms array.
427 $terms[] = (int) $term->term_id;
428 }
429
430 // set the terms.
431 wp_set_post_terms( $post_id, $terms, 'sc_collection' );
432 }
433
434 /**
435 * Add archived to the post meta.
436 *
437 * @param \SureCart\Models\Model $model The model.
438 *
439 * @return string
440 */
441 protected function getPostStatusFromModel( \SureCart\Models\Model $model ) {
442 // if it's archived, use that.
443 if ( $model->archived ) {
444 return 'sc_archived';
445 }
446
447 // if it's draft, use that.
448 if ( 'draft' === ( $model->status ?? '' ) ) {
449 return 'draft';
450 }
451
452 // default to publish.
453 return 'publish';
454 }
455 }
456