PluginProbe
Elementor Website Builder – more than just a page builder / 4.1.0-beta3
Elementor Website Builder – more than just a page builder v4.1.0-beta3
4.3.3 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 All 456 releases
← All changes | modules/global-classes/global-classes-repository.php +336 -50 4.0.9 → 4.1.0-beta3 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\Modules\GlobalClasses\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,28 @@
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 + 'meta_key' => [
33 + 'frontend' => self::META_KEY_FRONTEND,
34 + 'preview' => self::META_KEY_PREVIEW,
35 + ],
36 + ];
37 +
23 38 private ?Global_Classes $cache = null;
24 39
25 - private ?Kit $kit = null;
26 -
27 40 public function __construct( ?Kit $kit = null ) {
28 - $this->kit = $kit;
41 + if ( null !== $kit ) {
42 + $this->set_kit( $kit );
43 + }
29 44 }
30 45
31 46 public static function make( ?Kit $kit = null ): Global_Classes_Repository {
32 47 return new self( $kit );
@@ -31,84 +46,355 @@
31 46 public static function make( ?Kit $kit = null ): Global_Classes_Repository {
32 47 return new self( $kit );
33 48 }
34 49
35 - public function context( string $context ): self {
36 - $this->context = $context;
50 + protected function on_preview_change(): void {
37 51 $this->cache = null;
38 -
39 - return $this;
40 52 }
41 53
54 + /**
55 + * This method may be too heavy to use
56 + * Be mindful as this call would cause the server to freeze for as much time as needed until it fetches
57 + * all global classes
58 + */
42 59 public function all( bool $force = false ): Global_Classes {
43 60 if ( ! $force && null !== $this->cache ) {
44 61 return $this->cache;
45 62 }
46 63
47 - $meta_key = $this->get_meta_key();
48 - $kit = $this->get_kit();
49 - $all = $kit->get_json_meta( $meta_key );
64 + $this->cache = $this->all_from_posts();
50 65
51 - $is_preview = static::META_KEY_PREVIEW === $meta_key;
52 - $is_empty = empty( $all );
66 + return $this->cache;
67 + }
53 68
54 - if ( $is_preview && $is_empty ) {
55 - $all = $kit->get_json_meta( static::META_KEY_FRONTEND );
69 + public function all_labels(): array {
70 + return $this->labels()->get_ordered_labels();
71 + }
72 +
73 + public function get_order(): array {
74 + return Global_Classes_Order::make( $this->get_kit() )->get_order();
75 + }
76 +
77 + public function update_order_and_labels( array $order, array $new_labels ): void {
78 + Global_Classes_Order::make( $this->get_kit() )->set_order( $order );
79 +
80 + $labels = $this->labels();
81 + $existing_labels = $labels->get_labels();
82 +
83 + foreach ( $new_labels as $id => $label ) {
84 + $existing_labels[ $id ] = $label;
56 85 }
57 86
58 - $all['order'] = Global_Classes_Parser::sanitize_order( $all['items'] ?? [], $all['order'] ?? [] );
87 + $labels->set_labels( $existing_labels );
59 88
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 );
89 + $this->cache = null;
90 + }
91 +
92 + private function labels(): Global_Classes_Labels {
93 + return Global_Classes_Labels::make( $this->get_kit() )->set_preview( $this->is_preview() );
94 + }
95 +
96 + public function get( string $class_id ): ?array {
97 + $post = Global_Class_Post::find_by_class_id( $class_id, $this->is_preview() );
98 +
99 + return $post ? $post->to_array() : null;
100 + }
101 +
102 + public function get_by_ids( array $class_ids ): array {
103 + if ( empty( $class_ids ) ) {
104 + return [];
105 + }
106 +
107 + $items = [];
108 +
109 + foreach ( $this->iterate_class_posts_for_ids( $class_ids ) as $class_post ) {
110 + $class_data = $class_post->to_array();
111 + $items[ $class_data['id'] ] = $class_data;
112 + }
113 +
114 + return $items;
115 + }
116 +
117 + public function apply_changes( array $touched_items, array $changes, array $order ): void {
118 + $labels = $this->labels();
119 + $before = $labels->get_labels();
120 + $is_preview = $this->is_preview();
121 + $to_delete = $changes['deleted'] ?? [];
122 + $to_create = $changes['added'] ?? [];
123 + $to_update = $changes['modified'] ?? [];
124 + $order_changed = isset( $changes['order'] ) && $changes['order'];
125 +
126 + $final_label_map = [];
127 + foreach ( $order as $id ) {
128 + if ( isset( $touched_items[ $id ] ) ) {
129 + $final_label_map[ $id ] = $touched_items[ $id ]['label'];
130 + } elseif ( isset( $before[ $id ] ) ) {
131 + $final_label_map[ $id ] = $before[ $id ];
66 132 }
67 - );
133 + }
68 134
69 - $this->cache = Global_Classes::make( $all['items'] ?? [], $all['order'] ?? [] );
135 + $this->persist_class_batch_mutations( $to_delete, $to_create, $to_update, $touched_items, $is_preview );
70 136
71 - return $this->cache;
137 + $classes_order = Global_Classes_Order::make( $this->get_kit() );
138 + $classes_order->set_order( $order );
139 + $labels->set_labels( $final_label_map );
140 +
141 + if ( ! $is_preview ) {
142 + Global_Classes_Sync_Map::make( $this->get_kit() )->apply_changes( $touched_items, $to_delete );
143 +
144 + $this->each_class_id_batch( array_values( $to_update ), function ( string $class_id ) {
145 + $post = Global_Class_Post::find_by_class_id( $class_id, false );
146 + if ( $post ) {
147 + delete_post_meta( $post->get_post_id(), Global_Class_Post::META_KEY_DATA_PREVIEW );
148 + clean_post_cache( $post->get_post_id() );
149 + }
150 + } );
151 + }
152 +
153 + $this->cache = null;
154 + $this->flush_runtime_cache();
155 +
156 + do_action( 'elementor/global_classes/update', $this->get_context_key( 'event' ), [
157 + 'added' => $to_create,
158 + 'deleted' => $to_delete,
159 + 'modified' => $to_update,
160 + 'order' => $order_changed,
161 + ] );
72 162 }
73 163
164 + public function each_item( callable $cb, bool $skip_migration = false, int $batch_size = self::READ_BATCH_SIZE ): void {
165 + $order = Global_Classes_Order::make( $this->get_kit() )->get_order();
166 +
167 + if ( empty( $order ) ) {
168 + return;
169 + }
170 +
171 + foreach ( array_chunk( $order, $batch_size ) as $chunk ) {
172 + foreach ( $this->iterate_class_posts_for_ids( $chunk ) as $class_post ) {
173 + $cb( $class_post->to_array( $skip_migration ) );
174 + }
175 + }
176 + }
177 +
74 178 public function put( array $items, array $order ) {
75 - $current_value = $this->all()->get();
179 + $current_ids = Global_Classes_Order::make( $this->get_kit() )->get_order();
76 180
77 - $updated_value = [
78 - 'items' => $items,
79 - 'order' => $order,
181 + $new_ids = array_keys( $items );
182 +
183 + $current_order_string = implode( ';', $current_ids );
184 +
185 + $changes = [
186 + 'added' => array_values( array_diff( $new_ids, $current_ids ) ),
187 + 'deleted' => array_values( array_diff( $current_ids, $new_ids ) ),
188 + 'modified' => array_values( array_intersect( $new_ids, $current_ids ) ),
189 + 'order' => implode( ';', $order ) !== $current_order_string,
80 190 ];
81 191
82 - // `update_metadata` fails for identical data, so we skip it.
83 - if ( $current_value === $updated_value ) {
84 - return;
192 + $this->put_to_posts( $items, $order, $current_ids );
193 +
194 + $this->cache = null;
195 +
196 + do_action( 'elementor/global_classes/update', $this->get_context_key( 'event' ), $changes );
197 + }
198 +
199 + private function all_from_posts(): Global_Classes {
200 + $classes_order = Global_Classes_Order::make( $this->get_kit() );
201 + $order = $classes_order->get_order();
202 +
203 + if ( empty( $order ) ) {
204 + return Global_Classes::make( [], [] );
85 205 }
86 206
87 - $meta_key = $this->get_meta_key();
88 - $value = $this->get_kit()->update_json_meta( $meta_key, $updated_value );
207 + $items = [];
89 208
90 - $should_delete_preview = static::META_KEY_FRONTEND === $meta_key;
209 + foreach ( $this->iterate_class_posts_for_ids( $order ) as $class_post ) {
210 + $class_data = $class_post->to_array();
211 + $items[ $class_data['id'] ] = $class_data;
212 + }
91 213
92 - if ( $should_delete_preview ) {
93 - $this->get_kit()->delete_meta( static::META_KEY_PREVIEW );
214 + $order = Global_Classes_Parser::sanitize_order( $items, $order );
215 +
216 + return Global_Classes::make( $items, $order );
217 + }
218 +
219 + private function put_to_posts( array $items, array $order, array $current_ids ): void {
220 + $is_preview = $this->is_preview();
221 + $new_ids = array_keys( $items );
222 +
223 + $to_delete = array_diff( $current_ids, $new_ids );
224 + $to_create = array_diff( $new_ids, $current_ids );
225 + $to_update = array_intersect( $new_ids, $current_ids );
226 +
227 + $this->persist_class_batch_mutations( $to_delete, $to_create, $to_update, $items, $is_preview );
228 +
229 + $classes_order = Global_Classes_Order::make( $this->get_kit() );
230 + $classes_order->set_order( $order );
231 +
232 + $label_map = [];
233 + foreach ( $order as $id ) {
234 + if ( isset( $items[ $id ]['label'] ) ) {
235 + $label_map[ $id ] = $items[ $id ]['label'];
236 + }
94 237 }
238 + $this->labels()->set_labels( $label_map );
95 239
96 - if ( ! $value ) {
97 - throw new \Exception( 'Failed to update global classes' );
240 + if ( ! $is_preview ) {
241 + $touched_ids = array_merge( array_values( $to_create ), array_values( $to_update ) );
242 + $touched_items = array_intersect_key(
243 + $items,
244 + array_flip( $touched_ids )
245 + );
246 + Global_Classes_Sync_Map::make( $this->get_kit() )->apply_changes( $touched_items, array_values( $to_delete ) );
247 +
248 + $this->each_class_id_batch( array_values( $to_update ), function ( string $class_id ) {
249 + $post = Global_Class_Post::find_by_class_id( $class_id, false );
250 + if ( $post ) {
251 + delete_post_meta( $post->get_post_id(), Global_Class_Post::META_KEY_DATA_PREVIEW );
252 + clean_post_cache( $post->get_post_id() );
253 + }
254 + } );
98 255 }
256 + }
99 257
100 - $this->cache = null;
258 + private function iterate_class_posts_for_ids( array $class_ids ): \Generator {
259 + foreach ( array_chunk( $class_ids, self::READ_BATCH_SIZE ) as $chunk ) {
260 + $posts = get_posts( [
261 + 'post_type' => Global_Class_Post_Type::CPT,
262 + 'post_status' => 'publish',
263 + 'posts_per_page' => -1,
264 + 'meta_query' => [
265 + [
266 + 'key' => Global_Class_Post::META_KEY_ID,
267 + 'value' => $chunk,
268 + 'compare' => 'IN',
269 + ],
270 + ],
271 + ] );
101 272
102 - do_action( 'elementor/global_classes/update', $this->context, $updated_value, $current_value );
273 + $seen_class_ids = [];
274 +
275 + foreach ( $posts as $post ) {
276 + $class_id = get_post_meta( $post->ID, Global_Class_Post::META_KEY_ID, true );
277 +
278 + if ( $class_id && isset( $seen_class_ids[ $class_id ] ) ) {
279 + // if for some reason there's already a post meta to that class - delete any other ones
280 + // otherwise we may end up updating a stale post meta
281 + wp_delete_post( $post->ID, true );
282 + continue;
283 + }
284 +
285 + if ( $class_id ) {
286 + $seen_class_ids[ $class_id ] = true;
287 + }
288 +
289 + yield Global_Class_Post::from_post( $post, $this->is_preview() );
290 + clean_post_cache( $post->ID );
291 + }
292 + unset( $posts, $seen_class_ids );
293 + $this->flush_runtime_cache();
294 + }
103 295 }
104 296
105 - private function get_meta_key(): string {
106 - return static::CONTEXT_FRONTEND === $this->context
107 - ? static::META_KEY_FRONTEND
108 - : static::META_KEY_PREVIEW;
297 + private function persist_class_batch_mutations(
298 + array $to_delete,
299 + array $to_create,
300 + array $to_update,
301 + array $items_by_id,
302 + bool $is_preview
303 + ): void {
304 + $relations = new Global_Classes_Relations();
305 +
306 + $this->each_class_id_batch( array_values( $to_delete ), function ( string $class_id ) use ( $is_preview, $relations ) {
307 + $post = Global_Class_Post::find_by_class_id( $class_id, false );
308 +
309 + if ( $post ) {
310 + if ( $is_preview ) {
311 + $post->set_preview( true );
312 + $post->update_data( [] );
313 + clean_post_cache( $post->get_post_id() );
314 + } else {
315 + $relations->clear_class_relations( $class_id );
316 + $post->delete();
317 + }
318 + }
319 + } );
320 +
321 + $this->each_class_id_batch( $to_create, function ( string $class_id ) use ( $items_by_id, $is_preview ) {
322 + if ( ! isset( $items_by_id[ $class_id ] ) ) {
323 + return;
324 + }
325 +
326 + $item = $items_by_id[ $class_id ];
327 + $data = Global_Class_Data_Normalizer::normalize_style_fields( $item );
328 +
329 + if ( $is_preview ) {
330 + $post = Global_Class_Post::find_by_class_id( $class_id, true );
331 +
332 + if ( $post ) {
333 + $post->set_preview( true );
334 + $post->update_data( $data );
335 + $post->update_label( $item['label'] );
336 + clean_post_cache( $post->get_post_id() );
337 + } else {
338 + $created = Global_Class_Post::create( $class_id, $item['label'], $data );
339 + if ( $created ) {
340 + clean_post_cache( $created->get_post_id() );
341 + }
342 + }
343 + } else {
344 + $created = Global_Class_Post::create( $class_id, $item['label'], $data );
345 + if ( $created ) {
346 + clean_post_cache( $created->get_post_id() );
347 + }
348 + }
349 + } );
350 +
351 + $this->each_class_id_batch( $to_update, function ( string $class_id ) use ( $items_by_id, $is_preview ) {
352 + if ( ! isset( $items_by_id[ $class_id ] ) ) {
353 + return;
354 + }
355 +
356 + $item = $items_by_id[ $class_id ];
357 + $post = Global_Class_Post::find_by_class_id( $class_id, $is_preview );
358 +
359 + if ( ! $post ) {
360 + return;
361 + }
362 +
363 + $data = Global_Class_Data_Normalizer::normalize_style_fields( $item );
364 + $post->update_data( $data );
365 + $post->update_label( $item['label'] );
366 + clean_post_cache( $post->get_post_id() );
367 + } );
109 368 }
110 369
111 - private function get_kit(): Kit {
112 - return $this->kit ?? Plugin::$instance->kits_manager->get_active_kit();
370 + public function delete_all(): void {
371 + $order = $this->get_order();
372 +
373 + $this->each_class_id_batch( $order, function ( string $class_id ) {
374 + $post = Global_Class_Post::find_by_class_id( $class_id );
375 +
376 + if ( $post ) {
377 + $post->delete();
378 + }
379 + } );
380 +
381 + Global_Classes_Order::make( $this->get_kit() )->set_order( [] );
382 + $this->labels()->set_labels( [] );
383 + }
384 +
385 + private function each_class_id_batch( $class_ids, callable $callback, int $batch_size = self::PERSIST_BATCH_SIZE ): void {
386 + $class_ids = is_array( $class_ids ) ? $class_ids : iterator_to_array( $class_ids, false );
387 + foreach ( array_chunk( array_values( $class_ids ), $batch_size ) as $batch ) {
388 + foreach ( $batch as $class_id ) {
389 + $callback( $class_id );
390 + }
391 + $this->flush_runtime_cache();
392 + }
393 + }
394 +
395 + private function flush_runtime_cache(): void {
396 + if ( function_exists( 'wp_cache_flush_runtime' ) ) {
397 + wp_cache_flush_runtime();
398 + }
113 399 }
114 400 }