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
elementor / modules / global-classes / database / migrations / migrate-all-kits-post-ids.php

migrate-all-kits-post-ids.php in Elementor Website Builder – more than just a page builder 4.3.2, at modules/global-classes/database/migrations/migrate-all-kits-post-ids.php

305 lines 9.1 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\Database\Migrations;
4
5 use Elementor\Core\Database\Base_Migration;
6 use Elementor\Core\Kits\Documents\Kit;
7 use Elementor\Modules\GlobalClasses\Global_Class_Post;
8 use Elementor\Modules\GlobalClasses\Global_Class_Post_Type;
9 use Elementor\Modules\GlobalClasses\Global_Classes_Order;
10 use Elementor\Modules\GlobalClasses\Global_Classes_Post_IDs;
11 use Elementor\Modules\GlobalClasses\Global_Classes_Repository;
12 use Elementor\Modules\GlobalClasses\Utils\Global_Class_Data_Normalizer;
13 use Elementor\Modules\GlobalClasses\Utils\Kit_Utils;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit; // Exit if accessed directly.
17 }
18
19 /**
20 * Ensures every kit on the site has a complete, exclusive _elementor_global_classes_post_ids map.
21 *
22 * Pass A — Kits that were never migrated (no order meta, but have the old aggregated meta):
23 * Runs the same migrate-to-posts logic that previously applied only to the active kit.
24 *
25 * Pass B — Kits that are already migrated but may be missing some entries in their post-id map
26 * (possible due to the old lazy-backfill being shared across kits):
27 * Fills in missing class_id → post_id entries using a conflict-aware resolver that
28 * never reuses a post_id already claimed by another kit.
29 *
30 * Pass C — Ensures no single post_id is referenced by more than one kit's map.
31 * For each shared post_id, the kit with the smallest ID is kept as the owner; all
32 * other ("loser") kits get a freshly created CPT post cloned from the current data.
33 */
34 class Migrate_All_Kits_Post_IDs extends Base_Migration {
35
36 public function up(): void {
37 Global_Class_Post_Type::ensure_registered();
38
39 $kits = Kit_Utils::get_all_kit_documents();
40
41 if ( empty( $kits ) ) {
42 return;
43 }
44
45 // Pass A: restructure kits that still use the old aggregate meta.
46 $this->restructure_unmigrated_kits( $kits );
47
48 // Pass B: fill any gaps in the post-id map for each kit.
49 $claimed_post_ids = $this->build_claimed_post_ids( $kits );
50 $this->fill_missing_post_ids( $kits, $claimed_post_ids );
51
52 // Pass C: break any remaining sharing — one post_id must belong to exactly one kit.
53 $this->deduplicate_shared_post_ids( $kits );
54 }
55
56 // -------------------------------------------------------------------------
57 // Pass A
58 // -------------------------------------------------------------------------
59
60 /**
61 * @param Kit[] $kits
62 */
63 private function restructure_unmigrated_kits( array $kits ): void {
64 foreach ( $kits as $kit ) {
65 Migrate_To_Posts::migrate_kit( $kit );
66 }
67 }
68
69 // -------------------------------------------------------------------------
70 // Pass B
71 // -------------------------------------------------------------------------
72
73 /**
74 * Build the union of all post_ids already mapped by any kit on this site.
75 *
76 * @param Kit[] $kits
77 * @return array<int, true> post_id => true
78 */
79 private function build_claimed_post_ids( array $kits ): array {
80 $claimed_post_ids = [];
81
82 foreach ( $kits as $kit ) {
83 $map = $this->read_post_ids_map( $kit );
84
85 foreach ( $map as $post_id ) {
86 $claimed_post_ids[ (int) $post_id ] = true;
87 }
88 }
89
90 return $claimed_post_ids;
91 }
92
93 /**
94 * @param Kit[] $kits
95 * @param array<int, true> $claimed_post_ids Mutable — updated as we resolve entries.
96 */
97 private function fill_missing_post_ids( array $kits, array &$claimed_post_ids ): void {
98 foreach ( $kits as $kit ) {
99 $order = Global_Classes_Order::make( $kit )->set_preview( false )->get_order();
100
101 if ( empty( $order ) ) {
102 continue;
103 }
104
105 $map = $this->read_post_ids_map( $kit );
106 $missing = array_diff( $order, array_keys( $map ) );
107
108 if ( empty( $missing ) ) {
109 continue;
110 }
111
112 $aggregate_items = Migrate_To_Posts::get_aggregate_global_classes( $kit )['items'] ?? [];
113 $resolved = [];
114
115 foreach ( $missing as $class_id ) {
116 $post_id = $this->resolve_post_id_for_class( $class_id, $kit, $aggregate_items, $claimed_post_ids );
117
118 if ( null !== $post_id ) {
119 $resolved[ $class_id ] = $post_id;
120 $claimed_post_ids[ $post_id ] = true;
121 }
122 }
123
124 if ( ! empty( $resolved ) ) {
125 Global_Classes_Post_IDs::make( $kit )->set_many( $resolved );
126 }
127 }
128 }
129
130 /**
131 * Find or create a post_id for a class_id that belongs exclusively to $kit.
132 *
133 * Priority:
134 * 1. Find an existing CPT post for this class_id that is not yet claimed by any kit.
135 * 2. Create a fresh CPT post from the kit's aggregate data.
136 * 3. If no aggregate data exists, skip (returns null).
137 *
138 * @param string $class_id
139 * @param ?Kit $kit
140 * @param array<string, array> $aggregate_items
141 * @param array<int, true> $claimed_post_ids Passed by reference so the caller can mark reused ids.
142 * @return int|null
143 */
144 private function resolve_post_id_for_class(
145 string $class_id,
146 ?Kit $kit,
147 array $aggregate_items,
148 array &$claimed_post_ids
149 ): ?int {
150 // Try to reuse an unclaimed CPT post for this class_id.
151 $candidates = $this->query_cpt_post_ids_for_class( $class_id );
152
153 foreach ( $candidates as $candidate_id ) {
154 if ( ! isset( $claimed_post_ids[ $candidate_id ] ) ) {
155 return $candidate_id;
156 }
157 }
158
159 // All candidates are claimed (or there are none) — create a fresh post.
160 if ( empty( $aggregate_items[ $class_id ] ) ) {
161 if ( empty( $candidates ) ) {
162 // Nothing to create from; skip.
163 return null;
164 }
165
166 $source_post = Global_Class_Post::from_post_id( end( $candidates ), false );
167
168 if ( ! $source_post ) {
169 return null;
170 }
171
172 $created = $source_post->clone( $kit );
173
174 if ( ! $created ) {
175 return null;
176 }
177
178 return $created->get_post_id();
179 }
180
181 $item = $aggregate_items[ $class_id ];
182 $data = Global_Class_Data_Normalizer::normalize_style_fields( $item );
183
184 $label = $item['label'] ?? $class_id;
185
186 $created = Global_Class_Post::create( $class_id, $label, $data, $kit );
187
188 if ( ! $created ) {
189 return null;
190 }
191
192 return $created->get_post_id();
193 }
194
195 /**
196 * Query all published CPT post IDs that carry the given class_id, ordered ascending.
197 *
198 * @return int[]
199 */
200 private function query_cpt_post_ids_for_class( string $class_id ): array {
201 global $wpdb;
202
203 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
204 $sql = $wpdb->prepare(
205 "SELECT pm.post_id
206 FROM {$wpdb->postmeta} pm
207 INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
208 WHERE pm.meta_key = %s
209 AND pm.meta_value = %s
210 AND p.post_type = %s
211 AND p.post_status = %s
212 ORDER BY pm.post_id ASC",
213 Global_Class_Post::META_KEY_ID,
214 $class_id,
215 Global_Class_Post_Type::CPT,
216 'publish'
217 );
218 // phpcs:enable
219
220 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- prepared above.
221 $rows = $wpdb->get_col( $sql );
222
223 return array_map( 'intval', $rows );
224 }
225
226 // -------------------------------------------------------------------------
227 // Pass C
228 // -------------------------------------------------------------------------
229
230 /**
231 * @param Kit[] $kits
232 */
233 private function deduplicate_shared_post_ids( array $kits ): void {
234 // Build: post_id -> [(kit, class_id), ...] sorted by kit ID asc (lowest = owner).
235 $post_id_to_references = $this->build_post_id_reference_map( $kits );
236
237 foreach ( $post_id_to_references as $post_id => $references ) {
238 if ( count( $references ) <= 1 ) {
239 continue;
240 }
241
242 // First reference (lowest kit ID) is the owner. All others are losers.
243 $losers = array_slice( $references, 1 );
244
245 $source_post = Global_Class_Post::from_post_id( $post_id, false );
246
247 if ( ! $source_post ) {
248 continue;
249 }
250
251 $source_data = $source_post->get_data( true );
252 $source_label = $source_post->get_label();
253
254 foreach ( $losers as [ 'kit' => $loser_kit, 'class_id' => $class_id ] ) {
255 Global_Class_Post::create( $class_id, $source_label, $source_data, $loser_kit );
256 }
257 }
258 }
259
260 /**
261 * Returns an array keyed by post_id. Each value is a list of references:
262 * [ ['kit' => Kit, 'class_id' => string], ... ]
263 * sorted by kit post ID ascending so index 0 is always the "owner".
264 *
265 * @param Kit[] $kits Already sorted ascending by ID (Kit_Utils::get_all_kit_documents uses get_posts default order).
266 * @return array<int, array<int, array{kit: Kit, class_id: string}>>
267 */
268 private function build_post_id_reference_map( array $kits ): array {
269 // Sort kits ascending so the first reference is always the lowest-ID kit.
270 usort( $kits, fn( $a, $b ) => $a->get_id() <=> $b->get_id() );
271
272 $map = [];
273
274 foreach ( $kits as $kit ) {
275 $post_ids_map = $this->read_post_ids_map( $kit );
276
277 foreach ( $post_ids_map as $class_id => $post_id ) {
278 $post_id = (int) $post_id;
279
280 $map[ $post_id ][] = [
281 'kit' => $kit,
282 'class_id' => (string) $class_id,
283 ];
284 }
285 }
286
287 return $map;
288 }
289
290 // -------------------------------------------------------------------------
291 // Helpers
292 // -------------------------------------------------------------------------
293
294 /**
295 * Read the raw class_id -> post_id map stored on a kit's meta.
296 *
297 * @return array<string, int>
298 */
299 private function read_post_ids_map( Kit $kit ): array {
300 $raw = $kit->get_meta( Global_Classes_Post_IDs::META_KEY );
301
302 return is_array( $raw ) ? $raw : [];
303 }
304 }
305