• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wp-graphql / wp-graphql-woocommerce / 10274605810

06 Aug 2024 10:07PM UTC coverage: 84.595% (-0.5%) from 85.048%
10274605810

push

github

web-flow
fix: product connection resolution refactored to better work with the ProductQuery class (#880)

* fix: product connection resolution refactored to better work with the ProductQuery class

* chore: Linter and PHPStan compliance met

* devops: New product connection tests implemented and passing

* fix: products connection pricing filters fixed

* devops: CartTransactionQueueCest skipped until failing PHP version removed from CI matrix

69 of 81 new or added lines in 12 files covered. (85.19%)

138 existing lines in 7 files now uncovered.

12416 of 14677 relevant lines covered (84.59%)

71.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

29.21
/includes/utils/class-session-transaction-manager.php
1
<?php
2
/**
3
 * Manages concurrent requests that executes mutations on the session data.
4
 *
5
 * @package WPGraphQL\WooCommerce\Utils
6
 * @since 0.7.1
7
 */
8

9
namespace WPGraphQL\WooCommerce\Utils;
10

11
use GraphQL\Error\UserError;
12

13
/**
14
 * Class - Session_Transaction_Manager
15
 */
16
class Session_Transaction_Manager {
17
        /**
18
         * The request's transaction ID.
19
         *
20
         * @var null|string
21
         */
22
        public $transaction_id = null;
23

24
        /**
25
         * Instance of parent session handler
26
         *
27
         * @var \WPGraphQL\WooCommerce\Utils\QL_Session_Handler
28
         */
29
        private $session_handler = null;
30

31
        /**
32
         * Singleton instance of class.
33
         *
34
         * @var \WPGraphQL\WooCommerce\Utils\Session_Transaction_Manager
35
         */
36
        private static $instance = null;
37

38
        /**
39
         * Singleton retriever and cleaner.
40
         * Should not be called anywhere but in the session handler init function.
41
         *
42
         * @param \WPGraphQL\WooCommerce\Utils\QL_Session_Handler $session_handler  WooCommerce Session Handler instance.
43
         *
44
         * @return \WPGraphQL\WooCommerce\Utils\Session_Transaction_Manager
45
         */
46
        public static function get( &$session_handler ) {
47
                if ( is_null( self::$instance ) ) {
47✔
48
                        self::$instance = new self( $session_handler );
1✔
49
                }
50

51
                return self::$instance;
47✔
52
        }
53

54
        /**
55
         * Session_Transaction_Manager constructor
56
         *
57
         * @param \WPGraphQL\WooCommerce\Utils\QL_Session_Handler $session_handler  Reference back to session handler.
58
         */
59
        public function __construct( &$session_handler ) {
60
                $this->session_handler = $session_handler;
1✔
61

62
                add_action( 'graphql_before_resolve_field', [ $this, 'update_transaction_queue' ], 10, 4 );
1✔
63
                add_action( 'graphql_mutation_response', [ $this, 'pop_transaction_id' ], 20, 6 );
1✔
64
                add_action( 'woographql_session_transaction_complete', [ $this->session_handler, 'save_if_dirty' ], 10 );
1✔
65
        }
66

67
        /**
68
         * Pass all member call upstream to the session handler.
69
         *
70
         * @param string $name  Name of class member.
71
         *
72
         * @return mixed
73
         */
74
        public function __get( $name ) {
75
                return $this->session_handler->{$name};
×
76
        }
77

78
        /**
79
         * Return array of all mutations that alter the session data.
80
         * a.k.a. Session Mutations
81
         *
82
         * @return array
83
         */
84
        public static function get_session_mutations() {
85
                /**
86
                 * All session altering mutations should be passed to the array.
87
                 */
88
                return \apply_filters(
1✔
89
                        'woographql_session_mutations',
1✔
90
                        [
1✔
91
                                'addToCart',
1✔
92
                                'updateItemQuantities',
1✔
93
                                'addFee',
1✔
94
                                'applyCoupon',
1✔
95
                                'removeCoupons',
1✔
96
                                'emptyCart',
1✔
97
                                'removeItemsFromCart',
1✔
98
                                'restoreCartItems',
1✔
99
                                'updateItemQuantities',
1✔
100
                                'updateShippingMethod',
1✔
101
                                'updateCustomer',
1✔
102
                                'updateSession',
1✔
103
                        ]
1✔
104
                );
1✔
105
        }
106

107
        /**
108
         * Transaction queue workhorse.
109
         *
110
         * Creates an transaction ID if executing mutations that alter the session data, and stales
111
         * execution until the transaction ID is at the top of the queue.
112
         *
113
         * @param mixed                                $source   Operation root object.
114
         * @param array                                $args     Operation arguments.
115
         * @param \WPGraphQL\AppContext                $context  AppContext instance.
116
         * @param \GraphQL\Type\Definition\ResolveInfo $info     Operation ResolveInfo object.
117
         *
118
         * @return void
119
         */
120
        public function update_transaction_queue( $source, $args, $context, $info ) {
121
                // Bail early, if not one of the session mutations.
122
                if ( ! in_array( $info->fieldName, self::get_session_mutations(), true ) ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
1✔
123
                        return;
1✔
124
                }
125

126
                // Bail if transaction has already been completed. There are times when the underlying action runs twice.
127
                if ( ! is_null( $this->transaction_id ) ) {
×
128
                        $transaction_queue = get_transient( "woo_session_transactions_queue_{$this->session_handler->get_customer_id()}" );
×
129
                        if ( in_array( $this->transaction_id, array_column( $transaction_queue, 'transaction_id' ), true ) ) {
×
130
                                return;
×
131
                        }
132
                } else {
133
                        // Initialize transaction ID.
134
                        $mutation             = $info->fieldName; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
×
135
                        $this->transaction_id = \uniqid( "wooSession_{$mutation}_" );
×
136
                }
137

138
                // Wait until our transaction ID is at the top of the queue before continuing.
139
                if ( ! $this->next_transaction() ) {
×
140
                        usleep( 500000 );
×
141
                        $this->update_transaction_queue( $source, $args, $context, $info );
×
142
                } else {
143
                        $this->session_handler->reload_data();
×
144

145
                        // Set a timestamp on the transaction, which will allow us to check for any stale transactions that accidentally get left behind.
146
                        $this->set_timestamp();
×
147
                }
148
        }
149

150
        /**
151
         * Processes next transaction and returns whether the current transaction is the next transaction.
152
         *
153
         * @return bool
154
         */
155
        public function next_transaction() {
156
                // Update transaction queue.
157
                $transaction_queue = $this->get_transaction_queue();
×
158

159
                // If lead transaction object invalid pop transaction and loop.
160
                if ( ! is_array( $transaction_queue[0] ) ) {
×
161
                        array_shift( $transaction_queue );
×
162
                        $this->save_transaction_queue( $transaction_queue );
×
163

164
                        // If current transaction is the lead exit loop.
165
                } elseif ( $this->transaction_id === $transaction_queue[0]['transaction_id'] ) {
×
166
                        return true;
×
167
                } elseif ( true === $this->did_transaction_expire( $transaction_queue ) ) {
×
168
                        // If transaction has expired, remove it from the queue array and continue loop.
169
                        array_shift( $transaction_queue );
×
170
                        $this->save_transaction_queue( $transaction_queue );
×
171
                }
172

173
                return false;
×
174
        }
175

176
        /**
177
         * Adds transaction ID to the end of the queue, officially starting the transaction,
178
         * and returns the transaction queue.
179
         *
180
         * @return array
181
         */
182
        public function get_transaction_queue() {
183
                // Get transaction queue.
184
                $transaction_queue = get_transient( "woo_session_transactions_queue_{$this->session_handler->get_customer_id()}" );
×
185
                if ( ! $transaction_queue ) {
×
186
                        $transaction_queue = [];
×
187
                }
188

189
                // If transaction ID not in queue, add it, and start transaction.
190
                if ( ! in_array( $this->transaction_id, array_column( $transaction_queue, 'transaction_id' ), true ) ) {
×
191
                        $transaction_id = $this->transaction_id;
×
192
                        $snapshot       = $this->session_handler->get_session_data();
×
193

194
                        $transaction_queue[] = compact( 'transaction_id', 'snapshot' );
×
195

196
                        // Update queue.
197
                        $this->save_transaction_queue( $transaction_queue );
×
198
                }
199

200
                return $transaction_queue;
×
201
        }
202

203
        /**
204
         * Pop transaction ID off the top of the queue, ending the transaction.
205
         *
206
         * @param array                                $payload          The Payload returned from the mutation.
207
         * @param array                                $input            The mutation input args, after being filtered by 'graphql_mutation_input'.
208
         * @param array                                $unfiltered_input The unfiltered input args of the mutation
209
         * @param \WPGraphQL\AppContext                $context          The AppContext object.
210
         * @param \GraphQL\Type\Definition\ResolveInfo $info             The ResolveInfo object.
211
         * @param string                               $mutation         The name of the mutation field.
212
         *
213
         * @throws \GraphQL\Error\UserError If transaction ID is not on the top of the queue.
214
         *
215
         * @return void
216
         */
217
        public function pop_transaction_id( $payload, $input, $unfiltered_input, $context, $info, $mutation ) {
218
                // Bail if transaction not started.
219
                if ( is_null( $this->transaction_id ) ) {
×
220
                        return;
×
221
                }
222

223
                // Bail if not the expected mutation.
224
                if ( str_starts_with( $this->transaction_id, "wooSession_{$mutation}_" ) ) {
×
225
                        return;
×
226
                }
227

228
                // Get transaction queue.
229
                $transaction_queue = get_transient( "woo_session_transactions_queue_{$this->session_handler->get_customer_id()}" );
×
230

231
                // Throw if transaction ID not on top.
232
                if ( $this->transaction_id !== $transaction_queue[0]['transaction_id'] ) {
×
233
                        $this->save_transaction_queue( [] );
×
UNCOV
234
                        $this->transaction_id = null;
×
UNCOV
235
                        throw new UserError( __( 'Woo session transaction executed out of order', 'wp-graphql-woocommerce' ) );
×
236
                } else {
237

238
                        // Remove Transaction ID and update queue.
UNCOV
239
                        array_shift( $transaction_queue );
×
UNCOV
240
                        $this->save_transaction_queue( $transaction_queue );
×
241

242
                        /**
243
                         * Mark transaction completion
244
                         *
245
                         * @param string|null $transition_id     Removed transaction ID.
246
                         * @param array       $transaction_queue Transaction Queue.
247
                         */
UNCOV
248
                        do_action( 'woographql_session_transaction_complete', $this->transaction_id, $transaction_queue );
×
249

250
                        // Clear transaction ID.
UNCOV
251
                        $this->transaction_id = null;
×
252
                }
253
        }
254

255
        /**
256
         * Saves transaction queue.
257
         *
258
         * @param array $queue  Transaction queue.
259
         *
260
         * @return void
261
         */
262
        public function save_transaction_queue( $queue = [] ) {
263
                // If queue empty delete transient and bail.
264
                if ( empty( $queue ) ) {
×
UNCOV
265
                        delete_transient( "woo_session_transactions_queue_{$this->session_handler->get_customer_id()}" );
×
UNCOV
266
                        return;
×
267
                }
268

269
                // Save transaction queue.
UNCOV
270
                set_transient( "woo_session_transactions_queue_{$this->session_handler->get_customer_id()}", $queue, 5 * MINUTE_IN_SECONDS );
×
271
        }
272

273
        /**
274
         * Create transaction timestamp.
275
         *
276
         * @return void
277
         */
278
        public function set_timestamp() {
UNCOV
279
                $transaction_queue = $this->get_transaction_queue();
×
280

281
                // Bail if we don't have a queue to add a timestamp against.
UNCOV
282
                if ( empty( $transaction_queue[0] ) ) {
×
UNCOV
283
                        return;
×
284
                }
285

286
                $transaction_queue[0]['timestamp'] = time();
×
287

UNCOV
288
                $this->save_transaction_queue( $transaction_queue );
×
289
        }
290

291
        /**
292
         * The length of time in seconds a transaction should stay in the queue
293
         *
294
         * @return mixed|void
295
         */
296
        public function get_timestamp_threshold() {
UNCOV
297
                return apply_filters( 'woographql_session_transaction_timeout', 30 );
×
298
        }
299

300
        /**
301
         * Whether the transaction has expired. This helps prevent infinite loops while searching through the transaction
302
         * queue.
303
         *
304
         * @param array $transaction_queue  Transaction queue.
305
         *
306
         * @return bool
307
         */
308
        public function did_transaction_expire( $transaction_queue ) {
309
                // Guard against empty transaction queue. We assume that it is invalid since we cannot calculate.
UNCOV
310
                if ( empty( $transaction_queue ) ) {
×
UNCOV
311
                        return true;
×
312
                }
313

314
                // Guard against empty timestamp. We assume that it is invalid since we cannot calculate.
UNCOV
315
                if ( empty( $transaction_queue[0] ) || empty( $transaction_queue[0]['timestamp'] ) ) {
×
UNCOV
316
                        return true;
×
317
                }
318

319
                $now        = time();
×
320
                $stamp      = $transaction_queue[0]['timestamp'];
×
UNCOV
321
                $threshold  = $this->get_timestamp_threshold();
×
322
                $difference = $now - $stamp;
×
323

UNCOV
324
                return $difference > $threshold;
×
325
        }
326
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2024 Coveralls, Inc