PluginProbe
Elementor Website Builder – more than just a page builder / 4.3.2
Elementor Website Builder – more than just a page builder v4.3.2
4.3.2 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 All 455 releases
← All changes | modules/global-classes/global-classes-repository.php +452 -49 4.0.9 → 4.3.2 View file →
@@ -1,11 +1,12 @@
1 1 <?php
2 2 namespace Elementor\Modules\GlobalClasses;
3 3
4 4 use Elementor\Core\Kits\Documents\Kit;
5 -use Elementor\Modules\AtomicWidgets\PropTypeMigrations\Migrations_Orchestrator;
6 -use Elementor\Modules\GlobalClasses\Global_Classes_Parser;
7 -use Elementor\Plugin;
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;
8 9
9 10 if ( ! defined( 'ABSPATH' ) ) {
10 11 exit; // Exit if accessed directly.
11 12 }
@@ -10,8 +11,10 @@
10 11 exit; // Exit if accessed directly.
11 12 }
12 13
13 14 class Global_Classes_Repository {
15 + use Has_Kit_Dependency;
16 + use Has_Preview_Context;
14 17
15 18 const META_KEY_FRONTEND = '_elementor_global_classes';
16 19 const META_KEY_PREVIEW = '_elementor_global_classes_preview';
17 20
@@ -17,16 +20,24 @@
17 20
18 21 const CONTEXT_FRONTEND = 'frontend';
19 22 const CONTEXT_PREVIEW = 'preview';
20 23
21 - private string $context = self::CONTEXT_FRONTEND;
24 + const READ_BATCH_SIZE = 100;
25 + const PERSIST_BATCH_SIZE = 100;
22 26
27 + protected array $context_keys = [
28 + 'event' => [
29 + 'frontend' => self::CONTEXT_FRONTEND,
30 + 'preview' => self::CONTEXT_PREVIEW,
31 + ],
32 + ];
33 +
23 34 private ?Global_Classes $cache = null;
24 35
25 - private ?Kit $kit = null;
26 -
27 36 public function __construct( ?Kit $kit = null ) {
28 - $this->kit = $kit;
37 + if ( null !== $kit ) {
38 + $this->set_kit( $kit );
39 + }
29 40 }
30 41
31 42 public static function make( ?Kit $kit = null ): Global_Classes_Repository {
32 43 return new self( $kit );
@@ -31,84 +42,476 @@
31 42 public static function make( ?Kit $kit = null ): Global_Classes_Repository {
32 43 return new self( $kit );
33 44 }
34 45
35 - public function context( string $context ): self {
36 - $this->context = $context;
46 + protected function on_preview_change(): void {
37 47 $this->cache = null;
38 -
39 - return $this;
40 48 }
41 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 + */
42 55 public function all( bool $force = false ): Global_Classes {
43 56 if ( ! $force && null !== $this->cache ) {
44 57 return $this->cache;
45 58 }
46 59
47 - $meta_key = $this->get_meta_key();
48 - $kit = $this->get_kit();
49 - $all = $kit->get_json_meta( $meta_key );
60 + $this->cache = $this->all_from_posts();
50 61
51 - $is_preview = static::META_KEY_PREVIEW === $meta_key;
52 - $is_empty = empty( $all );
62 + return $this->cache;
63 + }
53 64
54 - if ( $is_preview && $is_empty ) {
55 - $all = $kit->get_json_meta( static::META_KEY_FRONTEND );
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;
56 83 }
57 84
58 - $all['order'] = Global_Classes_Parser::sanitize_order( $all['items'] ?? [], $all['order'] ?? [] );
85 + $labels->set_labels( $existing_labels );
59 86
60 - Migrations_Orchestrator::make()->migrate(
61 - $all,
62 - $kit->get_id(),
63 - $meta_key,
64 - function( $migrated_data ) use ( $kit, $meta_key ) {
65 - $kit->update_json_meta( $meta_key, $migrated_data );
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();
66 113 }
67 - );
114 + }
68 115
69 - $this->cache = Global_Classes::make( $all['items'] ?? [], $all['order'] ?? [] );
116 + return $items;
117 + }
70 118
71 - return $this->cache;
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 + }
72 175 }
73 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 +
74 206 public function put( array $items, array $order ) {
75 - $current_value = $this->all()->get();
207 + $current_ids = Global_Classes_Order::make( $this->get_kit() )
208 + ->set_preview( $this->is_preview() )
209 + ->get_order();
76 210
77 - $updated_value = [
78 - 'items' => $items,
79 - 'order' => $order,
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,
80 220 ];
81 221
82 - // `update_metadata` fails for identical data, so we skip it.
83 - if ( $current_value === $updated_value ) {
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 ) ) {
84 460 return;
85 461 }
86 462
87 - $meta_key = $this->get_meta_key();
88 - $value = $this->get_kit()->update_json_meta( $meta_key, $updated_value );
463 + $preview_labels = Global_Classes_Labels::make( $this->get_kit() )->set_preview( true );
464 + $labels_map = $preview_labels->get_labels();
89 465
90 - $should_delete_preview = static::META_KEY_FRONTEND === $meta_key;
466 + if ( empty( $labels_map ) ) {
467 + return;
468 + }
91 469
92 - if ( $should_delete_preview ) {
93 - $this->get_kit()->delete_meta( static::META_KEY_PREVIEW );
470 + $ids_to_remove = array_intersect( array_unique( $class_ids ), array_keys( $labels_map ) );
471 +
472 + if ( empty( $ids_to_remove ) ) {
473 + return;
94 474 }
95 475
96 - if ( ! $value ) {
97 - throw new \Exception( 'Failed to update global classes' );
476 + foreach ( $ids_to_remove as $id ) {
477 + unset( $labels_map[ $id ] );
98 478 }
99 479
100 - $this->cache = null;
480 + $preview_labels->set_labels( $labels_map );
481 + }
101 482
102 - do_action( 'elementor/global_classes/update', $this->context, $updated_value, $current_value );
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 + }
103 500 }
104 501
105 - private function get_meta_key(): string {
106 - return static::CONTEXT_FRONTEND === $this->context
107 - ? static::META_KEY_FRONTEND
108 - : static::META_KEY_PREVIEW;
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 + }
109 510 }
110 511
111 - private function get_kit(): Kit {
112 - return $this->kit ?? Plugin::$instance->kits_manager->get_active_kit();
512 + private function flush_runtime_cache(): void {
513 + if ( function_exists( 'wp_cache_flush_runtime' ) ) {
514 + wp_cache_flush_runtime();
515 + }
113 516 }
114 517 }