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

wp-graphql / wp-graphql / 17334288077

29 Aug 2025 09:07PM UTC coverage: 84.593% (+0.4%) from 84.169%
17334288077

push

github

actions-user
chore: update changeset for PR #3410

15884 of 18777 relevant lines covered (84.59%)

260.51 hits per line

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

58.04
/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
 * @todo Replace this type with a generic.
17
 * @phpstan-type TModel \WPGraphQL\Model\Model<mixed>
18
 */
19
abstract class AbstractDataLoader {
20

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

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

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

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

52
        /**
53
         * AbstractDataLoader constructor.
54
         *
55
         * @param \WPGraphQL\AppContext $context
56
         */
57
        public function __construct( AppContext $context ) {
557✔
58
                $this->context = $context;
557✔
59
        }
60

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

76
                $database_id = absint( $database_id ) ? absint( $database_id ) : sanitize_text_field( $database_id );
360✔
77

78
                $this->buffer( [ $database_id ] );
360✔
79

80
                return new Deferred(
360✔
81
                        function () use ( $database_id ) {
360✔
82
                                return $this->load( $database_id );
360✔
83
                        }
360✔
84
                );
360✔
85
        }
86

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

110
                return $this;
520✔
111
        }
112

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

139
                return isset( $result[ $key ] ) ? $this->normalize_entry( $result[ $key ], $key ) : null;
519✔
140
        }
141

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

181
                return $this;
×
182
        }
183

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

200
                return $this;
×
201
        }
202

203
        /**
204
         * Clears the entire cache. To be used when some event results in unknown
205
         * invalidations across this particular `DataLoader`. Returns itself for
206
         * method chaining.
207
         *
208
         * @return \WPGraphQL\Data\Loader\AbstractDataLoader
209
         */
210
        public function clear_all() {
×
211
                $this->cached = [];
×
212

213
                return $this;
×
214
        }
215

216
        /**
217
         * Loads multiple keys. Returns generator where each entry directly corresponds to entry in
218
         * $keys. If second argument $asArray is set to true, returns array instead of generator
219
         *
220
         * @param int[]|string[] $keys
221
         * @param bool           $asArray
222
         *
223
         * @return \Generator|array<int|string,mixed>
224
         * @throws \Exception
225
         */
226
        public function load_many( array $keys, $asArray = false ) {
340✔
227
                if ( empty( $keys ) ) {
340✔
228
                        return [];
×
229
                }
230
                if ( ! $this->shouldCache ) {
340✔
231
                        $this->buffer = [];
×
232
                }
233
                $this->buffer( $keys );
340✔
234
                $generator = $this->generate_many( $keys, $this->load_buffered() );
340✔
235

236
                return $asArray ? iterator_to_array( $generator ) : $generator;
340✔
237
        }
238

239
        /**
240
         * Given an array of keys, this yields the object from the cached results
241
         *
242
         * @param int[]|string[]          $keys   The keys to generate results for
243
         * @param array<int|string,mixed> $result The results for all keys
244
         *
245
         * @return \Generator
246
         */
247
        private function generate_many( array $keys, array $result ) {
1✔
248
                foreach ( $keys as $key ) {
1✔
249
                        $key = $this->key_to_scalar( $key );
1✔
250
                        yield isset( $result[ $key ] ) ? $this->normalize_entry( $result[ $key ], $key ) : null;
1✔
251
                }
252
        }
253

254
        /**
255
         * This checks to see if any items are in the buffer, and if there are this
256
         * executes the loaders `loadKeys` method to load the items and adds them
257
         * to the cache if necessary
258
         *
259
         * @return array<int|string,mixed>
260
         * @throws \Exception
261
         */
262
        private function load_buffered(): array {
520✔
263
                // Do not load previously-cached entries:
264
                $keysToLoad = [];
520✔
265
                foreach ( $this->buffer as $key => $unused ) {
520✔
266
                        if ( ! $this->get_cached( $key ) ) {
520✔
267
                                $keysToLoad[] = $key;
520✔
268
                        }
269
                }
270

271
                $result = [];
520✔
272
                if ( ! empty( $keysToLoad ) ) {
520✔
273
                        try {
274
                                $loaded = $this->loadKeys( $keysToLoad );
520✔
275
                        } catch ( \Throwable $e ) {
×
276
                                throw new Exception(
×
277
                                        'Method ' . static::class . '::loadKeys is expected to return array, but it threw: ' .
×
278
                                        esc_html( $e->getMessage() ),
×
279
                                        0,
×
280
                                        $e // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
×
281
                                );
×
282
                        }
283

284
                        if ( ! is_array( $loaded ) ) {
520✔
285
                                throw new Exception(
×
286
                                        'Method ' . static::class . '::loadKeys is expected to return an array with keys ' .
×
287
                                        'but got: ' . esc_html( Utils::printSafe( $loaded ) )
×
288
                                );
×
289
                        }
290
                        if ( $this->shouldCache ) {
520✔
291
                                foreach ( $loaded as $key => $value ) {
520✔
292
                                        $this->set_cached( $key, $value );
520✔
293
                                }
294
                        }
295
                }
296

297
                // Re-include previously-cached entries to result:
298
                $result += array_intersect_key( $this->cached, $this->buffer );
520✔
299

300
                $this->buffer = [];
520✔
301

302
                return $result;
520✔
303
        }
304

305
        /**
306
         * This helps to ensure null values aren't being loaded by accident.
307
         *
308
         * @param mixed $key
309
         */
310
        private function get_scalar_key_hint( $key ): string {
×
311
                if ( null === $key ) {
×
312
                        return ' Make sure to add additional checks for null values.';
×
313
                } else {
314
                        return ' Try overriding ' . self::class . '::key_to_scalar if your keys are composite.';
×
315
                }
316
        }
317

318
        /**
319
         * For loaders that need to decode keys, this method can help with that.
320
         * For example, if we wanted to accept a list of RELAY style global IDs and pass them
321
         * to the loader, we could have the loader centrally decode the keys into their
322
         * integer values in the PostObjectLoader by overriding this method.
323
         *
324
         * @param int|string|mixed $key
325
         *
326
         * @return int|string
327
         */
328
        protected function key_to_scalar( $key ) {
520✔
329
                return $key;
520✔
330
        }
331

332
        /**
333
         * @param mixed $entry The entry loaded from the dataloader to be used to generate a Model
334
         * @param mixed $key   The Key used to identify the loaded entry
335
         *
336
         * @return TModel|null
337
         */
338
        protected function normalize_entry( $entry, $key ) {
516✔
339

340
                /**
341
                 * This filter allows the model generated by the DataLoader to be filtered.
342
                 *
343
                 * Returning anything other than null here will bypass the default model generation
344
                 * for an object.
345
                 *
346
                 * One example would be WooCommerce Products returning a custom Model for posts of post_type "product".
347
                 *
348
                 * @param null               $model                The filtered model to return. Default null
349
                 * @param mixed              $entry                The entry loaded from the dataloader to be used to generate a Model
350
                 * @param mixed              $key                  The Key used to identify the loaded entry
351
                 * @param \WPGraphQL\Data\Loader\AbstractDataLoader $abstract_data_loader The AbstractDataLoader instance
352
                 */
353
                $model         = null;
516✔
354
                $pre_get_model = apply_filters( 'graphql_dataloader_pre_get_model', $model, $entry, $key, $this );
516✔
355

356
                /**
357
                 * If a Model has been pre-loaded via filter, return it and skip the
358
                 */
359
                if ( ! empty( $pre_get_model ) ) {
516✔
360
                        $model = $pre_get_model;
×
361
                } else {
362
                        $model = $this->get_model( $entry, $key );
516✔
363
                }
364

365
                if ( $model instanceof Model && 'private' === $model->get_visibility() ) {
516✔
366
                        return null;
20✔
367
                }
368

369
                /**
370
                 * Filter the model before returning.
371
                 *
372
                 * @param mixed              $model  The Model to be returned by the loader
373
                 * @param mixed              $entry  The entry loaded by dataloader that was used to create the Model
374
                 * @param mixed              $key    The Key that was used to load the entry
375
                 * @param \WPGraphQL\Data\Loader\AbstractDataLoader $loader The AbstractDataLoader Instance
376
                 */
377
                return apply_filters( 'graphql_dataloader_get_model', $model, $entry, $key, $this );
511✔
378
        }
379

380
        /**
381
         * Returns a cached data object by key.
382
         *
383
         * @param int|string $key Key.
384
         *
385
         * @return mixed
386
         */
387
        protected function get_cached( $key ) {
520✔
388
                $value = null;
520✔
389
                if ( isset( $this->cached[ $key ] ) ) {
520✔
390
                        $value = $this->cached[ $key ];
355✔
391
                }
392

393
                /**
394
                 * Use this filter to retrieving cached data objects from third-party caching system.
395
                 *
396
                 * @param mixed       $value        Value to be cached.
397
                 * @param int|string  $key          Key identifying object.
398
                 * @param string      $loader_class Loader class name.
399
                 * @param mixed       $loader       Loader instance.
400
                 */
401
                $value = apply_filters(
520✔
402
                        'graphql_dataloader_get_cached',
520✔
403
                        $value,
520✔
404
                        $key,
520✔
405
                        static::class,
520✔
406
                        $this
520✔
407
                );
520✔
408

409
                if ( $value && ! isset( $this->cached[ $key ] ) ) {
520✔
410
                        $this->cached[ $key ] = $value;
×
411
                }
412

413
                return $value;
520✔
414
        }
415

416
        /**
417
         * Caches a data object by key.
418
         *
419
         * @param int|string $key    Key.
420
         * @param mixed      $value  Data object.
421
         *
422
         * @return void
423
         */
424
        protected function set_cached( $key, $value ) {
520✔
425
                /**
426
                 * Use this filter to store entry in a third-party caching system.
427
                 *
428
                 * @param mixed  $value         Value to be cached.
429
                 * @param mixed  $key           Key identifying object.
430
                 * @param string $loader_class  Loader class name.
431
                 * @param mixed  $loader        Loader instance.
432
                 */
433
                $this->cached[ $key ] = apply_filters(
520✔
434
                        'graphql_dataloader_set_cached',
520✔
435
                        $value,
520✔
436
                        $key,
520✔
437
                        static::class,
520✔
438
                        $this
520✔
439
                );
520✔
440
        }
441

442
        /**
443
         * If the loader needs to do any tweaks between getting raw data from the DB and caching,
444
         * this can be overridden by the specific loader and used for transformations, etc.
445
         *
446
         * @param mixed $entry The entry data to be used to generate a Model.
447
         * @param mixed $key   The Key to identify the entry by.
448
         *
449
         * @return ?TModel
450
         */
451
        protected function get_model( $entry, $key ) {
41✔
452
                return $entry;
41✔
453
        }
454

455
        /**
456
         * Given array of keys, loads and returns a map consisting of keys from `keys` array and loaded
457
         * values
458
         *
459
         * Note that order of returned values must match exactly the order of keys.
460
         * If some entry is not available for given key - it must include null for the missing key.
461
         *
462
         * For example:
463
         * loadKeys(['a', 'b', 'c']) -> ['a' => 'value1, 'b' => null, 'c' => 'value3']
464
         *
465
         * @param int[]|string[] $keys
466
         *
467
         * @return array<int|string,mixed>
468
         */
469
        abstract protected function loadKeys( array $keys ); // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid -- @todo deprecate for `::load_keys()`
470

471
        /**
472
         * @todo remove in 3.0.0
473
         * @deprecated Use load_many instead
474
         * @codeCoverageIgnore
475
         *
476
         * @param int[]|string[] $keys
477
         * @param bool           $asArray
478
         *
479
         * @return \Generator|array<int|string,mixed>
480
         * @throws \Exception
481
         */
482
        public function loadMany( array $keys, $asArray = false ) {
483
                _doing_it_wrong(
484
                        __METHOD__,
485
                        sprintf(
486
                                // translators: %s is the method name
487
                                esc_html__( 'This method will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
488
                                static::class . '::load_many()'
489
                        ),
490
                        '0.8.4'
491
                );
492
                return $this->load_many( $keys, $asArray );
493
        }
494

495
        /**
496
         * @todo remove in 3.0.0
497
         * @deprecated in favor of clear_all
498
         * @codeCoverageIgnore
499
         *
500
         * @return \WPGraphQL\Data\Loader\AbstractDataLoader
501
         */
502
        public function clearAll() {
503
                _doing_it_wrong(
504
                        __METHOD__,
505
                        sprintf(
506
                                // translators: %s is the method name
507
                                esc_html__( 'This method will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
508
                                static::class . '::clear_all()'
509
                        ),
510
                        '0.8.4'
511
                );
512
                return $this->clear_all();
513
        }
514

515
        /**
516
         * @todo remove in 3.0.0
517
         * @deprecated Use key_to_scalar instead
518
         * @codeCoverageIgnore
519
         *
520
         * @param int|string|mixed $key
521
         * @return int|string
522
         */
523
        protected function keyToScalar( $key ) {
524
                _doing_it_wrong(
525
                        __METHOD__,
526
                        sprintf(
527
                                // translators: %s is the method name
528
                                esc_html__( 'This method will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
529
                                static::class . '::key_to_scalar()'
530
                        ),
531
                        '0.8.4'
532
                );
533

534
                return $this->key_to_scalar( $key );
535
        }
536
}
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

© 2026 Coveralls, Inc