Preview: ItemEligibility.php
Size: 9.64 KB
/home/nshryvcy/radiantskinclinics.org/wp-content/plugins/woocommerce/src/Internal/OrderReviews/ItemEligibility.php
<?php
/**
* ItemEligibility class file.
*/
declare( strict_types = 1 );
namespace Automattic\WooCommerce\Internal\OrderReviews;
use WC_Order;
use WC_Order_Item;
use WC_Order_Item_Product;
use WP_Comment;
/**
* Decides how each Review Order line item should be rendered and supplies
* any pre-fill data for the row form.
*
* Two outcomes for a row:
*
* - `form` — render the editable form row (`customer-review-order-row.php`),
* optionally pre-filled with the rating + text the customer has already
* submitted for this product **on this order**.
* - `skip` — render nothing (e.g. the product has reviews disabled).
*
* Reviews left for a *different* order are not surfaced here: a customer who
* buys the same product again gets a fresh form row, because their experience
* the second time around may be different from the first.
*
* @internal Just for internal use.
*
* @since 10.8.0
*/
class ItemEligibility {
/**
* Render the editable form row.
*
* @since 10.8.0
*/
public const STATUS_FORM = 'form';
/**
* Render nothing (e.g. comments closed on the product).
*
* @since 10.8.0
*/
public const STATUS_SKIP = 'skip';
/**
* Commentmeta key storing the order this review was submitted for.
*
* @since 10.8.0
*/
public const ORDER_META_KEY = '_review_order_id';
/**
* Per-request cache for the "did this email review this product on this
* order" lookup, keyed by `order_id|product_id|email`. Value is a
* `WP_Comment` when one matches, or `null` when the slot has been checked
* and nothing matches (so a second call doesn't re-query).
*
* @var array<string, ?WP_Comment>
*/
private static array $review_cache = array();
/**
* Set of `order_id|email` pairs that have already been bulk-preloaded in
* this request, so a repeated `preload_for_items()` call (e.g. once from
* the Endpoint and once from the page template) doesn't re-run the query.
*
* @var array<string, true>
*/
private static array $preloaded = array();
/**
* Register the default filter callbacks the OrderReviews feature ships with.
*
* Auto-called by the WC dependency container after instantiation.
*
* @internal
*/
final public function init(): void {
add_filter(
'woocommerce_review_order_eligible_items',
array( self::class, 'exclude_fully_refunded_items' ),
10,
2
);
}
/**
* Pre-fill the per-request review cache for a set of items in one query.
*
* Call this from the template before iterating items so each subsequent
* `decide()` / `prefill_for_item()` call hits the cache instead of running
* its own `get_comments()` query.
*
* @since 10.8.0
*
* @param iterable<WC_Order_Item_Product|mixed> $items Order line items.
* @param WC_Order $order Order being reviewed.
*/
public static function preload_for_items( iterable $items, WC_Order $order ): void {
$email = $order->get_billing_email();
$order_id = $order->get_id();
if ( '' === $email || $order_id <= 0 ) {
return;
}
$preload_key = $order_id . '|' . $email;
if ( isset( self::$preloaded[ $preload_key ] ) ) {
return;
}
$product_ids = array();
foreach ( $items as $item ) {
if ( $item instanceof WC_Order_Item_Product ) {
$pid = (int) $item->get_product_id();
if ( $pid > 0 ) {
$product_ids[ $pid ] = $pid;
}
}
}
if ( empty( $product_ids ) ) {
return;
}
self::$preloaded[ $preload_key ] = true;
// Scope to this order's reviews only: a customer who buys the same
// product on a later order shouldn't see their old review here.
$comments = get_comments(
array(
'post__in' => array_values( $product_ids ),
'author_email' => $email,
'type' => 'review',
'status' => 'approve',
'include_unapproved' => array( $email ),
'orderby' => 'comment_date_gmt',
'order' => 'DESC',
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- bounded by post__in + author_email.
array(
'key' => self::ORDER_META_KEY,
'value' => (string) $order_id,
),
),
)
);
// Default every product id to null so subsequent reads don't re-query.
foreach ( $product_ids as $pid ) {
self::$review_cache[ self::cache_key( $order_id, $pid, $email ) ] = null;
}
if ( is_array( $comments ) ) {
foreach ( $comments as $comment ) {
if ( ! $comment instanceof WP_Comment ) {
continue;
}
$key = self::cache_key( $order_id, (int) $comment->comment_post_ID, $email );
if ( null === ( self::$review_cache[ $key ] ?? null ) ) {
self::$review_cache[ $key ] = $comment;
}
}
}
}
/**
* Reset the per-request cache. Test helper.
*
* @since 10.8.0
* @internal
*/
public static function reset_cache(): void {
self::$review_cache = array();
self::$preloaded = array();
}
/**
* Decide how an order line item should render on the Review Order page.
*
* Returns one of the STATUS_* constants plus the matched comment (when
* one exists for this order) and the product id.
*
* @since 10.8.0
*
* @param WC_Order_Item_Product $item Order line item.
* @param WC_Order $order Order being reviewed.
* @return array{status:string, comment:?WP_Comment, product_id:int}
*/
public static function decide( WC_Order_Item_Product $item, WC_Order $order ): array {
$product_id = (int) $item->get_product_id();
$result = array(
'status' => self::STATUS_FORM,
'comment' => null,
'product_id' => $product_id,
);
if ( $product_id <= 0 || ! comments_open( $product_id ) ) {
$result['status'] = self::STATUS_SKIP;
return $result;
}
$result['comment'] = self::find_existing_review( $product_id, $order );
return $result;
}
/**
* Pre-fill payload for a line item: rating, text, and comment id.
*
* Returns zero/empty values when no review exists for this order's row,
* so callers can use it unconditionally.
*
* @since 10.8.0
*
* @param WC_Order_Item_Product $item Order line item.
* @param WC_Order $order Order being reviewed.
* @return array{rating:int, text:string, comment_id:int}
*/
public static function prefill_for_item( WC_Order_Item_Product $item, WC_Order $order ): array {
$existing = self::find_existing_review( (int) $item->get_product_id(), $order );
if ( ! $existing instanceof WP_Comment ) {
return array(
'rating' => 0,
'text' => '',
'comment_id' => 0,
);
}
$rating = (int) get_comment_meta( (int) $existing->comment_ID, 'rating', true );
if ( $rating < 0 || $rating > 5 ) {
$rating = 0;
}
return array(
'rating' => $rating,
'text' => (string) $existing->comment_content,
'comment_id' => (int) $existing->comment_ID,
);
}
/**
* Drop fully-refunded line items from the eligible-items list.
*
* Default callback wired onto `woocommerce_review_order_eligible_items`
* so the page never shows a row for a product the customer no longer
* owns. A line item is considered fully refunded when the absolute
* refunded quantity is greater than or equal to the item's ordered
* quantity. Fractional quantities are honoured.
*
* @since 10.8.0
*
* @param WC_Order_Item[] $items Order line items.
* @param WC_Order $order Order being reviewed.
* @return WC_Order_Item[]
*/
public static function exclude_fully_refunded_items( array $items, WC_Order $order ): array {
$filtered = array();
foreach ( $items as $key => $item ) {
if ( ! $item instanceof WC_Order_Item_Product ) {
$filtered[ $key ] = $item;
continue;
}
$refunded_qty = (float) abs( (float) $order->get_qty_refunded_for_item( $item->get_id() ) );
$ordered_qty = (float) $item->get_quantity();
if ( $ordered_qty > 0 && $refunded_qty >= $ordered_qty ) {
continue;
}
$filtered[ $key ] = $item;
}
return $filtered;
}
/**
* Look up the customer's review for a product on this order.
*
* @since 10.8.0
*
* @param int $product_id Product id.
* @param WC_Order $order Order being reviewed.
* @return WP_Comment|null
*/
private static function find_existing_review( int $product_id, WC_Order $order ): ?WP_Comment {
$email = $order->get_billing_email();
$order_id = (int) $order->get_id();
if ( '' === $email || $order_id <= 0 || $product_id <= 0 ) {
return null;
}
$key = self::cache_key( $order_id, $product_id, $email );
if ( array_key_exists( $key, self::$review_cache ) ) {
return self::$review_cache[ $key ];
}
$comments = get_comments(
array(
'post_id' => $product_id,
'author_email' => $email,
'type' => 'review',
'status' => 'approve',
'include_unapproved' => array( $email ),
'number' => 1,
'orderby' => 'comment_date_gmt',
'order' => 'DESC',
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- bounded by post_id + author_email.
array(
'key' => self::ORDER_META_KEY,
'value' => (string) $order_id,
),
),
)
);
if ( ! is_array( $comments ) || empty( $comments ) ) {
self::$review_cache[ $key ] = null;
return null;
}
$first = reset( $comments );
$found = $first instanceof WP_Comment ? $first : null;
self::$review_cache[ $key ] = $found;
return $found;
}
/**
* Build the per-request cache key.
*
* @param int $order_id Order id.
* @param int $product_id Product id.
* @param string $email Customer email.
*/
private static function cache_key( int $order_id, int $product_id, string $email ): string {
return $order_id . '|' . $product_id . '|' . $email;
}
}
Directory Contents
Dirs: 0 × Files: 6