PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.0
1.7.0 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 All 50 releases
← All changes | app/Helpers/Helper.php +294 -119 1.6.1 → 1.7.0 View file →
@@ -11,8 +11,9 @@
11 11 use FluentCart\App\Models\Customer;
12 12 use FluentCart\App\Models\ProductVariation;
13 13 use FluentCart\App\Models\User;
14 14 use FluentCart\App\Services\DateTime\DateTime;
15 +use FluentCart\App\Services\Payments\PaymentHelper;
15 16 use FluentCart\App\Services\Localization\LocalizationManager;
16 17 use FluentCart\App\Services\Translations\TransStrings;
17 18 use FluentCart\App\Services\URL;
18 19 use FluentCart\Framework\Http\URL as BaseUrl;
@@ -42,8 +43,53 @@
42 43 *
43 44 * @param array $otherInfo A variant's other_info payload
44 45 * @return string|null Error message, or null when valid
45 46 */
47 + /**
48 + * One resolver for every `photo` a FluentCart record appends: the
49 + * uploaded customer photo for the user when one is set, otherwise the
50 + * WordPress avatar by user id or email. Gravatar is asked with
51 + * default=404, so an address with no avatar of its own fails to load
52 + * and the UI falls back to its placeholder icon instead of Gravatar's
53 + * stock image. esc_url_raw throughout — the value travels through
54 + * JSON, where esc_url's entity-encoded ampersands would break the
55 + * query string.
56 + *
57 + * @param int|string|null $userId user id owning a possible uploaded photo
58 + * @param string|null $email fallback identity when there is no user
59 + * @return string the photo URL, or '' when avatars are off or the
60 + * record carries no identity to resolve one from
61 + */
62 + public static function getUserAvatarUrl($userId, $email, int $size = 100): string
63 + {
64 + if (!get_option('show_avatars')) {
65 + return '';
66 + }
67 +
68 + if ($userId) {
69 + $customPhotoUrl = esc_url_raw(
70 + (string) get_user_meta((int) $userId, 'fc_customer_photo_url', true)
71 + );
72 +
73 + if ($customPhotoUrl) {
74 + return $customPhotoUrl;
75 + }
76 + }
77 +
78 + $userIdOrEmail = ((int) $userId) ?: (string) $email;
79 +
80 + if (!$userIdOrEmail) {
81 + return '';
82 + }
83 +
84 + $url = get_avatar_url($userIdOrEmail, [
85 + 'size' => $size,
86 + 'default' => '404',
87 + ]);
88 +
89 + return $url ? esc_url_raw($url) : '';
90 + }
91 +
46 92 public static function installmentTimesError($otherInfo)
47 93 {
48 94 if (Arr::get($otherInfo, 'installment', 'no') !== 'yes') {
49 95 return null;
@@ -278,17 +324,15 @@
278 324 }
279 325
280 326 public static function getOrderStatuses()
281 327 {
282 - $statuses = apply_filters_deprecated('fluent-cart/order_statuses', [
283 - [
284 - 'on-hold' => __('On Hold', 'fluent-cart'),
285 - 'processing' => __('Processing', 'fluent-cart'),
286 - 'completed' => __('Completed', 'fluent-cart'),
287 - //'archived' => __('Archived', 'fluent-cart'),
288 - 'cancelled' => __('Cancelled', 'fluent-cart'),
289 - ], []
290 - ], '1.3.16', 'fluent_cart/order_statuses', 'Use fluent_cart/order_statuses instead of fluent-cart/order_statuses. It will be removed in v1.4.3.');
328 + $statuses = [
329 + 'on-hold' => __('On Hold', 'fluent-cart'),
330 + 'processing' => __('Processing', 'fluent-cart'),
331 + 'completed' => __('Completed', 'fluent-cart'),
332 + //'archived' => __('Archived', 'fluent-cart'),
333 + 'cancelled' => __('Cancelled', 'fluent-cart'),
334 + ];
291 335
292 336 return apply_filters('fluent_cart/order_statuses', $statuses, []);
293 337 }
294 338
@@ -293,17 +337,15 @@
293 337 }
294 338
295 339 public static function getEditableOrderStatuses()
296 340 {
297 - $statuses = apply_filters_deprecated('fluent-cart/editable_order_statuses', [
298 - [
299 - 'on-hold' => __('On Hold', 'fluent-cart'),
300 - 'processing' => __('Processing', 'fluent-cart'),
301 - 'completed' => __('Completed', 'fluent-cart'),
302 - // 'archived' => __('Archived', 'fluent-cart'),
303 - 'cancelled' => __('Cancelled', 'fluent-cart')
304 - ], []
305 - ], '1.3.16', 'fluent_cart/editable_order_statuses', 'Use fluent_cart/editable_order_statuses instead of fluent-cart/editable_order_statuses. It will be removed in v1.4.3.');
341 + $statuses = [
342 + 'on-hold' => __('On Hold', 'fluent-cart'),
343 + 'processing' => __('Processing', 'fluent-cart'),
344 + 'completed' => __('Completed', 'fluent-cart'),
345 + // 'archived' => __('Archived', 'fluent-cart'),
346 + 'cancelled' => __('Cancelled', 'fluent-cart')
347 + ];
306 348
307 349 return apply_filters('fluent_cart/editable_order_statuses', $statuses, []);
308 350 }
309 351
@@ -308,14 +350,12 @@
308 350 }
309 351
310 352 public static function getEditableCustomerStatuses()
311 353 {
312 - $statuses = apply_filters_deprecated('fluent-cart/editable_customer_statuses', [
313 - [
314 - 'active' => __('Active', 'fluent-cart'),
315 - 'inactive' => __('Inactive', 'fluent-cart'),
316 - ], []
317 - ], '1.3.16', 'fluent_cart/editable_customer_statuses', 'Use fluent_cart/editable_customer_statuses instead of fluent-cart/editable_customer_statuses. It will be removed in v1.4.3.');
354 + $statuses = [
355 + 'active' => __('Active', 'fluent-cart'),
356 + 'inactive' => __('Inactive', 'fluent-cart'),
357 + ];
318 358
319 359 return apply_filters('fluent_cart/editable_customer_statuses', $statuses, []);
320 360 }
321 361
@@ -320,16 +360,14 @@
320 360 }
321 361
322 362 public static function getShippingStatuses()
323 363 {
324 - $statuses = apply_filters_deprecated('fluent-cart/shipping_statuses', [
325 - [
326 - 'unshipped' => __('Unshipped', 'fluent-cart'),
327 - 'shipped' => __('Shipped', 'fluent-cart'),
328 - 'delivered' => __('Delivered', 'fluent-cart'),
329 - 'unshippable' => __('Unshippable', 'fluent-cart'),
330 - ], []
331 - ], '1.3.16', 'fluent_cart/shipping_statuses', 'Use fluent_cart/shipping_statuses instead of fluent-cart/shipping_statuses. It will be removed in v1.4.3.');
364 + $statuses = [
365 + 'unshipped' => __('Unshipped', 'fluent-cart'),
366 + 'shipped' => __('Shipped', 'fluent-cart'),
367 + 'delivered' => __('Delivered', 'fluent-cart'),
368 + 'unshippable' => __('Unshippable', 'fluent-cart'),
369 + ];
332 370
333 371 return apply_filters('fluent_cart/shipping_statuses', $statuses, []);
334 372 }
335 373
@@ -334,16 +372,14 @@
334 372 }
335 373
336 374 public static function getEditableShippingStatuses()
337 375 {
338 - $statuses = apply_filters_deprecated('fluent-cart/editable_order_statuses', [
339 - [
340 - 'unshipped' => __('Unshipped', 'fluent-cart'),
341 - 'shipped' => __('Shipped', 'fluent-cart'),
342 - 'delivered' => __('Delivered', 'fluent-cart'),
343 - 'unshippable' => __('Unshippable', 'fluent-cart'),
344 - ], []
345 - ], '1.3.16', 'fluent_cart/editable_shipping_statuses', 'Use fluent_cart/editable_shipping_statuses instead of fluent-cart/editable_order_statuses. It will be removed in v1.4.3.');
376 + $statuses = [
377 + 'unshipped' => __('Unshipped', 'fluent-cart'),
378 + 'shipped' => __('Shipped', 'fluent-cart'),
379 + 'delivered' => __('Delivered', 'fluent-cart'),
380 + 'unshippable' => __('Unshippable', 'fluent-cart'),
381 + ];
346 382
347 383 return apply_filters('fluent_cart/editable_shipping_statuses', $statuses, []);
348 384 }
349 385
@@ -373,18 +409,16 @@
373 409 }
374 410
375 411 public static function getTransactionStatuses($withLabel = true)
376 412 {
377 - $statuses = apply_filters_deprecated('fluent-cart/transaction_statuses', [
378 - [
379 - 'pending' => __('Pending', 'fluent-cart'),
380 - 'paid' => __('Paid', 'fluent-cart'),
381 - 'require_capture' => __('Authorized (Require Capture)', 'fluent-cart'),
382 - 'failed' => __('Failed', 'fluent-cart'),
383 - 'refunded' => __('Refunded', 'fluent-cart'),
384 - 'active' => __('Active', 'fluent-cart'),
385 - ], []
386 - ], '1.3.16', 'fluent_cart/transaction_statuses', 'Use fluent_cart/transaction_statuses instead of fluent-cart/transaction_statuses. It will be removed in v1.4.3.');
413 + $statuses = [
414 + 'pending' => __('Pending', 'fluent-cart'),
415 + 'paid' => __('Paid', 'fluent-cart'),
416 + 'require_capture' => __('Authorized (Require Capture)', 'fluent-cart'),
417 + 'failed' => __('Failed', 'fluent-cart'),
418 + 'refunded' => __('Refunded', 'fluent-cart'),
419 + 'active' => __('Active', 'fluent-cart'),
420 + ];
387 421
388 422 $statuses = apply_filters('fluent_cart/transaction_statuses', $statuses, []);
389 423
390 424 if ($withLabel) {
@@ -395,16 +429,14 @@
395 429 }
396 430
397 431 public static function getEditableTransactionStatuses($withLabel = true)
398 432 {
399 - $statuses = apply_filters_deprecated('fluent-cart/editable_transaction_statuses', [
400 - [
401 - 'pending' => __('Pending', 'fluent-cart'),
402 - 'paid' => __('Paid', 'fluent-cart'),
403 - 'failed' => __('Failed', 'fluent-cart'),
404 - 'refunded' => __('Refunded', 'fluent-cart'),
405 - ], []
406 - ], '1.3.16', 'fluent_cart/editable_transaction_statuses', 'Use fluent_cart/editable_transaction_statuses instead of fluent-cart/editable_transaction_statuses. It will be removed in v1.4.3.');
433 + $statuses = [
434 + 'pending' => __('Pending', 'fluent-cart'),
435 + 'paid' => __('Paid', 'fluent-cart'),
436 + 'failed' => __('Failed', 'fluent-cart'),
437 + 'refunded' => __('Refunded', 'fluent-cart'),
438 + ];
407 439
408 440 $statuses = apply_filters('fluent_cart/editable_transaction_statuses', $statuses, []);
409 441
410 442 if ($withLabel) {
@@ -413,21 +445,8 @@
413 445
414 446 return array_keys($statuses);
415 447 }
416 448
417 - public static function loadSpoutLib()
418 - {
419 - static $loaded;
420 -
421 - if ($loaded) {
422 - return $loaded;
423 - }
424 -
425 - require_once FLUENTCART_PLUGIN_PATH . 'app/Services/Libs/Spout/Autoloader/autoload.php';
426 -
427 - return true;
428 - }
429 -
430 449 public static function productStatuses($withLabel = true): array
431 450 {
432 451 $statues = [
433 452 'publish' => __('Publish', 'fluent-cart'),
@@ -635,10 +654,12 @@
635 654 if (!$user) {
636 655 return null;
637 656 }
638 657
658 + // Identity only — see CustomerResource::getCurrentCustomer() for why an
659 + // email match must never reach a customer row.
639 660 return Customer::query()->where('user_id', $user->ID)
640 - ->orWhere('email', $user->user_email)
661 + ->orderBy('id', 'ASC')
641 662 ->first();
642 663 }
643 664
644 665 /**
@@ -666,27 +687,25 @@
666 687 }
667 688
668 689 public static function getAvailableCurrencyList()
669 690 {
670 - $currencies = apply_filters_deprecated('fluent-cart/available_currencies', [
671 - [
672 - 'BDT' => [
673 - "label" => __('Bangladeshi Taka', 'fluent-cart'),
674 - "value" => 'BDT',
675 - "symbol" => '৳',
676 - ],
677 - 'USD' => [
678 - "label" => __('United State Dollar', 'fluent-cart'),
679 - "value" => 'USD',
680 - "symbol" => '$',
681 - ],
682 - 'GBP' => [
683 - "label" => __('United Kingdom', 'fluent-cart'),
684 - "value" => 'GBP',
685 - "symbol" => '£',
686 - ],
687 - ], []
688 - ], '1.3.16', 'fluent_cart/available_currencies', 'Use fluent_cart/available_currencies instead of fluent-cart/available_currencies. It will be removed in v1.4.3.');
691 + $currencies = [
692 + 'BDT' => [
693 + "label" => __('Bangladeshi Taka', 'fluent-cart'),
694 + "value" => 'BDT',
695 + "symbol" => '৳',
696 + ],
697 + 'USD' => [
698 + "label" => __('United State Dollar', 'fluent-cart'),
699 + "value" => 'USD',
700 + "symbol" => '$',
701 + ],
702 + 'GBP' => [
703 + "label" => __('United Kingdom', 'fluent-cart'),
704 + "value" => 'GBP',
705 + "symbol" => '£',
706 + ],
707 + ];
689 708
690 709 return apply_filters('fluent_cart/available_currencies', $currencies, []);
691 710 }
692 711
@@ -1033,15 +1052,13 @@
1033 1052 }
1034 1053
1035 1054 public static function getCouponStatuses()
1036 1055 {
1037 - $statuses = apply_filters_deprecated('fluent-cart/coupon_statuses', [
1038 - [
1039 - 'active' => __('Active', 'fluent-cart'),
1040 - 'expired' => __('Expired', 'fluent-cart'),
1041 - 'disabled' => __('Disabled', 'fluent-cart'),
1042 - ], []
1043 - ], '1.3.16', 'fluent_cart/coupon_statuses', 'Use fluent_cart/coupon_statuses instead of fluent-cart/coupon_statuses. It will be removed in v1.4.3.');
1056 + $statuses = [
1057 + 'active' => __('Active', 'fluent-cart'),
1058 + 'expired' => __('Expired', 'fluent-cart'),
1059 + 'disabled' => __('Disabled', 'fluent-cart'),
1060 + ];
1044 1061
1045 1062 return apply_filters('fluent_cart/coupon_statuses', $statuses, []);
1046 1063 }
1047 1064
@@ -1307,8 +1324,150 @@
1307 1324
1308 1325 return !empty($otherInfo) ? $paymentInfo : null;
1309 1326 }
1310 1327
1328 + /**
1329 + * Billing-cycle text for a subscription with a config-defined schedule
1330 + * (see SubscriptionHelper::getBillingSchedule()) — cadences the
1331 + * billing_interval enum cannot express, e.g. "$100 per 3 years on Aug 19".
1332 + * Subscriptions without a schedule keep generateSubscriptionInfo().
1333 + */
1334 + public static function generateScheduleSubscriptionInfo(array $schedule, $otherInfo, $itemPrice, $currencyCode = null): ?string
1335 + {
1336 + if (is_object($otherInfo)) {
1337 + $otherInfo = json_decode(json_encode($otherInfo), true);
1338 + }
1339 +
1340 + $count = max(1, (int) Arr::get($schedule, 'interval', 1));
1341 +
1342 + switch (Arr::get($schedule, 'period')) {
1343 + case 'day':
1344 + $unitLabel = _n('day', 'days', $count, 'fluent-cart');
1345 + break;
1346 + case 'week':
1347 + $unitLabel = _n('week', 'weeks', $count, 'fluent-cart');
1348 + break;
1349 + case 'month':
1350 + $unitLabel = _n('month', 'months', $count, 'fluent-cart');
1351 + break;
1352 + case 'year':
1353 + $unitLabel = _n('year', 'years', $count, 'fluent-cart');
1354 + break;
1355 + default:
1356 + return self::generateSubscriptionInfo($otherInfo, $itemPrice, $currencyCode);
1357 + }
1358 +
1359 + $price = self::toDecimal($itemPrice, true, $currencyCode);
1360 + $recurringDiscountAmount = Arr::get($otherInfo, 'recurring_discounts.amount', 0);
1361 +
1362 + if ($recurringDiscountAmount) {
1363 + $newRecurringAmount = $itemPrice - $recurringDiscountAmount;
1364 + $price = "<del>" . $price . "</del> " . self::toDecimal($newRecurringAmount, true, $currencyCode);
1365 + }
1366 +
1367 + $interval = $count === 1
1368 + ? sprintf(
1369 + /* translators: %s is the interval unit (e.g., day, week, month, year) */
1370 + __('per %s', 'fluent-cart'),
1371 + $unitLabel
1372 + )
1373 + : sprintf(
1374 + /* translators: %1$d is the count number, %2$s is the plural unit name (e.g., days, months, years) */
1375 + __('per %1$d %2$s', 'fluent-cart'),
1376 + $count,
1377 + $unitLabel
1378 + );
1379 +
1380 + if ($anchorText = self::getScheduleAnchorText($schedule['period'], Arr::get($schedule, 'anchor', []))) {
1381 + $interval .= ' ' . $anchorText;
1382 + }
1383 +
1384 + $occurrence = (int) Arr::get($otherInfo, 'times', 0);
1385 +
1386 + if (empty($occurrence)) {
1387 + return sprintf(
1388 + /* translators: %1$s is the price, %2$s is the interval, %3$s is "until cancel" text */
1389 + __('%1$s %2$s %3$s', 'fluent-cart'),
1390 + $price,
1391 + $interval,
1392 + __('until cancel', 'fluent-cart')
1393 + );
1394 + }
1395 +
1396 + return sprintf(
1397 + /* translators: %1$s is the price, %2$s is the interval, %3$s is the occurrence count, %4$s is "cycle(s)" */
1398 + __('%1$s %2$s, for %3$s %4$s', 'fluent-cart'),
1399 + $price,
1400 + $interval,
1401 + $occurrence,
1402 + _n('cycle', 'cycles', $occurrence, 'fluent-cart')
1403 + );
1404 + }
1405 +
1406 + /**
1407 + * Human-readable billing anchor, e.g. "on Friday", "on the 10th",
1408 + * "on the last day", "on Aug 19". Anchor day 31 encodes "last day of
1409 + * the month" (see SubscriptionHelper::getBillingSchedule()).
1410 + */
1411 + private static function getScheduleAnchorText(string $period, $anchor): string
1412 + {
1413 + if (!is_array($anchor) || !$anchor) {
1414 + return '';
1415 + }
1416 +
1417 + if ($period === 'week' && !empty($anchor['weekday'])) {
1418 + $weekdays = [
1419 + 1 => __('Monday', 'fluent-cart'),
1420 + 2 => __('Tuesday', 'fluent-cart'),
1421 + 3 => __('Wednesday', 'fluent-cart'),
1422 + 4 => __('Thursday', 'fluent-cart'),
1423 + 5 => __('Friday', 'fluent-cart'),
1424 + 6 => __('Saturday', 'fluent-cart'),
1425 + 7 => __('Sunday', 'fluent-cart'),
1426 + ];
1427 +
1428 + if (isset($weekdays[$anchor['weekday']])) {
1429 + /* translators: %s is a weekday name, e.g. "on Friday" */
1430 + return sprintf(__('on %s', 'fluent-cart'), $weekdays[$anchor['weekday']]);
1431 + }
1432 +
1433 + return '';
1434 + }
1435 +
1436 + if ($period === 'month' && !empty($anchor['day'])) {
1437 + $day = (int) $anchor['day'];
1438 +
1439 + if ($day === 31) {
1440 + return __('on the last day', 'fluent-cart');
1441 + }
1442 +
1443 + /* translators: %s is an ordinal day of month, e.g. "on the 10th" */
1444 + return sprintf(__('on the %s', 'fluent-cart'), gmdate('jS', gmmktime(12, 0, 0, 1, $day, 2001)));
1445 + }
1446 +
1447 + if ($period === 'year' && (!empty($anchor['day']) || !empty($anchor['month']))) {
1448 + $day = (int) Arr::get($anchor, 'day', 0);
1449 + $month = (int) Arr::get($anchor, 'month', 0);
1450 +
1451 + if ($month && $day) {
1452 + /* translators: %s is a date, e.g. "on Aug 19" */
1453 + // wp_date(), not gmdate(): gmdate() has no locale, so this month
1454 + // name stayed English inside an otherwise translated sentence.
1455 + return sprintf(__('on %s', 'fluent-cart'), wp_date('M', gmmktime(12, 0, 0, $month, 1, 2001), new \DateTimeZone('UTC')) . ' ' . $day);
1456 + }
1457 +
1458 + if ($month) {
1459 + /* translators: %s is a month name, e.g. "in August" */
1460 + return sprintf(__('in %s', 'fluent-cart'), wp_date('F', gmmktime(12, 0, 0, $month, 1, 2001), new \DateTimeZone('UTC')));
1461 + }
1462 +
1463 + /* translators: %s is an ordinal day of month, e.g. "on the 10th" */
1464 + return sprintf(__('on the %s', 'fluent-cart'), gmdate('jS', gmmktime(12, 0, 0, 1, $day, 2001)));
1465 + }
1466 +
1467 + return '';
1468 + }
1469 +
1311 1470 public static function generateSetupFeeInfo($otherInfo, $asArray = false)
1312 1471 {
1313 1472 // Convert to array if it's an object
1314 1473 if (is_object($otherInfo)) {
@@ -1373,9 +1532,8 @@
1373 1532
1374 1533 public static function getCountryList(): array
1375 1534 {
1376 1535 $options = App::getInstance('localization')->countriesOptions();
1377 - $options = apply_filters_deprecated('fluent-cart/util/countries', [$options, []], '1.3.16', 'fluent_cart/util/countries', 'Use fluent_cart/util/countries instead of fluent-cart/util/countries. It will be removed in v1.4.3.');
1378 1536
1379 1537 return apply_filters('fluent_cart/util/countries', $options, []);
1380 1538 }
1381 1539
@@ -1815,26 +1973,9 @@
1815 1973 }
1816 1974
1817 1975 public static function subscriptionIntervalInDays($interval)
1818 1976 {
1819 - switch ($interval) {
1820 - case 'daily':
1821 - return 1;
1822 - case 'weekly':
1823 - return 7;
1824 - case 'monthly':
1825 - return 30;
1826 - case 'quarterly':
1827 - return 90;
1828 - case 'half_yearly':
1829 - return 182;
1830 - case 'yearly':
1831 - return 365;
1832 - default:
1833 - return apply_filters('fluent_cart/subscription_interval_in_days', 0, [
1834 - 'interval' => $interval,
1835 - ]);
1836 - }
1977 + return PaymentHelper::getIntervalDays($interval);
1837 1978 }
1838 1979
1839 1980 public static function parseTermIdsForFilter($filters): array
1840 1981 {
@@ -1889,12 +2030,12 @@
1889 2030 {
1890 2031 $allChildVariants = Arr::pluck($variants, 'other_info.bundle_child_ids');
1891 2032
1892 2033 $allChildVariants = array_unique(Arr::flatten($allChildVariants));
1893 - $allChildVariants = Arr::except(
2034 + $allChildVariants = array_values(array_diff(
1894 2035 $allChildVariants,
1895 2036 Arr::pluck($variants, 'id')
1896 - );
2037 + ));
1897 2038 $allChildVariants = array_filter($allChildVariants);
1898 2039 $childVariants = ProductVariation::query()
1899 2040 ->whereIn('id', $allChildVariants)
1900 2041 ->with('product:ID,post_title')
@@ -2015,6 +2156,40 @@
2015 2156 }
2016 2157 return (int) $order->tax_behavior === 2
2017 2158 ? esc_html__('(Included)', 'fluent-cart')
2018 2159 : esc_html__('(Excluded)', 'fluent-cart');
2160 + }
2161 +
2162 + /**
2163 + * Product placeholder illustration (an open box holding a picture) shown
2164 + * wherever a product has no image. The web copy is a 1200px WebP (sharp on 2x screens); email
2165 + * clients (Outlook) and PDF renderers get a 300px PNG of the same art.
2166 + */
2167 + const PRODUCT_PLACEHOLDER_IMAGE = 'images/product-placeholder.webp';
2168 + const PRODUCT_PLACEHOLDER_EMAIL_IMAGE = 'images/product-placeholder.png';
2169 +
2170 + public static function getProductPlaceholderUrl(): string
2171 + {
2172 + return \FluentCart\App\Vite::getAssetUrl(static::PRODUCT_PLACEHOLDER_IMAGE);
2173 + }
2174 +
2175 + public static function getProductPlaceholderEmailUrl(): string
2176 + {
2177 + return \FluentCart\App\Vite::getAssetUrl(static::PRODUCT_PLACEHOLDER_EMAIL_IMAGE);
2178 + }
2179 +
2180 + /**
2181 + * A product image URL, or the placeholder when there is none. Carts store
2182 + * the thumbnail URL in cart_data, so carts built before the illustration
2183 + * still carry the old generic icon (images/placeholder.svg); map that too.
2184 + */
2185 + public static function productImageOrPlaceholder($url): string
2186 + {
2187 + $url = is_string($url) ? $url : '';
2188 +
2189 + if ($url === '' || preg_match('#/images/placeholder\.svg(\?.*)?$#', $url)) {
2190 + return static::getProductPlaceholderUrl();
2191 + }
2192 +
2193 + return $url;
2019 2194 }
2020 2195 }