PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.19
ووسلام – همگام سازی ووکامرس و باسلام v1.10.19
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 1.10.1 1.10.0 1.9.2 1.9.1 1.9.0 1.8.8 1.8.5 1.8.6 All 53 releases
sync-basalam / includes / Services / Products / ProductConnection.php

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

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