PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.28
ووسلام – همگام سازی ووکامرس و باسلام v1.10.28
1.10.28 1.1.26 1.10.27 1.10.24 1.10.25 1.10.23 1.10.22 1.10.21 1.10.19 1.10.20 1.10.18 1.10.17 1.10.15 1.10.14 1.10.13 1.10.12 1.10.10 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 1.10.2 All 61 releases
sync-basalam / includes / Services / Products / ProductConnection.php

ProductConnection.php in ووسلام – همگام سازی ووکامرس و باسلام 1.10.28, at includes/Services/Products/ProductConnection.php

499 lines 14.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SyncBasalam\Services\Products;
4
5 use SyncBasalam\Jobs\Exceptions\NonRetryableException;
6 use SyncBasalam\Logger\Logger;
7 use SyncBasalam\Utilities\ProductMetaKey;
8
9 defined('ABSPATH') || exit;
10
11 /**
12 * اتصال �
13 حصول ووکا�
14 رس به �
15 حصول باسلا�
16 .
17 *
18 * هر �
19 حصول باسلا�
20 باید فقط به یک �
21 حصول ووکا�
22 رس �
23 تصل باشد. هر روشی که از یک �
24 حصول
25 * نسخه کپی �
26 ی‌سازد (کپی ووکا�
27 رس، افزونه‌های کپی �
28 حصول، درون‌ریزی csv، کپی �
29 ستقی�
30 دیتابیس)
31 * شناسه اتصال را روی �
32 حصول جدید ه�
33 �
34 ی‌نویسد و از آن به بعد بروزرسانی �
35 حصول جدید،
36 * �
37 حصول قدی�
38 ی را در باسلا�
39 تغییر �
40 ی‌دهد.
41 */
42 class ProductConnection
43 {
44 /**
45 * حذف کا�
46 ل اتصال �
47 حصول به باسلا�
48 (شا�
49 ل کلیدهای �
50 ربوط به غرفه‌های قبلی و ه�
51 ه تنوع‌ها).
52 */
53 public static function purge($productId): void
54 {
55 $productId = (int) $productId;
56
57 if ($productId <= 0) return;
58
59 foreach (self::storedConnectionMetaKeys($productId) as $metaKey) {
60 delete_post_meta($productId, $metaKey);
61 }
62
63 foreach (self::variationIds($productId) as $variationId) {
64 delete_post_meta($variationId, ProductMetaKey::basalamVariationId());
65 delete_post_meta($variationId, 'sync_basalam_variation_map');
66 }
67 }
68
69 /**
70 * پاک کردن کا�
71 ل هر چیزی که ووسلا�
72 روی �
73 حصول و تنوع‌هایش ذخیره کرده است.
74 *
75 * نسخه کپی یک �
76 حصول تازه است: نه اتصال �
77 حصول اصلی، نه هیچ تنظی�
78 ووسلا�
79 ی (ویدیو،
80 * تغییر قی�
81 ت اختصاصی، فیلدهای طلا/�
82 وبایل، نوع و �
83 قدار �
84 حصول، ع�
85 ده‌فروشی، وضعیت تخفیف)
86 * نباید از �
87 حصول قبلی به آن ارث برسد.
88 */
89 public static function purgeAll($productId): void
90 {
91 $productId = (int) $productId;
92
93 if ($productId <= 0) return;
94
95 $postIds = array_merge([$productId], self::variationIds($productId));
96
97 foreach ($postIds as $postId) {
98 $metaKeys = apply_filters('sync_basalam_duplicate_purge_meta_keys', self::storedPluginMetaKeys($postId), $postId, $productId);
99
100 foreach ((array) $metaKeys as $metaKey) {
101 delete_post_meta($postId, $metaKey);
102 }
103 }
104 }
105
106 /**
107 * پاک کردن کا�
108 ل فقط در صورتی که این �
109 حصول نسخه کپی باشد؛ یعنی �
110 حصول قدی�
111 ی‌تری با ه�
112 ان اتصال وجود داشته باشد.
113 *
114 * برای هوک افزونه‌های کپی �
115 حصول استفاده �
116 ی‌شود، جایی که �
117 ط�
118 ئن نیستی�
119 شناسه‌ی داده شده
120 * �
121 حصول جدید است یا �
122 حصول اصلی؛ اینطوری داده‌های �
123 حصول اصلی هیچ‌وقت پاک ن�
124 ی‌شود.
125 */
126 public static function purgeIfCopy($productId): bool
127 {
128 $productId = (int) $productId;
129
130 if ($productId <= 0) return false;
131
132 $conflicts = self::conflictMap([$productId]);
133 $olderOwners = $conflicts[$productId] ?? [];
134
135 if (!$olderOwners || $productId < min($olderOwners)) return false;
136
137 self::purgeAll($productId);
138
139 return true;
140 }
141
142 /**
143 * آیا این کلید �
144 تا �
145 تعلق به ووسلا�
146 است؟
147 */
148 public static function isPluginMetaKey($metaKey): bool
149 {
150 $metaKey = (string) $metaKey;
151
152 foreach (self::pluginMetaKeyPrefixes() as $prefix) {
153 if (strpos($metaKey, $prefix) === 0) return true;
154 }
155
156 return false;
157 }
158
159 /**
160 * ه�
161 ه کلیدهای ووسلا�
162 که ه�
163 ‌اکنون روی یک پست (�
164 حصول یا تنوع) ذخیره شده‌اند.
165 */
166 public static function storedPluginMetaKeys(int $postId): array
167 {
168 return self::storedMetaKeysByPrefix($postId, self::pluginMetaKeyPrefixes());
169 }
170
171 /**
172 * پیشوند ه�
173 ه �
174 تاهای ووسلا�
175 روی �
176 حصول و تنوع (اتصال، وضعیت، تخفیف و ه�
177 ه فیلدهای تنظی�
178 ات).
179 */
180 public static function pluginMetaKeyPrefixes(): array
181 {
182 return ['sync_basalam_', '_sync_basalam_'];
183 }
184
185 /**
186 * کلیدهای اتصالی که ه�
187 ‌اکنون روی �
188 حصول ذخیره شده‌اند؛ بدون توجه به شناسه غرفه انتهای کلید.
189 */
190 public static function storedConnectionMetaKeys(int $productId): array
191 {
192 return self::storedMetaKeysByPrefix($productId, ProductMetaKey::basalamProductMetaKeyPrefixes());
193 }
194
195 private static function storedMetaKeysByPrefix(int $postId, array $prefixes): array
196 {
197 global $wpdb;
198
199 $keys = [];
200
201 foreach ($prefixes as $prefix) {
202 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Meta keys are vendor suffixed, so a LIKE lookup on core postmeta is required; results are not cacheable per key.
203 $rows = $wpdb->get_col(
204 $wpdb->prepare(
205 "SELECT DISTINCT meta_key FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key LIKE %s",
206 $postId,
207 $wpdb->esc_like($prefix) . '%'
208 )
209 );
210
211 if ($rows) $keys = array_merge($keys, $rows);
212 }
213
214 return array_values(array_unique($keys));
215 }
216
217 /**
218 * شناسه �
219 حصولات دیگری که به ه�
220 ان �
221 حصول باسلا�
222 �
223 تصل هستند.
224 *
225 * @return array<int, int[]> [شناسه �
226 حصول ووکا�
227 رس => شناسه �
228 حصولات �
229 تعارض]
230 */
231 public static function conflictMap(array $productIds): array
232 {
233 global $wpdb;
234
235 $productIds = array_values(array_unique(array_filter(array_map('intval', $productIds))));
236
237 if (!$productIds) return [];
238
239 $metaKey = ProductMetaKey::basalamProductId();
240 $idPlaceholders = implode(',', array_fill(0, count($productIds), '%d'));
241
242 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Placeholders are generated from an integer list and passed to prepare().
243 $ownedRows = $wpdb->get_results(
244 $wpdb->prepare(
245 "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s AND meta_value != '' AND post_id IN ({$idPlaceholders})",
246 array_merge([$metaKey], $productIds)
247 )
248 );
249
250 if (!$ownedRows) return [];
251
252 $basalamIds = array_values(array_unique(wp_list_pluck($ownedRows, 'meta_value')));
253 $valuePlaceholders = implode(',', array_fill(0, count($basalamIds), '%s'));
254
255 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Placeholders are generated from the value list and passed to prepare().
256 $ownerRows = $wpdb->get_results(
257 $wpdb->prepare(
258 "SELECT pm.post_id, pm.meta_value
259 FROM {$wpdb->postmeta} pm
260 INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
261 WHERE pm.meta_key = %s AND pm.meta_value IN ({$valuePlaceholders}) AND p.post_type = 'product'",
262 array_merge([$metaKey], $basalamIds)
263 )
264 );
265
266 $ownersByBasalamId = [];
267
268 foreach ($ownerRows as $row) {
269 $ownersByBasalamId[(string) $row->meta_value][] = (int) $row->post_id;
270 }
271
272 $conflicts = [];
273
274 foreach ($ownedRows as $row) {
275 $productId = (int) $row->post_id;
276 $owners = $ownersByBasalamId[(string) $row->meta_value] ?? [];
277 $others = array_values(array_diff(array_unique($owners), [$productId]));
278
279 if ($others) $conflicts[$productId] = $others;
280 }
281
282 return $conflicts;
283 }
284
285 /**
286 * آیا این �
287 حصول با �
288 حصول دیگری روی یک �
289 حصول باسلا�
290 �
291 شترک است؟
292 */
293 public static function hasConflict($productId): bool
294 {
295 return self::conflictingProductIds($productId) !== [];
296 }
297
298 public static function conflictingProductIds($productId): array
299 {
300 $productId = (int) $productId;
301
302 if ($productId <= 0) return [];
303
304 $conflicts = self::conflictMap([$productId]);
305
306 if (empty($conflicts[$productId])) return [];
307
308 self::reportConflict($productId, $conflicts[$productId]);
309
310 return $conflicts[$productId];
311 }
312
313 /**
314 * جلوگیری از ارسال هر درخواستی به باسلا�
315 وقتی اتصال �
316 حصول قابل اعت�
317 اد نیست.
318 *
319 * @throws NonRetryableException
320 */
321 public static function assertUnique($productId, $basalamProductId = null): void
322 {
323 $productId = (int) $productId;
324 $basalamProductId = $basalamProductId !== null && $basalamProductId !== ''
325 ? $basalamProductId
326 : get_post_meta($productId, ProductMetaKey::basalamProductId(), true);
327
328 if (empty($basalamProductId)) {
329 throw NonRetryableException::invalidData('این �
330 حصول به هیچ �
331 حصولی در باسلا�
332 �
333 تصل نیست.');
334 }
335
336 $conflictingIds = self::conflictingProductIds($productId);
337
338 if (!$conflictingIds) return;
339
340 throw NonRetryableException::permanent(esc_html(self::conflictMessage($basalamProductId, $conflictingIds)));
341 }
342
343 public static function conflictMessage($basalamProductId, array $conflictingIds): string
344 {
345 return sprintf(
346 'این �
347 حصول با �
348 حصول(های) %s روی یک �
349 حصول باسلا�
350 (شناسه %s) اتصال �
351 شترک دارد. برای جلوگیری از تغییر اشتباه �
352 حصول در باسلا�
353 ، ع�
354 لیات انجا�
355 نشد؛ ابتدا اتصال �
356 حصول تکراری (�
357 ع�
358 ولا نسخه کپی شده) را قطع کنید.',
359 implode('، ', array_map('strval', $conflictingIds)),
360 (string) $basalamProductId
361 );
362 }
363
364 /**
365 * ه�
366 ه اتصال‌های تکراری سایت.
367 *
368 * @return array<int, array{basalam_product_id: string, product_ids: int[]}>
369 */
370 public static function duplicateConnections(): array
371 {
372 global $wpdb;
373
374 $metaKey = ProductMetaKey::basalamProductId();
375
376 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-off integrity scan over core postmeta; there is no cache to read from.
377 $rows = $wpdb->get_results(
378 $wpdb->prepare(
379 "SELECT pm.meta_value AS basalam_product_id, GROUP_CONCAT(pm.post_id ORDER BY pm.post_id ASC) AS product_ids
380 FROM {$wpdb->postmeta} pm
381 INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
382 WHERE pm.meta_key = %s AND pm.meta_value != '' AND p.post_type = 'product'
383 GROUP BY pm.meta_value
384 HAVING COUNT(DISTINCT pm.post_id) > 1",
385 $metaKey
386 )
387 );
388
389 $duplicates = [];
390
391 foreach ($rows as $row) {
392 $productIds = array_values(array_unique(array_map('intval', explode(',', (string) $row->product_ids))));
393
394 sort($productIds);
395
396 $duplicates[] = [
397 'basalam_product_id' => (string) $row->basalam_product_id,
398 'product_ids' => $productIds,
399 ];
400 }
401
402 return $duplicates;
403 }
404
405 /**
406 * قطع اتصال نسخه‌های کپی و نگه داشتن قدی�
407 ی‌ترین �
408 حصول (�
409 حصول اصلی).
410 *
411 * @return array<int, array{basalam_product_id: string, kept: int, disconnected: int[]}>
412 */
413 public static function repairDuplicateConnections(): array
414 {
415 $repairs = [];
416
417 foreach (self::duplicateConnections() as $duplicate) {
418 $productIds = $duplicate['product_ids'];
419 $kept = array_shift($productIds);
420
421 if (!$productIds) continue;
422
423 foreach ($productIds as $productId) {
424 self::purge($productId);
425 }
426
427 $repair = [
428 'basalam_product_id' => $duplicate['basalam_product_id'],
429 'kept' => (int) $kept,
430 'disconnected' => $productIds,
431 ];
432
433 Logger::warning('اتصال تکراری به یک �
434 حصول باسلا�
435 پیدا و اصلاح شد.', [
436 'شناسه_�
437 حصول_باسلا�
438 ' => $repair['basalam_product_id'],
439 '�
440 حصول_باقی_�
441 انده' => $repair['kept'],
442 'اتصال_قطع_شده' => implode('، ', array_map('strval', $repair['disconnected'])),
443 'ع�
444 لیات' => 'اصلاح اتصال تکراری',
445 ]);
446
447 $repairs[] = $repair;
448 }
449
450 if ($repairs) DuplicateConnectionReport::recordRepairs($repairs);
451
452 return $repairs;
453 }
454
455 /**
456 * ثبت اتصال �
457 شترک در گزارش �
458 دیر و لاگ (فقط بار اول برای هر �
459 ورد).
460 */
461 public static function reportConflict(int $productId, array $conflictingIds): void
462 {
463 $isNew = DuplicateConnectionReport::recordConflict($productId, $conflictingIds);
464
465 if (!$isNew) return;
466
467 Logger::error('ه�
468 گا�
469 ‌سازی �
470 توقف شد: اتصال این �
471 حصول با �
472 حصول دیگری �
473 شترک است.', [
474 'product_id' => $productId,
475 '�
476 حصولات_�
477 تعارض' => implode('، ', array_map('strval', $conflictingIds)),
478 'ع�
479 لیات' => 'بررسی اتصال �
480 حصول',
481 ]);
482 }
483
484 private static function variationIds(int $productId): array
485 {
486 global $wpdb;
487
488 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Children are read directly so that variations are cleaned even before the parent object is loadable as a variable product.
489 $ids = $wpdb->get_col(
490 $wpdb->prepare(
491 "SELECT ID FROM {$wpdb->posts} WHERE post_parent = %d AND post_type = 'product_variation'",
492 $productId
493 )
494 );
495
496 return array_map('intval', $ids ?: []);
497 }
498 }
499