| 1 |
<?php |
| 2 |
|
| 3 |
namespace FluentCart\App\Services\ShortCodeParser\Parsers; |
| 4 |
|
| 5 |
use FluentCart\App\Models\ProductReview; |
| 6 |
use FluentCart\App\Services\ProductReviewService; |
| 7 |
use FluentCart\Framework\Support\Arr; |
| 8 |
|
| 9 |
/** |
| 10 |
* {{review.*}} — the review a notification is about. |
| 11 |
* |
| 12 |
* The other parsers all reach their subject through an order. A review has no |
| 13 |
* order to reach through: it can be written before one, after one, or without |
| 14 |
* one at all, so its own row is the only thing the email may read from. That |
| 15 |
* is also why review notifications declare their own `to` — the built-in |
| 16 |
* customer recipient resolves to {{order.customer.email}}, which is nothing |
| 17 |
* here — and why `recipient_email` goes through the same resolver every |
| 18 |
* review notification uses, so no two emails can disagree about who wrote it. |
| 19 |
* |
| 20 |
* Handed either the model or its array form: the mailer passes models through |
| 21 |
* to the template parser, but a stored custom body can be parsed against |
| 22 |
* whatever a caller serialised. |
| 23 |
*/ |
| 24 |
class ReviewParser extends BaseParser |
| 25 |
{ |
| 26 |
/** @var ProductReview|array|null */ |
| 27 |
private $review; |
| 28 |
|
| 29 |
/** @var mixed */ |
| 30 |
private $product; |
| 31 |
|
| 32 |
/** @var ProductReview|array|null the store's reply, when the email is about one */ |
| 33 |
private $reply; |
| 34 |
|
| 35 |
public function __construct($data) |
| 36 |
{ |
| 37 |
$this->review = Arr::get($data, 'review'); |
| 38 |
$this->product = Arr::get($data, 'product'); |
| 39 |
$this->reply = Arr::get($data, 'reply'); |
| 40 |
parent::__construct($data); |
| 41 |
} |
| 42 |
|
| 43 |
public function parse($accessor = null, $code = null, $transformer = null): ?string |
| 44 |
{ |
| 45 |
$review = $this->review; |
| 46 |
|
| 47 |
if (!$review) { |
| 48 |
return ''; |
| 49 |
} |
| 50 |
|
| 51 |
switch ($accessor) { |
| 52 |
case 'reviewer_name': |
| 53 |
return esc_html($this->attributeOf($review, 'reviewer_name')); |
| 54 |
case 'reviewer_email': |
| 55 |
return esc_html($this->attributeOf($review, 'reviewer_email')); |
| 56 |
case 'recipient_email': |
| 57 |
// Raw, not escaped: this is an address for the To header. A |
| 58 |
// model goes through the resolver, which refuses anything |
| 59 |
// is_email() would; an array form gets the same refusal. |
| 60 |
if ($review instanceof ProductReview) { |
| 61 |
return ProductReviewService::resolveNotificationRecipient($review); |
| 62 |
} |
| 63 |
$address = trim($this->attributeOf($review, 'reviewer_email')); |
| 64 |
return $address !== '' && is_email($address) ? $address : ''; |
| 65 |
case 'title': |
| 66 |
return esc_html($this->attributeOf($review, 'title')); |
| 67 |
case 'content': |
| 68 |
// `review` is the column and is hidden from the array form; |
| 69 |
// `content` is the accessor and is appended to it. Read the |
| 70 |
// one that is present in both shapes. |
| 71 |
return wp_kses_post($this->attributeOf($review, 'content')); |
| 72 |
case 'rating': |
| 73 |
return (string) (int) $this->attributeOf($review, 'rating'); |
| 74 |
case 'rating_stars': |
| 75 |
return $this->ratingStars((int) $this->attributeOf($review, 'rating')); |
| 76 |
case 'status': |
| 77 |
return esc_html($this->attributeOf($review, 'status')); |
| 78 |
case 'product_title': |
| 79 |
return esc_html($this->productTitle()); |
| 80 |
case 'product_url': |
| 81 |
return esc_url($this->productUrl()); |
| 82 |
case 'reply_content': |
| 83 |
return $this->reply ? wp_kses_post($this->attributeOf($this->reply, 'content')) : ''; |
| 84 |
case 'replier_name': |
| 85 |
return $this->reply ? esc_html($this->attributeOf($this->reply, 'reviewer_name')) : ''; |
| 86 |
default: |
| 87 |
// Unknown accessor: null hands the code to the fallback filter |
| 88 |
// rather than printing an empty string over a typo. |
| 89 |
return null; |
| 90 |
} |
| 91 |
} |
| 92 |
|
| 93 |
/** |
| 94 |
* One field, off a model or an array. |
| 95 |
* |
| 96 |
* @param ProductReview|array $source |
| 97 |
* @param string $key |
| 98 |
* @return string |
| 99 |
*/ |
| 100 |
private function attributeOf($source, string $key): string |
| 101 |
{ |
| 102 |
if (is_object($source)) { |
| 103 |
return (string) ($source->{$key} ?? ''); |
| 104 |
} |
| 105 |
|
| 106 |
return (string) Arr::get((array) $source, $key, ''); |
| 107 |
} |
| 108 |
|
| 109 |
private function productTitle(): string |
| 110 |
{ |
| 111 |
$title = $this->attributeOf($this->product ?: [], 'post_title'); |
| 112 |
|
| 113 |
if ($title === '') { |
| 114 |
$postId = (int) $this->attributeOf($this->review, 'post_id'); |
| 115 |
$title = $postId ? (string) get_post_field('post_title', $postId, 'raw') : ''; |
| 116 |
} |
| 117 |
|
| 118 |
return $title; |
| 119 |
} |
| 120 |
|
| 121 |
private function productUrl(): string |
| 122 |
{ |
| 123 |
$postId = (int) ($this->attributeOf($this->product ?: [], 'ID') ?: $this->attributeOf($this->review, 'post_id')); |
| 124 |
|
| 125 |
return $postId ? (string) get_permalink($postId) : ''; |
| 126 |
} |
| 127 |
|
| 128 |
/** |
| 129 |
* Five stars, the given number filled. Plain text entities rather than an |
| 130 |
* image, so it survives every mail client's image blocking. |
| 131 |
*/ |
| 132 |
private function ratingStars(int $rating): string |
| 133 |
{ |
| 134 |
$rating = max(0, min(5, $rating)); |
| 135 |
|
| 136 |
return str_repeat('★', $rating) . str_repeat('☆', 5 - $rating); |
| 137 |
} |
| 138 |
} |
| 139 |
|