ListCoupons.php
127 lines
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Automattic\WooCommerce\Api\Queries\Coupons; |
| 6 | |
| 7 | use Automattic\WooCommerce\Api\Attributes\ConnectionOf; |
| 8 | use Automattic\WooCommerce\Api\Attributes\Description; |
| 9 | use Automattic\WooCommerce\Api\Attributes\Name; |
| 10 | use Automattic\WooCommerce\Api\Attributes\RequiredCapability; |
| 11 | use Automattic\WooCommerce\Api\Enums\Coupons\CouponStatus; |
| 12 | use Automattic\WooCommerce\Api\Pagination\Connection; |
| 13 | use Automattic\WooCommerce\Api\Pagination\Edge; |
| 14 | use Automattic\WooCommerce\Api\Pagination\IdCursorFilter; |
| 15 | use Automattic\WooCommerce\Api\Pagination\PageInfo; |
| 16 | use Automattic\WooCommerce\Api\Pagination\PaginationParams; |
| 17 | use Automattic\WooCommerce\Api\Types\Coupons\Coupon; |
| 18 | use Automattic\WooCommerce\Api\Utils\Coupons\CouponMapper; |
| 19 | |
| 20 | #[Name( 'coupons' )] |
| 21 | #[Description( 'List coupons with cursor-based pagination.' )] |
| 22 | /** |
| 23 | * Query to list coupons with cursor-based pagination. |
| 24 | */ |
| 25 | #[RequiredCapability( 'read_private_shop_coupons' )] |
| 26 | class ListCoupons { |
| 27 | /** |
| 28 | * List coupons with optional filtering and pagination. |
| 29 | * |
| 30 | * @param PaginationParams $pagination The pagination parameters. |
| 31 | * @param ?CouponStatus $status Optional status filter. |
| 32 | * @return Connection |
| 33 | */ |
| 34 | #[ConnectionOf( Coupon::class )] |
| 35 | public function execute( |
| 36 | PaginationParams $pagination, |
| 37 | #[Description( 'Filter by coupon status.' )] |
| 38 | ?CouponStatus $status = null, |
| 39 | ): Connection { |
| 40 | $first = $pagination->first; |
| 41 | $last = $pagination->last; |
| 42 | $after = $pagination->after; |
| 43 | $before = $pagination->before; |
| 44 | $limit = $first ?? $last ?? PaginationParams::get_default_page_size(); |
| 45 | |
| 46 | // Use WP_Query for the count and a filtered query for cursor-based |
| 47 | // pagination. We only need `found_posts` (which comes from the |
| 48 | // SQL_CALC_FOUND_ROWS query WP runs alongside the main SELECT), so |
| 49 | // the main SELECT fetches only one row — posts_per_page => -1 would |
| 50 | // materialize every ID just to throw it away. |
| 51 | $count_args = array( |
| 52 | 'post_type' => 'shop_coupon', |
| 53 | 'posts_per_page' => 1, |
| 54 | 'fields' => 'ids', |
| 55 | 'post_status' => $status?->value ?? 'any', |
| 56 | ); |
| 57 | $count_query = new \WP_Query( $count_args ); |
| 58 | $total_count = $count_query->found_posts; |
| 59 | |
| 60 | // Fetch posts with cursor filtering via post__in or meta_query workaround. |
| 61 | // For simplicity, we use direct ID-based filtering. |
| 62 | $posts_query_args = array( |
| 63 | 'post_type' => 'shop_coupon', |
| 64 | 'posts_per_page' => $limit + 1, |
| 65 | 'orderby' => 'ID', |
| 66 | 'order' => null !== $last ? 'DESC' : 'ASC', |
| 67 | 'post_status' => $status?->value ?? 'any', |
| 68 | ); |
| 69 | |
| 70 | if ( null !== $after ) { |
| 71 | $posts_query_args[ IdCursorFilter::AFTER_ID ] = IdCursorFilter::decode_id_cursor( $after, 'after' ); |
| 72 | } |
| 73 | if ( null !== $before ) { |
| 74 | $posts_query_args[ IdCursorFilter::BEFORE_ID ] = IdCursorFilter::decode_id_cursor( $before, 'before' ); |
| 75 | } |
| 76 | IdCursorFilter::ensure_registered(); |
| 77 | |
| 78 | $query = new \WP_Query( $posts_query_args ); |
| 79 | $posts = $query->posts; |
| 80 | |
| 81 | // Determine pagination. |
| 82 | $has_extra = count( $posts ) > $limit; |
| 83 | if ( $has_extra ) { |
| 84 | $posts = array_slice( $posts, 0, $limit ); |
| 85 | } |
| 86 | |
| 87 | // If we fetched in DESC order for $last, reverse to get ascending order. |
| 88 | if ( null !== $last ) { |
| 89 | $posts = array_reverse( $posts ); |
| 90 | } |
| 91 | |
| 92 | // Build edges and nodes. |
| 93 | $edges = array(); |
| 94 | $nodes = array(); |
| 95 | foreach ( $posts as $post ) { |
| 96 | $wc_coupon = new \WC_Coupon( $post->ID ); |
| 97 | $coupon = CouponMapper::from_wc_coupon( $wc_coupon ); |
| 98 | |
| 99 | $edge = new Edge(); |
| 100 | $edge->cursor = base64_encode( (string) $coupon->id ); |
| 101 | $edge->node = $coupon; |
| 102 | |
| 103 | $edges[] = $edge; |
| 104 | $nodes[] = $coupon; |
| 105 | } |
| 106 | |
| 107 | $page_info = new PageInfo(); |
| 108 | // Relay semantics for backward pagination (`last`, `before`): the |
| 109 | // returned window ends just before `$before`, so items after the |
| 110 | // window exist whenever `$before` was supplied — not whenever |
| 111 | // `$after` was. `has_previous_page` in the backward case is driven |
| 112 | // by the "did we fetch limit+1?" sentinel (`$has_extra`). |
| 113 | $page_info->has_next_page = null !== $last ? ( null !== $before ) : $has_extra; |
| 114 | $page_info->has_previous_page = null !== $last ? $has_extra : ( null !== $after ); |
| 115 | $page_info->start_cursor = ! empty( $edges ) ? $edges[0]->cursor : null; |
| 116 | $page_info->end_cursor = ! empty( $edges ) ? $edges[ count( $edges ) - 1 ]->cursor : null; |
| 117 | |
| 118 | $connection = new Connection(); |
| 119 | $connection->edges = $edges; |
| 120 | $connection->nodes = $nodes; |
| 121 | $connection->page_info = $page_info; |
| 122 | $connection->total_count = $total_count; |
| 123 | |
| 124 | return $connection; |
| 125 | } |
| 126 | } |
| 127 |