PluginProbe
WPGraphQL / 2.22.1
WPGraphQL v2.22.1
2.22.3 2.22.2 2.22.1 2.22.0 2.21.1 2.21.0 2.20.0 2.19.0 2.18.0 2.17.0 2.16.0 2.15.1 2.15.0 2.14.1 2.14.0 2.13.0 2.2.0 2.3.0 2.3.3 2.3.6 2.3.8 2.5.0 2.5.1 2.5.2 2.5.3 All 177 releases
wp-graphql / src / Data / Loader / AbstractDataLoader.php

AbstractDataLoader.php in WPGraphQL 2.22.1, at src/Data/Loader/AbstractDataLoader.php

549 lines 14.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 ) {
58 $this->context = $context;
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 ) {
72 if ( empty( $database_id ) ) {
73 return null;
74 }
75
76 $database_id = absint( $database_id ) ? absint( $database_id ) : sanitize_text_field( $database_id );
77
78 $this->buffer( [ $database_id ] );
79
80 return new Deferred(
81 function () use ( $database_id ) {
82 return $this->load( $database_id );
83 }
84 );
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 ) {
96 foreach ( $keys as $index => $key ) {
97 $key = $this->key_to_scalar( $key );
98 if ( ! is_scalar( $key ) ) {
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;
108 }
109
110 return $this;
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 ) {
123 $key = $this->key_to_scalar( $key );
124 if ( ! is_scalar( $key ) ) {
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 ) {
133 $this->buffer = [];
134 }
135 $keys = [ $key ];
136 $this->buffer( $keys );
137 $result = $this->load_buffered();
138
139 return isset( $result[ $key ] ) ? $this->normalize_entry( $result[ $key ], $key ) : null;
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 ) {
227 if ( empty( $keys ) ) {
228 return [];
229 }
230 if ( ! $this->shouldCache ) {
231 $this->buffer = [];
232 }
233 $this->buffer( $keys );
234 $generator = $this->generate_many( $keys, $this->load_buffered() );
235
236 return $asArray ? iterator_to_array( $generator ) : $generator;
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 ) {
248 foreach ( $keys as $key ) {
249 $key = $this->key_to_scalar( $key );
250 yield isset( $result[ $key ] ) ? $this->normalize_entry( $result[ $key ], $key ) : null;
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 {
263 // Do not load previously-cached entries:
264 $keysToLoad = [];
265 foreach ( $this->buffer as $key => $unused ) {
266 if ( ! $this->get_cached( $key ) ) {
267 $keysToLoad[] = $key;
268 }
269 }
270
271 $result = [];
272 if ( ! empty( $keysToLoad ) ) {
273 try {
274 $loaded = $this->loadKeys( $keysToLoad );
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 ) ) {
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 ) {
291 foreach ( $loaded as $key => $value ) {
292 $this->set_cached( $key, $value );
293 }
294 }
295 }
296
297 // Re-include previously-cached entries to result:
298 $result += array_intersect_key( $this->cached, $this->buffer );
299
300 $this->buffer = [];
301
302 return $result;
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 ) {
329 return $key;
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 ) {
339
340 $model = null;
341 /**
342 * This filter allows the model generated by the DataLoader to be filtered.
343 *
344 * Returning anything other than null here will bypass the default model generation
345 * for an object.
346 *
347 * One example would be WooCommerce Products returning a custom Model for posts of post_type "product".
348 *
349 * @param null $model The filtered model to return. Default null
350 * @param mixed $entry The entry loaded from the dataloader to be used to generate a Model
351 * @param mixed $key The Key used to identify the loaded entry
352 * @param \WPGraphQL\Data\Loader\AbstractDataLoader $abstract_data_loader The AbstractDataLoader instance
353 *
354 * @hookGroup models
355 * @since 0.0.5
356 */
357 $pre_get_model = apply_filters( 'graphql_dataloader_pre_get_model', $model, $entry, $key, $this );
358
359 /**
360 * If a Model has been pre-loaded via filter, return it and skip the
361 */
362 if ( ! empty( $pre_get_model ) ) {
363 $model = $pre_get_model;
364 } else {
365 $model = $this->get_model( $entry, $key );
366 }
367
368 if ( $model instanceof Model && 'private' === $model->get_visibility() ) {
369 return null;
370 }
371
372 /**
373 * Filter the model before returning.
374 *
375 * @param mixed $model The Model to be returned by the loader
376 * @param mixed $entry The entry loaded by dataloader that was used to create the Model
377 * @param mixed $key The Key that was used to load the entry
378 * @param \WPGraphQL\Data\Loader\AbstractDataLoader $loader The AbstractDataLoader Instance
379 *
380 * @hookGroup models
381 * @since 0.0.5
382 */
383 return apply_filters( 'graphql_dataloader_get_model', $model, $entry, $key, $this );
384 }
385
386 /**
387 * Returns a cached data object by key.
388 *
389 * @param int|string $key Key.
390 *
391 * @return mixed
392 */
393 protected function get_cached( $key ) {
394 $value = null;
395 if ( isset( $this->cached[ $key ] ) ) {
396 $value = $this->cached[ $key ];
397 }
398
399 /**
400 * Use this filter to retrieving cached data objects from third-party caching system.
401 *
402 * @param mixed $value Value to be cached.
403 * @param int|string $key Key identifying object.
404 * @param string $loader_class Loader class name.
405 * @param mixed $loader Loader instance.
406 *
407 * @hookGroup models
408 * @since 0.0.5
409 */
410 $value = apply_filters(
411 'graphql_dataloader_get_cached',
412 $value,
413 $key,
414 static::class,
415 $this
416 );
417
418 if ( $value && ! isset( $this->cached[ $key ] ) ) {
419 $this->cached[ $key ] = $value;
420 }
421
422 return $value;
423 }
424
425 /**
426 * Caches a data object by key.
427 *
428 * @param int|string $key Key.
429 * @param mixed $value Data object.
430 *
431 * @return void
432 */
433 protected function set_cached( $key, $value ) {
434 /**
435 * Use this filter to store entry in a third-party caching system.
436 *
437 * @param mixed $value Value to be cached.
438 * @param mixed $key Key identifying object.
439 * @param string $loader_class Loader class name.
440 * @param mixed $loader Loader instance.
441 *
442 * @hookGroup models
443 * @since 0.0.5
444 */
445 $this->cached[ $key ] = apply_filters(
446 'graphql_dataloader_set_cached',
447 $value,
448 $key,
449 static::class,
450 $this
451 );
452 }
453
454 /**
455 * If the loader needs to do any tweaks between getting raw data from the DB and caching,
456 * this can be overridden by the specific loader and used for transformations, etc.
457 *
458 * @param mixed $entry The entry data to be used to generate a Model.
459 * @param mixed $key The Key to identify the entry by.
460 *
461 * @return ?TModel
462 */
463 protected function get_model( $entry, $key ) {
464 return $entry;
465 }
466
467 /**
468 * Given array of keys, loads and returns a map consisting of keys from `keys` array and loaded
469 * values
470 *
471 * Note that order of returned values must match exactly the order of keys.
472 * If some entry is not available for given key - it must include null for the missing key.
473 *
474 * For example:
475 * loadKeys(['a', 'b', 'c']) -> ['a' => 'value1, 'b' => null, 'c' => 'value3']
476 *
477 * @param int[]|string[] $keys
478 *
479 * @return array<int|string,mixed>
480 */
481 abstract protected function loadKeys( array $keys ); // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid -- @todo deprecate for `::load_keys()`
482
483 /**
484 * @todo remove in 3.0.0
485 * @deprecated Use load_many instead
486 * @codeCoverageIgnore
487 *
488 * @param int[]|string[] $keys
489 * @param bool $asArray
490 *
491 * @return \Generator|array<int|string,mixed>
492 * @throws \Exception
493 */
494 public function loadMany( array $keys, $asArray = false ) {
495 _doing_it_wrong(
496 __METHOD__,
497 sprintf(
498 // translators: %s is the method name
499 esc_html__( 'This method will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
500 static::class . '::load_many()'
501 ),
502 '0.8.4'
503 );
504 return $this->load_many( $keys, $asArray );
505 }
506
507 /**
508 * @todo remove in 3.0.0
509 * @deprecated in favor of clear_all
510 * @codeCoverageIgnore
511 *
512 * @return \WPGraphQL\Data\Loader\AbstractDataLoader
513 */
514 public function clearAll() {
515 _doing_it_wrong(
516 __METHOD__,
517 sprintf(
518 // translators: %s is the method name
519 esc_html__( 'This method will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
520 static::class . '::clear_all()'
521 ),
522 '0.8.4'
523 );
524 return $this->clear_all();
525 }
526
527 /**
528 * @todo remove in 3.0.0
529 * @deprecated Use key_to_scalar instead
530 * @codeCoverageIgnore
531 *
532 * @param int|string|mixed $key
533 * @return int|string
534 */
535 protected function keyToScalar( $key ) {
536 _doing_it_wrong(
537 __METHOD__,
538 sprintf(
539 // translators: %s is the method name
540 esc_html__( 'This method will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
541 static::class . '::key_to_scalar()'
542 ),
543 '0.8.4'
544 );
545
546 return $this->key_to_scalar( $key );
547 }
548 }
549