PluginProbe
Elementor Website Builder – more than just a page builder / 4.2.0
Elementor Website Builder – more than just a page builder v4.2.0
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 4.1.0-beta1 4.1.0-dev1 4.0.7 All 451 releases
elementor / modules / global-classes / atomic-global-styles.php

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

414 lines 13.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Elementor\Modules\GlobalClasses;
4
5 use Elementor\Modules\AtomicWidgets\Styles\CacheValidity\Cache_Validity;
6 use Elementor\Plugin;
7 use Elementor\Modules\AtomicWidgets\Styles\Atomic_Styles_Manager;
8
9 class Atomic_Global_Styles {
10 const STYLES_KEY = 'global';
11 const RELATED_KEY = 'related';
12 const RELATED_REVERSE_KEY = 'related-reverse';
13
14 private Global_Classes_Relations $relations;
15
16 public function __construct( Global_Classes_Relations $relations ) {
17 $this->relations = $relations;
18 }
19
20 public function register_hooks() {
21 add_action(
22 'elementor/atomic-widgets/styles/register',
23 fn( Atomic_Styles_Manager $styles_manager, array $post_ids ) => $this->register_styles( $styles_manager, $post_ids ),
24 20,
25 2
26 );
27
28 add_action(
29 'elementor/global_classes/update',
30 fn( string $context, array $changes ) => $this->invalidate_cache_for_updated_classes( $context, $changes ),
31 10,
32 2
33 );
34
35 add_action(
36 'deleted_post',
37 fn( $post_id ) => $this->on_kit_delete( $post_id )
38 );
39
40 add_action(
41 'elementor/core/files/clear_cache',
42 fn() => $this->invalidate_all_cache(),
43 );
44
45 add_filter(
46 'elementor/atomic-widgets/settings/transformers/classes',
47 fn( $value ) => $this->transform_classes_names( $value )
48 );
49
50 add_action(
51 'elementor/document/after_save',
52 fn( $document ) => $this->on_document_save( $document )
53 );
54 }
55
56 private function register_styles( Atomic_Styles_Manager $styles_manager, array $post_ids ) {
57 $context = $this->get_context();
58
59 $parent_to_embedded = [];
60 $visited = [];
61
62 foreach ( $post_ids as $post_id ) {
63 $this->resolve_embedded_post_descendants( (int) $post_id, $parent_to_embedded, $visited );
64 }
65
66 $this->persist_relation_maps( $parent_to_embedded, $context );
67
68 $all_embedded = [];
69 foreach ( $parent_to_embedded as $children ) {
70 $all_embedded = array_merge( $all_embedded, $children );
71 }
72 $all_embedded = array_flip( array_unique( $all_embedded ) ); // use as set
73
74 foreach ( $post_ids as $post_id ) {
75 $post_id_int = (int) $post_id;
76
77 if ( isset( $all_embedded[ $post_id_int ] ) ) {
78 // This post is embedded inside another rendered post – skip it so
79 // it doesn't produce its own global-{id}-*.css file.
80 continue;
81 }
82
83 $embedded_ids = $parent_to_embedded[ $post_id_int ] ?? [];
84 $aggregate_ids = array_merge( [ $post_id_int ], $embedded_ids );
85
86 $get_styles = fn() => $this->get_document_global_styles( $aggregate_ids, $context );
87
88 $styles_manager->register(
89 [ $this->get_cache_root_key(), $post_id, $context ],
90 $get_styles
91 );
92 }
93 }
94
95 /**
96 * Recursively resolve embedded post descendants for a parent post id.
97 *
98 * Applies the `elementor/document/related_posts` filter transitively and
99 * guards against cycles with the shared $visited set.
100 *
101 * @param int $pid Parent post id being inspected.
102 * @param array<int,int[]> $parent_to_embedded Accumulated forward map.
103 * @param array<int,true> $visited Cycle guard.
104 * @return int[] Embedded descendant post ids to merge into $pid's global styles.
105 */
106 private function resolve_embedded_post_descendants( int $pid, array &$parent_to_embedded, array &$visited ): array {
107 if ( isset( $visited[ $pid ] ) ) {
108 return $parent_to_embedded[ $pid ] ?? [];
109 }
110
111 $visited[ $pid ] = true;
112
113 $related = (array) apply_filters( 'elementor/document/related_posts', [], $pid );
114 $related = array_values( array_unique( array_map( 'intval', array_filter( $related, 'is_numeric' ) ) ) );
115
116 $all_related = $related;
117
118 foreach ( $related as $related_post ) {
119 $further_related_posts = $this->resolve_embedded_post_descendants( $related_post, $parent_to_embedded, $visited );
120 $all_related = array_values( array_unique( array_merge( $all_related, $further_related_posts ) ) );
121 }
122
123 $parent_to_embedded[ $pid ] = $all_related;
124
125 return $all_related;
126 }
127
128 /**
129 * Returns the merged, ordered global class styles for one or more post ids.
130 *
131 * @param int[] $post_ids One or more post ids whose classes should be merged.
132 * @param string $context Frontend or preview context.
133 * @return array Ordered style items ready for serialization.
134 */
135 private function get_document_global_styles( array $post_ids, string $context ): array {
136 $is_preview = Global_Classes_Repository::CONTEXT_PREVIEW === $context;
137
138 $class_ids = [];
139 foreach ( $post_ids as $pid ) {
140 $ids = $this->relations->set_preview( $is_preview )->get_styles_by_post( (int) $pid );
141 $class_ids = array_merge( $class_ids, $ids );
142 }
143 $class_ids = array_values( array_unique( $class_ids ) );
144
145 if ( empty( $class_ids ) ) {
146 return [];
147 }
148
149 $repository = Global_Classes_Repository::make();
150
151 if ( $is_preview ) {
152 $repository->set_preview( true );
153 }
154
155 $global_order = $repository->all_labels();
156 $ordered_class_ids = array_values( array_intersect( array_keys( $global_order ), $class_ids ) );
157
158 if ( empty( $ordered_class_ids ) ) {
159 return [];
160 }
161
162 $items = $repository->get_by_ids( $ordered_class_ids );
163 $reversed_order = array_reverse( $ordered_class_ids );
164
165 $styles = [];
166
167 foreach ( $reversed_order as $class_id ) {
168 $item = $items[ $class_id ] ?? null;
169
170 if ( ! $item ) {
171 continue;
172 }
173
174 $resolved_label = $global_order[ $class_id ] ?? $item['label'];
175 $item['id'] = $resolved_label;
176 $item['label'] = $resolved_label;
177 $styles[] = $item;
178 }
179
180 return $styles;
181 }
182
183 /**
184 * Persist the parent-to-child and child-to-parent relation maps.
185 *
186 * @param array<int,int[]> $parent_to_embedded Forward map: parent_id => child_ids[].
187 */
188 private function persist_relation_maps( array $parent_to_embedded, string $context ): void {
189 $cache_validity = new Cache_Validity();
190
191 foreach ( $parent_to_embedded as $parent => $new_children ) {
192 $forward_path = [ $this->get_cache_root_key( self::RELATED_KEY ), $parent, $context ];
193
194 $old_related_posts = array_map( 'intval', (array) ( $cache_validity->get_meta( $forward_path ) ?? [] ) );
195 $new_related_posts = array_map( 'intval', $new_children );
196
197 $added_posts = array_diff( $new_related_posts, $old_related_posts );
198 $removed_posts = array_diff( $old_related_posts, $new_related_posts );
199
200 $cache_validity->validate( $forward_path, $new_related_posts );
201
202 foreach ( $added_posts as $added_post ) {
203 $this->add_reverse_relation( $cache_validity, $added_post, $parent, $context );
204 }
205 foreach ( $removed_posts as $removed_post ) {
206 $this->remove_reverse_relation( $cache_validity, $removed_post, $parent, $context );
207 }
208 }
209 }
210
211 private function add_reverse_relation( Cache_Validity $cache_validity, int $child, int $parent, string $context ): void {
212 $reverse_path = [ $this->get_cache_root_key( self::RELATED_REVERSE_KEY ), $child, $context ];
213
214 $existing = array_map( 'intval', (array) ( $cache_validity->get_meta( $reverse_path ) ?? [] ) );
215
216 if ( in_array( $parent, $existing, true ) ) {
217 return;
218 }
219
220 $cache_validity->validate( $reverse_path, array_values( array_merge( $existing, [ $parent ] ) ) );
221 }
222
223 private function remove_reverse_relation( Cache_Validity $cache_validity, int $child, int $parent, string $context ): void {
224 $reverse_path = [ $this->get_cache_root_key( self::RELATED_REVERSE_KEY ), $child, $context ];
225
226 $existing = array_map( 'intval', (array) ( $cache_validity->get_meta( $reverse_path ) ?? [] ) );
227 $pruned = array_values( array_filter( $existing, fn( int $p ) => $p !== $parent ) );
228
229 $cache_validity->validate( $reverse_path, $pruned );
230 }
231
232 /**
233 * Look up all parent post ids that declared $child_post_id as embedded.
234 *
235 * @param int $child_post_id
236 * @return int[]
237 */
238 private function get_parent_post_ids( int $child_post_id, string $context ): array {
239 $cache_validity = new Cache_Validity();
240
241 $parents = $cache_validity->get_meta( [
242 $this->get_cache_root_key( self::RELATED_REVERSE_KEY ),
243 $child_post_id,
244 $context,
245 ] );
246
247 if ( ! is_array( $parents ) ) {
248 return [];
249 }
250
251 return array_values( array_unique( array_map( 'intval', $parents ) ) );
252 }
253
254 /**
255 * Walk the reverse relation map transitively to collect every ancestor
256 * post that embeds $post_id (direct parent, grandparent, etc.).
257 *
258 * @param int $post_id
259 * @return int[]
260 */
261 private function get_ancestor_post_ids( int $post_id, string $context ): array {
262 $ancestors = [];
263 $visited = [];
264 $queue = [ $post_id ];
265
266 while ( ! empty( $queue ) ) {
267 $current = array_shift( $queue );
268
269 foreach ( $this->get_parent_post_ids( $current, $context ) as $parent_id ) {
270 if ( isset( $visited[ $parent_id ] ) ) {
271 continue;
272 }
273
274 $visited[ $parent_id ] = true;
275 $ancestors[] = $parent_id;
276 $queue[] = $parent_id;
277 }
278 }
279
280 return array_values( array_unique( array_map( 'intval', $ancestors ) ) );
281 }
282
283 private function on_kit_delete( $post_id ) {
284 if ( ! Plugin::$instance->kits_manager->is_kit( $post_id ) ) {
285 return;
286 }
287
288 $this->invalidate_all_cache();
289 }
290
291 /**
292 * When an embedded post (component/template) is saved, its own CSS cache
293 * is cleared by Global_Classes_Relations. We additionally need to clear
294 * the parent's global CSS so it is regenerated with the updated child content.
295 */
296 private function on_document_save( $document ): void {
297 $post_id = (int) $document->get_main_id();
298 $context = $this->get_context();
299
300 $cache_validity = new Cache_Validity();
301 $cache_validity->invalidate( [ $this->get_cache_root_key( self::RELATED_KEY ), $post_id, $context ] );
302
303 foreach ( $this->get_ancestor_post_ids( $post_id, $context ) as $ancestor_id ) {
304 $this->invalidate_document_cache( $ancestor_id, $context );
305
306 $cache_validity->invalidate( [ $this->get_cache_root_key( self::RELATED_KEY ), $ancestor_id, $context ] );
307 }
308 }
309
310 private function invalidate_cache_for_updated_classes( string $context, array $changes ) {
311 if ( isset( $changes['order'] ) && $changes['order'] ) {
312 $this->invalidate_all_cache( $context );
313
314 return;
315 }
316
317 $affected = array_unique( array_merge(
318 $changes['added'] ?? [],
319 $changes['deleted'] ?? [],
320 $changes['modified'] ?? []
321 ) );
322
323 if ( empty( $affected ) ) {
324 return;
325 }
326
327 $document_ids = [];
328 $is_preview = Global_Classes_Repository::CONTEXT_PREVIEW === $context;
329
330 if ( ! empty( $changes['affected_post_ids'] ) ) {
331 foreach ( $changes['affected_post_ids'] as $post_id ) {
332 $document_ids[ (int) $post_id ] = true;
333 }
334 }
335
336 foreach ( $affected as $class_id ) {
337 foreach ( $this->relations->set_preview( $is_preview )->get_posts_by_style( $class_id ) as $doc_id ) {
338 $document_ids[ $doc_id ] = true;
339 }
340 }
341
342 if ( empty( $document_ids ) ) {
343 return;
344 }
345
346 // Also include ancestor posts of the directly-affected documents so that
347 // aggregated CSS bundles are regenerated when a descendant class changes.
348 $with_parents = $document_ids;
349 foreach ( array_keys( $document_ids ) as $doc_id ) {
350 foreach ( $this->get_ancestor_post_ids( (int) $doc_id, $context ) as $ancestor_id ) {
351 $with_parents[ $ancestor_id ] = true;
352 }
353 }
354
355 foreach ( array_keys( $with_parents ) as $post_id ) {
356 $this->invalidate_document_cache( $post_id, $context );
357 }
358 }
359
360 private function invalidate_document_cache( int $post_id, ?string $context = null ) {
361 if ( empty( $context ) ) {
362 do_action( 'elementor/atomic-widgets/styles/clear', [ $this->get_cache_root_key(), $post_id ] );
363 do_action( 'elementor/atomic-widgets/styles/clear', [ $this->get_cache_root_key( self::RELATED_KEY ), $post_id ] );
364 do_action( 'elementor/atomic-widgets/styles/clear', [ $this->get_cache_root_key( self::RELATED_REVERSE_KEY ), $post_id ] );
365 } else {
366 do_action( 'elementor/atomic-widgets/styles/clear', [ $this->get_cache_root_key(), $post_id, $context ] );
367 do_action( 'elementor/atomic-widgets/styles/clear', [ $this->get_cache_root_key( self::RELATED_KEY ), $post_id, $context ] );
368 do_action( 'elementor/atomic-widgets/styles/clear', [ $this->get_cache_root_key( self::RELATED_REVERSE_KEY ), $post_id, $context ] );
369 }
370 }
371
372 private function invalidate_all_cache( ?string $context = null ) {
373 if ( empty( $context ) || Global_Classes_Repository::CONTEXT_FRONTEND === $context ) {
374 do_action( 'elementor/atomic-widgets/styles/clear', [ $this->get_cache_root_key() ] );
375
376 $cache_validity = new Cache_Validity();
377 $cache_validity->invalidate( [ $this->get_cache_root_key( self::RELATED_KEY ) ] );
378 $cache_validity->invalidate( [ $this->get_cache_root_key( self::RELATED_REVERSE_KEY ) ] );
379
380 return;
381 }
382
383 do_action( 'elementor/atomic-widgets/styles/clear', [ $this->get_cache_root_key(), $context ] );
384 }
385
386 private function transform_classes_names( $ids ) {
387
388 $labels = Global_Classes_Repository::make()
389 ->set_preview( $this->is_preview() )
390 ->all_labels();
391
392 return array_map(
393 static function( $id ) use ( $labels ) {
394 return $labels[ $id ] ?? $id;
395 },
396 $ids
397 );
398 }
399
400 private function get_context(): string {
401 return $this->is_preview()
402 ? Global_Classes_Repository::CONTEXT_PREVIEW
403 : Global_Classes_Repository::CONTEXT_FRONTEND;
404 }
405
406 private function is_preview(): bool {
407 return Plugin::$instance->preview->is_editor_or_preview();
408 }
409
410 private function get_cache_root_key( string $key = null ): string {
411 return $key ? self::STYLES_KEY . '_' . $key : self::STYLES_KEY;
412 }
413 }
414