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

wp-graphql / wp-graphql / 14716683875

28 Apr 2025 07:58PM UTC coverage: 84.287% (+1.6%) from 82.648%
14716683875

push

github

actions-user
release: merge develop into master for v2.3.0

15905 of 18870 relevant lines covered (84.29%)

257.23 hits per line

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

54.61
/src/Data/Loader/AbstractDataLoader.php
1
<?php
2

3
namespace WPGraphQL\Data\Loader;
4

5
use Exception;
6
use GraphQL\Deferred;
7
use GraphQL\Utils\Utils;
8
use WPGraphQL\AppContext;
9
use WPGraphQL\Model\Model;
10

11
/**
12
 * Class AbstractDataLoader
13
 *
14
 * @package WPGraphQL\Data\Loader
15
 */
16
abstract class AbstractDataLoader {
17

18
        /**
19
         * Whether the loader should cache results or not. In some cases the loader may be used to just
20
         * get content but not bother with caching it.
21
         *
22
         * Default: true
23
         *
24
         * @var bool
25
         */
26
        private $shouldCache = true;
27

28
        /**
29
         * This stores an array of items that have already been loaded
30
         *
31
         * @var array<int|string,mixed>
32
         */
33
        private $cached = [];
34

35
        /**
36
         * This stores an array of IDs that need to be loaded
37
         *
38
         * @var array<int|string,int|string>
39
         */
40
        private $buffer = [];
41

42
        /**
43
         * This stores a reference to the AppContext for the loader to make use of
44
         *
45
         * @var \WPGraphQL\AppContext
46
         */
47
        protected $context;
48

49
        /**
50
         * AbstractDataLoader constructor.
51
         *
52
         * @param \WPGraphQL\AppContext $context
53
         */
54
        public function __construct( AppContext $context ) {
756✔
55
                $this->context = $context;
756✔
56
        }
57

58
        /**
59
         * Given a Database ID, the particular loader will buffer it and resolve it deferred.
60
         *
61
         * @param mixed|int|string $database_id The database ID for a particular loader to load an object
62
         *
63
         * @return \GraphQL\Deferred|null
64
         * @throws \Exception
65
         */
66
        public function load_deferred( $database_id ) {
360✔
67
                if ( empty( $database_id ) ) {
360✔
68
                        return null;
×
69
                }
70

71
                $database_id = absint( $database_id ) ? absint( $database_id ) : sanitize_text_field( $database_id );
360✔
72

73
                $this->buffer( [ $database_id ] );
360✔
74

75
                return new Deferred(
360✔
76
                        function () use ( $database_id ) {
360✔
77
                                return $this->load( $database_id );
360✔
78
                        }
360✔
79
                );
360✔
80
        }
81

82
        /**
83
         * Add keys to buffer to be loaded in single batch later.
84
         *
85
         * @param int[]|string[] $keys The keys of the objects to buffer
86
         *
87
         * @return $this
88
         * @throws \Exception
89
         */
90
        public function buffer( array $keys ) {
520✔
91
                foreach ( $keys as $index => $key ) {
520✔
92
                        $key = $this->key_to_scalar( $key );
520✔
93
                        if ( ! is_scalar( $key ) ) {
520✔
94
                                throw new Exception(
×
95
                                        static::class . '::buffer expects all keys to be scalars, but key ' .
×
96
                                        'at position ' . esc_html( $index ) . ' is ' . esc_html(
×
97
                                                Utils::printSafe( $keys ) . '. ' .
×
98
                                                $this->get_scalar_key_hint( $key )
×
99
                                        )
×
100
                                );
×
101
                        }
102
                        $this->buffer[ $key ] = 1;
520✔
103
                }
104

105
                return $this;
520✔
106
        }
107

108
        /**
109
         * Loads a key and returns value represented by this key.
110
         * Internally this method will load all currently buffered items and cache them locally.
111
         *
112
         * @param int|string|mixed $key
113
         *
114
         * @return ?\WPGraphQL\Model\Model
115
         * @throws \Exception
116
         */
117
        public function load( $key ) {
519✔
118
                $key = $this->key_to_scalar( $key );
519✔
119
                if ( ! is_scalar( $key ) ) {
519✔
120
                        throw new Exception(
×
121
                                static::class . '::load expects key to be scalar, but got ' . esc_html(
×
122
                                        Utils::printSafe( $key ) .
×
123
                                        $this->get_scalar_key_hint( $key )
×
124
                                )
×
125
                        );
×
126
                }
127
                if ( ! $this->shouldCache ) {
519✔
128
                        $this->buffer = [];
×
129
                }
130
                $keys = [ $key ];
519✔
131
                $this->buffer( $keys );
519✔
132
                $result = $this->load_buffered();
519✔
133

134
                return isset( $result[ $key ] ) ? $this->normalize_entry( $result[ $key ], $key ) : null;
519✔
135
        }
136

137
        /**
138
         * Adds the provided key and value to the cache. If the key already exists, no
139
         * change is made. Returns itself for method chaining.
140
         *
141
         * @param mixed $key
142
         * @param mixed $value
143
         *
144
         * @return $this
145
         * @throws \Exception
146
         */
147
        public function prime( $key, $value ) {
×
148
                $key = $this->key_to_scalar( $key );
×
149
                if ( ! is_scalar( $key ) ) {
×
150
                        throw new Exception(
×
151
                                static::class . '::prime is expecting scalar $key, but got ' . esc_html(
×
152
                                        Utils::printSafe( $key )
×
153
                                        . $this->get_scalar_key_hint( $key )
×
154
                                )
×
155
                        );
×
156
                }
157
                if ( null === $value ) {
×
158
                        throw new Exception(
×
159
                                static::class . '::prime is expecting non-null $value, but got null. Double-check for null or ' .
×
160
                                ' use `clear` if you want to clear the cache'
×
161
                        );
×
162
                }
163
                if ( ! $this->get_cached( $key ) ) {
×
164
                        /**
165
                         * For adding third-party caching support.
166
                         * Use this filter to store the queried value in a cache.
167
                         *
168
                         * @param mixed  $value         Queried object.
169
                         * @param mixed  $key           Object key.
170
                         * @param string $loader_class  Loader classname. Use as a means of identified the loader.
171
                         * @param mixed  $loader        Loader instance.
172
                         */
173
                        $this->set_cached( $key, $value );
×
174
                }
175

176
                return $this;
×
177
        }
178

179
        /**
180
         * Clears the value at `key` from the cache, if it exists. Returns itself for
181
         * method chaining.
182
         *
183
         * @param int[]|string[] $keys
184
         *
185
         * @return $this
186
         */
187
        public function clear( array $keys ) {
×
188
                foreach ( $keys as $key ) {
×
189
                        $key = $this->key_to_scalar( $key );
×
190
                        if ( isset( $this->cached[ $key ] ) ) {
×
191
                                unset( $this->cached[ $key ] );
×
192
                        }
193
                }
194

195
                return $this;
×
196
        }
197

198
        /**
199
         * Clears the entire cache. To be used when some event results in unknown
200
         * invalidations across this particular `DataLoader`. Returns itself for
201
         * method chaining.
202
         *
203
         * @return \WPGraphQL\Data\Loader\AbstractDataLoader
204
         * @deprecated in favor of clear_all
205
         */
206
        public function clearAll() {
×
207
                _deprecated_function( __METHOD__, '0.8.4', static::class . '::clear_all()' );
×
208
                return $this->clear_all();
×
209
        }
210

211
        /**
212
         * Clears the entire cache. To be used when some event results in unknown
213
         * invalidations across this particular `DataLoader`. Returns itself for
214
         * method chaining.
215
         *
216
         * @return \WPGraphQL\Data\Loader\AbstractDataLoader
217
         */
218
        public function clear_all() {
×
219
                $this->cached = [];
×
220

221
                return $this;
×
222
        }
223

224
        /**
225
         * Loads multiple keys. Returns generator where each entry directly corresponds to entry in
226
         * $keys. If second argument $asArray is set to true, returns array instead of generator
227
         *
228
         * @param int[]|string[] $keys
229
         * @param bool           $asArray
230
         *
231
         * @return \Generator|array<int|string,mixed>
232
         * @throws \Exception
233
         *
234
         * @deprecated Use load_many instead
235
         */
236
        public function loadMany( array $keys, $asArray = false ) {
×
237
                _deprecated_function( __METHOD__, '0.8.4', static::class . '::load_many()' );
×
238
                return $this->load_many( $keys, $asArray );
×
239
        }
240

241
        /**
242
         * Loads multiple keys. Returns generator where each entry directly corresponds to entry in
243
         * $keys. If second argument $asArray is set to true, returns array instead of generator
244
         *
245
         * @param int[]|string[] $keys
246
         * @param bool           $asArray
247
         *
248
         * @return \Generator|array<int|string,mixed>
249
         * @throws \Exception
250
         */
251
        public function load_many( array $keys, $asArray = false ) {
340✔
252
                if ( empty( $keys ) ) {
340✔
253
                        return [];
×
254
                }
255
                if ( ! $this->shouldCache ) {
340✔
256
                        $this->buffer = [];
×
257
                }
258
                $this->buffer( $keys );
340✔
259
                $generator = $this->generate_many( $keys, $this->load_buffered() );
340✔
260

261
                return $asArray ? iterator_to_array( $generator ) : $generator;
340✔
262
        }
263

264
        /**
265
         * Given an array of keys, this yields the object from the cached results
266
         *
267
         * @param int[]|string[]          $keys   The keys to generate results for
268
         * @param array<int|string,mixed> $result The results for all keys
269
         *
270
         * @return \Generator
271
         */
272
        private function generate_many( array $keys, array $result ) {
1✔
273
                foreach ( $keys as $key ) {
1✔
274
                        $key = $this->key_to_scalar( $key );
1✔
275
                        yield isset( $result[ $key ] ) ? $this->normalize_entry( $result[ $key ], $key ) : null;
1✔
276
                }
277
        }
278

279
        /**
280
         * This checks to see if any items are in the buffer, and if there are this
281
         * executes the loaders `loadKeys` method to load the items and adds them
282
         * to the cache if necessary
283
         *
284
         * @return array<int|string,mixed>
285
         * @throws \Exception
286
         */
287
        private function load_buffered(): array {
520✔
288
                // Do not load previously-cached entries:
289
                $keysToLoad = [];
520✔
290
                foreach ( $this->buffer as $key => $unused ) {
520✔
291
                        if ( ! $this->get_cached( $key ) ) {
520✔
292
                                $keysToLoad[] = $key;
520✔
293
                        }
294
                }
295

296
                $result = [];
520✔
297
                if ( ! empty( $keysToLoad ) ) {
520✔
298
                        try {
299
                                $loaded = $this->loadKeys( $keysToLoad );
520✔
300
                        } catch ( \Throwable $e ) {
×
301
                                throw new Exception(
×
302
                                        'Method ' . static::class . '::loadKeys is expected to return array, but it threw: ' .
×
303
                                        esc_html( $e->getMessage() ),
×
304
                                        0,
×
305
                                        $e // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
×
306
                                );
×
307
                        }
308

309
                        if ( ! is_array( $loaded ) ) {
520✔
310
                                throw new Exception(
×
311
                                        'Method ' . static::class . '::loadKeys is expected to return an array with keys ' .
×
312
                                        'but got: ' . esc_html( Utils::printSafe( $loaded ) )
×
313
                                );
×
314
                        }
315
                        if ( $this->shouldCache ) {
520✔
316
                                foreach ( $loaded as $key => $value ) {
520✔
317
                                        $this->set_cached( $key, $value );
520✔
318
                                }
319
                        }
320
                }
321

322
                // Re-include previously-cached entries to result:
323
                $result += array_intersect_key( $this->cached, $this->buffer );
520✔
324

325
                $this->buffer = [];
520✔
326

327
                return $result;
520✔
328
        }
329

330
        /**
331
         * This helps to ensure null values aren't being loaded by accident.
332
         *
333
         * @param mixed $key
334
         */
335
        private function get_scalar_key_hint( $key ): string {
×
336
                if ( null === $key ) {
×
337
                        return ' Make sure to add additional checks for null values.';
×
338
                } else {
339
                        return ' Try overriding ' . self::class . '::key_to_scalar if your keys are composite.';
×
340
                }
341
        }
342

343
        /**
344
         * For loaders that need to decode keys, this method can help with that.
345
         * For example, if we wanted to accept a list of RELAY style global IDs and pass them
346
         * to the loader, we could have the loader centrally decode the keys into their
347
         * integer values in the PostObjectLoader by overriding this method.
348
         *
349
         * @param int|string|mixed $key
350
         *
351
         * @return int|string
352
         */
353
        protected function key_to_scalar( $key ) {
520✔
354
                return $key;
520✔
355
        }
356

357
        /**
358
         * @param int|string|mixed $key
359
         *
360
         * @return int|string
361
         * @deprecated Use key_to_scalar instead
362
         */
363
        protected function keyToScalar( $key ) {
×
364
                _deprecated_function( __METHOD__, '0.8.4', static::class . '::key_to_scalar()' );
×
365
                return $this->key_to_scalar( $key );
×
366
        }
367

368
        /**
369
         * @param mixed $entry The entry loaded from the dataloader to be used to generate a Model
370
         * @param mixed $key   The Key used to identify the loaded entry
371
         *
372
         * @return \WPGraphQL\Model\Model|null
373
         */
374
        protected function normalize_entry( $entry, $key ) {
516✔
375

376
                /**
377
                 * This filter allows the model generated by the DataLoader to be filtered.
378
                 *
379
                 * Returning anything other than null here will bypass the default model generation
380
                 * for an object.
381
                 *
382
                 * One example would be WooCommerce Products returning a custom Model for posts of post_type "product".
383
                 *
384
                 * @param null               $model                The filtered model to return. Default null
385
                 * @param mixed              $entry                The entry loaded from the dataloader to be used to generate a Model
386
                 * @param mixed              $key                  The Key used to identify the loaded entry
387
                 * @param \WPGraphQL\Data\Loader\AbstractDataLoader $abstract_data_loader The AbstractDataLoader instance
388
                 */
389
                $model         = null;
516✔
390
                $pre_get_model = apply_filters( 'graphql_dataloader_pre_get_model', $model, $entry, $key, $this );
516✔
391

392
                /**
393
                 * If a Model has been pre-loaded via filter, return it and skip the
394
                 */
395
                if ( ! empty( $pre_get_model ) ) {
516✔
396
                        $model = $pre_get_model;
×
397
                } else {
398
                        $model = $this->get_model( $entry, $key );
516✔
399
                }
400

401
                if ( $model instanceof Model && 'private' === $model->get_visibility() ) {
516✔
402
                        return null;
20✔
403
                }
404

405
                /**
406
                 * Filter the model before returning.
407
                 *
408
                 * @param mixed              $model  The Model to be returned by the loader
409
                 * @param mixed              $entry  The entry loaded by dataloader that was used to create the Model
410
                 * @param mixed              $key    The Key that was used to load the entry
411
                 * @param \WPGraphQL\Data\Loader\AbstractDataLoader $loader The AbstractDataLoader Instance
412
                 */
413
                return apply_filters( 'graphql_dataloader_get_model', $model, $entry, $key, $this );
511✔
414
        }
415

416
        /**
417
         * Returns a cached data object by key.
418
         *
419
         * @param int|string $key Key.
420
         *
421
         * @return mixed
422
         */
423
        protected function get_cached( $key ) {
520✔
424
                $value = null;
520✔
425
                if ( isset( $this->cached[ $key ] ) ) {
520✔
426
                        $value = $this->cached[ $key ];
355✔
427
                }
428

429
                /**
430
                 * Use this filter to retrieving cached data objects from third-party caching system.
431
                 *
432
                 * @param mixed       $value        Value to be cached.
433
                 * @param int|string  $key          Key identifying object.
434
                 * @param string      $loader_class Loader class name.
435
                 * @param mixed       $loader       Loader instance.
436
                 */
437
                $value = apply_filters(
520✔
438
                        'graphql_dataloader_get_cached',
520✔
439
                        $value,
520✔
440
                        $key,
520✔
441
                        static::class,
520✔
442
                        $this
520✔
443
                );
520✔
444

445
                if ( $value && ! isset( $this->cached[ $key ] ) ) {
520✔
446
                        $this->cached[ $key ] = $value;
×
447
                }
448

449
                return $value;
520✔
450
        }
451

452
        /**
453
         * Caches a data object by key.
454
         *
455
         * @param int|string $key    Key.
456
         * @param mixed      $value  Data object.
457
         *
458
         * @return void
459
         */
460
        protected function set_cached( $key, $value ) {
520✔
461
                /**
462
                 * Use this filter to store entry in a third-party caching system.
463
                 *
464
                 * @param mixed  $value         Value to be cached.
465
                 * @param mixed  $key           Key identifying object.
466
                 * @param string $loader_class  Loader class name.
467
                 * @param mixed  $loader        Loader instance.
468
                 */
469
                $this->cached[ $key ] = apply_filters(
520✔
470
                        'graphql_dataloader_set_cached',
520✔
471
                        $value,
520✔
472
                        $key,
520✔
473
                        static::class,
520✔
474
                        $this
520✔
475
                );
520✔
476
        }
477

478
        /**
479
         * If the loader needs to do any tweaks between getting raw data from the DB and caching,
480
         * this can be overridden by the specific loader and used for transformations, etc.
481
         *
482
         * @param mixed $entry The entry data to be used to generate a Model.
483
         * @param mixed $key   The Key to identify the entry by.
484
         *
485
         * @return ?\WPGraphQL\Model\Model
486
         */
487
        protected function get_model( $entry, $key ) {
41✔
488
                return $entry;
41✔
489
        }
490

491
        /**
492
         * Given array of keys, loads and returns a map consisting of keys from `keys` array and loaded
493
         * values
494
         *
495
         * Note that order of returned values must match exactly the order of keys.
496
         * If some entry is not available for given key - it must include null for the missing key.
497
         *
498
         * For example:
499
         * loadKeys(['a', 'b', 'c']) -> ['a' => 'value1, 'b' => null, 'c' => 'value3']
500
         *
501
         * @param int[]|string[] $keys
502
         *
503
         * @return array<int|string,mixed>
504
         */
505
        abstract protected function loadKeys( array $keys ); // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid -- @todo deprecate for `::load_keys()`
506
}
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

© 2025 Coveralls, Inc