PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.0
Elementor Website Builder – more than just a page builder v4.3.0
4.3.1 4.3.0 4.3.0-beta3 4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 All 454 releases
elementor / modules / global-classes / global-classes-repository.php

global-classes-repository.php in Elementor Website Builder – more than just a page builder 4.3.0, at modules/global-classes/global-classes-repository.php

518 lines 15.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Elementor\Modules\GlobalClasses;
3
4 use Elementor\Core\Kits\Documents\Kit;
5 use Elementor\Modules\DesignSystemSync\Classes\Global_Classes_Sync_Map;
6 use Elementor\Core\Kits\Concerns\Has_Kit_Dependency;
7 use Elementor\Modules\GlobalClasses\Concerns\Has_Preview_Context;
8 use Elementor\Modules\GlobalClasses\Utils\Global_Class_Data_Normalizer;
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit; // Exit if accessed directly.
12 }
13
14 class Global_Classes_Repository {
15 use Has_Kit_Dependency;
16 use Has_Preview_Context;
17
18 const META_KEY_FRONTEND = '_elementor_global_classes';
19 const META_KEY_PREVIEW = '_elementor_global_classes_preview';
20
21 const CONTEXT_FRONTEND = 'frontend';
22 const CONTEXT_PREVIEW = 'preview';
23
24 const READ_BATCH_SIZE = 100;
25 const PERSIST_BATCH_SIZE = 100;
26
27 protected array $context_keys = [
28 'event' => [
29 'frontend' => self::CONTEXT_FRONTEND,
30 'preview' => self::CONTEXT_PREVIEW,
31 ],
32 ];
33
34 private ?Global_Classes $cache = null;
35
36 public function __construct( ?Kit $kit = null ) {
37 if ( null !== $kit ) {
38 $this->set_kit( $kit );
39 }
40 }
41
42 public static function make( ?Kit $kit = null ): Global_Classes_Repository {
43 return new self( $kit );
44 }
45
46 protected function on_preview_change(): void {
47 $this->cache = null;
48 }
49
50 /**
51 * This method may be too heavy to use
52 * Be mindful as this call would cause the server to freeze for as much time as needed until it fetches
53 * all global classes
54 */
55 public function all( bool $force = false ): Global_Classes {
56 if ( ! $force && null !== $this->cache ) {
57 return $this->cache;
58 }
59
60 $this->cache = $this->all_from_posts();
61
62 return $this->cache;
63 }
64
65 public function all_labels(): array {
66 return $this->labels()->get_ordered_labels();
67 }
68
69 public function get_order(): array {
70 return Global_Classes_Order::make( $this->get_kit() )->set_preview( $this->is_preview() )->get_order();
71 }
72
73 public function update_order_and_labels( array $order, array $new_labels ): void {
74 Global_Classes_Order::make( $this->get_kit() )
75 ->set_preview( $this->is_preview() )
76 ->set_order( $order );
77
78 $labels = $this->labels();
79 $existing_labels = $labels->get_labels();
80
81 foreach ( $new_labels as $id => $label ) {
82 $existing_labels[ $id ] = $label;
83 }
84
85 $labels->set_labels( $existing_labels );
86
87 $this->cache = null;
88 }
89
90 private function labels(): Global_Classes_Labels {
91 return Global_Classes_Labels::make( $this->get_kit() )->set_preview( $this->is_preview() );
92 }
93
94 public function get( string $class_id ): ?array {
95 $post = Global_Class_Post::find_by_class_id( $class_id, $this->is_preview(), $this->get_kit() );
96
97 return $post ? $post->to_array() : null;
98 }
99
100 public function get_by_ids( array $class_ids ): array {
101 if ( empty( $class_ids ) ) {
102 return [];
103 }
104
105 $post_ids = Global_Classes_Post_IDs::make( $this->get_kit() )->get_post_ids( $class_ids );
106 $items = [];
107
108 foreach ( $post_ids as $class_id => $post_id ) {
109 $post = Global_Class_Post::from_post_id( $post_id, $this->is_preview() );
110
111 if ( $post ) {
112 $items[ $class_id ] = $post->to_array();
113 }
114 }
115
116 return $items;
117 }
118
119 public function apply_changes( array $touched_items, array $changes, array $order ): void {
120 $labels = $this->labels();
121 $before = $labels->get_labels();
122 $is_preview = $this->is_preview();
123 $to_delete = $changes['deleted'] ?? [];
124 $to_create = $changes['added'] ?? [];
125 $to_update = $changes['modified'] ?? [];
126 $order_changed = isset( $changes['order'] ) && $changes['order'];
127
128 $final_label_map = [];
129 foreach ( $order as $id ) {
130 if ( isset( $touched_items[ $id ] ) ) {
131 $final_label_map[ $id ] = $touched_items[ $id ]['label'];
132 } elseif ( isset( $before[ $id ] ) ) {
133 $final_label_map[ $id ] = $before[ $id ];
134 }
135 }
136
137 $affected_post_ids = $this->get_posts_affected_by_deletion( $to_delete );
138
139 $this->persist_class_batch_mutations( $to_delete, $to_create, $to_update, $touched_items, $is_preview );
140
141 $classes_order = Global_Classes_Order::make( $this->get_kit() )->set_preview( $this->is_preview() );
142 $classes_order->set_order( $order );
143 $labels->set_labels( $final_label_map );
144
145 if ( ! $is_preview ) {
146 Global_Classes_Sync_Map::make( $this->get_kit() )->apply_changes( $touched_items, $to_delete );
147 $this->propagate_order_to_preview( $order, $to_delete );
148
149 $this->bulk_clear_preview_meta( array_values( $to_update ) );
150 $this->clear_preview_labels_for_ids( array_merge(
151 array_values( $to_create ),
152 array_values( $to_update ),
153 array_values( $to_delete )
154 ) );
155 }
156
157 $this->cache = null;
158 $this->flush_runtime_cache();
159
160 do_action( 'elementor/global_classes/update', $this->get_context_key( 'event' ), [
161 'added' => $to_create,
162 'deleted' => $to_delete,
163 'modified' => $to_update,
164 'order' => $order_changed,
165 'affected_post_ids' => $affected_post_ids,
166 ] );
167
168 if ( ! empty( $to_delete ) && ! $is_preview ) {
169 do_action(
170 'elementor/global_classes/cleanup',
171 $to_delete,
172 $affected_post_ids
173 );
174 }
175 }
176
177 private function propagate_order_to_preview( array $published_order, array $deleted_ids ): void {
178 $preview_order = Global_Classes_Order::make( $this->get_kit() )->set_preview( true );
179 $existing_order = $preview_order->get_order();
180 $unpublished_ids = array_flip( array_diff( $existing_order, $published_order, $deleted_ids ) );
181 $merged_order = $published_order;
182
183 foreach ( $existing_order as $position => $id ) {
184 if ( isset( $unpublished_ids[ $id ] ) ) {
185 array_splice( $merged_order, min( $position, count( $merged_order ) ), 0, [ $id ] );
186 }
187 }
188
189 $preview_order->set_order( $merged_order );
190 }
191
192 public function each_item( callable $cb, bool $skip_migration = false, int $batch_size = self::READ_BATCH_SIZE ): void {
193 $order = Global_Classes_Order::make( $this->get_kit() )->get_order();
194
195 if ( empty( $order ) ) {
196 return;
197 }
198
199 foreach ( array_chunk( $order, $batch_size ) as $chunk ) {
200 foreach ( $this->iterate_class_posts_for_ids( $chunk ) as $class_post ) {
201 $cb( $class_post->to_array( $skip_migration ) );
202 }
203 }
204 }
205
206 public function put( array $items, array $order ) {
207 $current_ids = Global_Classes_Order::make( $this->get_kit() )
208 ->set_preview( $this->is_preview() )
209 ->get_order();
210
211 $new_ids = array_keys( $items );
212 $current_order_string = implode( ';', $current_ids );
213 $deleted_class_ids = array_values( array_diff( $current_ids, $new_ids ) );
214
215 $changes = [
216 'added' => array_values( array_diff( $new_ids, $current_ids ) ),
217 'deleted' => $deleted_class_ids,
218 'modified' => array_values( array_intersect( $new_ids, $current_ids ) ),
219 'order' => implode( ';', $order ) !== $current_order_string,
220 ];
221
222 /**
223 * We collect all affected ids before the put_to_posts execution
224 * as the update mechanism would handle the Global_Classes_Relations as it iterates over the update batches
225 * So once we get to the cleanup phase - we would no longer have the relevant relations
226 *
227 * On top of that - by collecting all affected posts in advance, means we would iterate over each document's elements only once
228 * (as the alternative would be to trigger the cleanup per removed class, but that means we may end up iterating over the same document N times, if all N styles are used in it)
229 */
230 $affected_post_ids = $this->get_posts_affected_by_deletion( $deleted_class_ids );
231
232 $this->put_to_posts( $items, $order, $current_ids );
233
234 $this->cache = null;
235
236 $changes['affected_post_ids'] = $affected_post_ids;
237
238 do_action( 'elementor/global_classes/update', $this->get_context_key( 'event' ), $changes );
239
240 if ( ! empty( $deleted_class_ids ) && ! $this->is_preview() ) {
241 do_action(
242 'elementor/global_classes/cleanup',
243 $deleted_class_ids,
244 $affected_post_ids
245 );
246 }
247 }
248
249 private function get_posts_affected_by_deletion( array $deleted_class_ids ): array {
250 if ( empty( $deleted_class_ids ) ) {
251 return [];
252 }
253
254 $relations = new Global_Classes_Relations();
255 $post_ids = [];
256
257 foreach ( $deleted_class_ids as $class_id ) {
258 $post_ids[] = $relations->get_posts_by_style( $class_id );
259 }
260
261 return array_values( array_unique( array_merge( ...$post_ids ) ) );
262 }
263
264 private function all_from_posts(): Global_Classes {
265 $order = Global_Classes_Order::make( $this->get_kit() )
266 ->set_preview( $this->is_preview() )
267 ->get_order();
268
269 if ( empty( $order ) ) {
270 return Global_Classes::make( [], [] );
271 }
272
273 $items = [];
274
275 foreach ( $this->iterate_class_posts_for_ids( $order ) as $class_post ) {
276 $class_data = $class_post->to_array();
277 $items[ $class_data['id'] ] = $class_data;
278 }
279
280 $order = Global_Classes_Parser::sanitize_order( $items, $order );
281
282 return Global_Classes::make( $items, $order );
283 }
284
285 private function put_to_posts( array $items, array $order, array $current_ids ): void {
286 $is_preview = $this->is_preview();
287 $new_ids = array_keys( $items );
288
289 $to_delete = array_diff( $current_ids, $new_ids );
290 $to_create = array_diff( $new_ids, $current_ids );
291 $to_update = array_intersect( $new_ids, $current_ids );
292
293 $this->persist_class_batch_mutations( $to_delete, $to_create, $to_update, $items, $is_preview );
294
295 $classes_order = Global_Classes_Order::make( $this->get_kit() )->set_preview( $this->is_preview() );
296 $classes_order->set_order( $order );
297
298 $label_map = [];
299 foreach ( $order as $id ) {
300 if ( isset( $items[ $id ]['label'] ) ) {
301 $label_map[ $id ] = $items[ $id ]['label'];
302 }
303 }
304 $this->labels()->set_labels( $label_map );
305
306 if ( ! $is_preview ) {
307 $touched_ids = array_merge( array_values( $to_create ), array_values( $to_update ) );
308 $touched_items = array_intersect_key(
309 $items,
310 array_flip( $touched_ids )
311 );
312 Global_Classes_Sync_Map::make( $this->get_kit() )->apply_changes( $touched_items, array_values( $to_delete ) );
313
314 $this->bulk_clear_preview_meta( array_values( $to_update ) );
315 $this->clear_preview_labels_for_ids( array_merge(
316 array_values( $to_create ),
317 array_values( $to_update ),
318 array_values( $to_delete )
319 ) );
320 }
321 }
322
323 private function iterate_class_posts_for_ids( array $class_ids ): \Generator {
324 foreach ( array_chunk( $class_ids, self::READ_BATCH_SIZE ) as $chunk ) {
325 $posts = get_posts( [
326 'post_type' => Global_Class_Post_Type::CPT,
327 'post_status' => 'publish',
328 'posts_per_page' => -1,
329 'meta_query' => [
330 [
331 'key' => Global_Class_Post::META_KEY_ID,
332 'value' => $chunk,
333 'compare' => 'IN',
334 ],
335 ],
336 ] );
337
338 foreach ( $posts as $post ) {
339 yield Global_Class_Post::from_post( $post, $this->is_preview() );
340 clean_post_cache( $post->ID );
341 }
342 unset( $posts );
343 $this->flush_runtime_cache();
344 }
345 }
346
347 private function persist_class_batch_mutations(
348 array $to_delete,
349 array $to_create,
350 array $to_update,
351 array $items_by_id,
352 bool $is_preview
353 ): void {
354 $relations = new Global_Classes_Relations();
355 $post_ids_map = Global_Classes_Post_IDs::make( $this->get_kit() );
356
357 $ids_to_resolve = array_values( array_merge(
358 array_values( $to_delete ),
359 array_values( $to_update ),
360 array_values( $to_create )
361 ) );
362 $post_ids = $post_ids_map->get_post_ids( $ids_to_resolve );
363
364 $this->each_class_id_batch(
365 array_values( $to_delete ),
366 function ( string $class_id ) use ( $is_preview, $relations, $post_ids ) {
367 $post = isset( $post_ids[ $class_id ] )
368 ? Global_Class_Post::from_post_id( $post_ids[ $class_id ], false )
369 : null;
370
371 if ( ! $post ) {
372 return;
373 }
374
375 if ( $is_preview ) {
376 $post->set_preview( true );
377 $post->update_data( [] );
378 clean_post_cache( $post->get_post_id() );
379 } else {
380 $relations->clear_class_relations( $class_id );
381 $post->delete();
382 }
383 }
384 );
385
386 $this->each_class_id_batch( $to_create, function ( string $class_id ) use ( $items_by_id, $is_preview, $post_ids, $post_ids_map ) {
387 if ( ! isset( $items_by_id[ $class_id ] ) ) {
388 return;
389 }
390
391 $item = $items_by_id[ $class_id ];
392 $data = Global_Class_Data_Normalizer::normalize_style_fields( $item );
393 $kit = $this->get_kit();
394 $existing_post_id = $post_ids[ $class_id ] ?? null;
395 $existing_post = $existing_post_id ? Global_Class_Post::from_post_id( $existing_post_id, $is_preview ) : null;
396
397 if ( $existing_post ) {
398 $existing_post->update_data( $data );
399
400 if ( ! $is_preview ) {
401 $existing_post->update_label( $item['label'] );
402 }
403
404 clean_post_cache( $existing_post->get_post_id() );
405
406 return;
407 }
408
409 $created = Global_Class_Post::create( $class_id, $item['label'], $data, $kit );
410
411 if ( $created ) {
412 $post_ids_map->set( $class_id, $created->get_post_id() );
413 clean_post_cache( $created->get_post_id() );
414 }
415 } );
416
417 $this->each_class_id_batch(
418 $to_update,
419 function ( string $class_id ) use ( $items_by_id, $is_preview, $post_ids ) {
420 if ( ! isset( $items_by_id[ $class_id ] ) || ! isset( $post_ids[ $class_id ] ) ) {
421 return;
422 }
423
424 $item = $items_by_id[ $class_id ];
425 $post = Global_Class_Post::from_post_id( $post_ids[ $class_id ], $is_preview );
426
427 if ( ! $post ) {
428 return;
429 }
430
431 $data = Global_Class_Data_Normalizer::normalize_style_fields( $item );
432 $post->update_data( $data );
433
434 if ( ! $is_preview ) {
435 $post->update_label( $item['label'] );
436 }
437
438 clean_post_cache( $post->get_post_id() );
439 }
440 );
441 }
442
443 public function delete_all(): void {
444 $order = $this->get_order();
445
446 $this->each_class_id_batch( $order, function ( string $class_id ) {
447 $post = Global_Class_Post::find_by_class_id( $class_id, false, $this->get_kit() );
448
449 if ( $post ) {
450 $post->delete();
451 }
452 } );
453
454 Global_Classes_Order::make( $this->get_kit() )->set_order( [] );
455 $this->labels()->set_labels( [] );
456 }
457
458 private function clear_preview_labels_for_ids( array $class_ids ): void {
459 if ( empty( $class_ids ) ) {
460 return;
461 }
462
463 $preview_labels = Global_Classes_Labels::make( $this->get_kit() )->set_preview( true );
464 $labels_map = $preview_labels->get_labels();
465
466 if ( empty( $labels_map ) ) {
467 return;
468 }
469
470 $ids_to_remove = array_intersect( array_unique( $class_ids ), array_keys( $labels_map ) );
471
472 if ( empty( $ids_to_remove ) ) {
473 return;
474 }
475
476 foreach ( $ids_to_remove as $id ) {
477 unset( $labels_map[ $id ] );
478 }
479
480 $preview_labels->set_labels( $labels_map );
481 }
482
483 private function bulk_clear_preview_meta( array $class_ids ): void {
484 if ( empty( $class_ids ) ) {
485 return;
486 }
487
488 $post_ids_map = Global_Classes_Post_IDs::make( $this->get_kit() );
489
490 foreach ( array_chunk( $class_ids, self::PERSIST_BATCH_SIZE ) as $chunk ) {
491 $post_ids = $post_ids_map->get_post_ids( $chunk );
492
493 foreach ( $post_ids as $post_id ) {
494 delete_post_meta( $post_id, Global_Class_Post::META_KEY_DATA_PREVIEW );
495 clean_post_cache( $post_id );
496 }
497
498 $this->flush_runtime_cache();
499 }
500 }
501
502 private function each_class_id_batch( $class_ids, callable $callback, int $batch_size = self::PERSIST_BATCH_SIZE ): void {
503 $class_ids = is_array( $class_ids ) ? $class_ids : iterator_to_array( $class_ids, false );
504 foreach ( array_chunk( array_values( $class_ids ), $batch_size ) as $batch ) {
505 foreach ( $batch as $class_id ) {
506 $callback( $class_id );
507 }
508 $this->flush_runtime_cache();
509 }
510 }
511
512 private function flush_runtime_cache(): void {
513 if ( function_exists( 'wp_cache_flush_runtime' ) ) {
514 wp_cache_flush_runtime();
515 }
516 }
517 }
518