File: /var/www/html/wp-content/plugins/wp-graphql/src/Data/Loader/AbstractDataLoader.php
<?php
namespace WPGraphQL\Data\Loader;
use Exception;
use GraphQL\Deferred;
use GraphQL\Utils\Utils;
use WPGraphQL\AppContext;
use WPGraphQL\Model\Model;
/**
* Class AbstractDataLoader
*
* @package WPGraphQL\Data\Loader
*
* @todo Replace this type with a generic.
* @phpstan-type TModel \WPGraphQL\Model\Model<mixed>
*/
abstract class AbstractDataLoader {
/**
* Whether the loader should cache results or not. In some cases the loader may be used to just
* get content but not bother with caching it.
*
* Default: true
*
* @var bool
*/
private $shouldCache = true;
/**
* This stores an array of items that have already been loaded
*
* @var array<int|string,mixed>
*/
private $cached = [];
/**
* This stores an array of IDs that need to be loaded
*
* @var array<int|string,int|string>
*/
private $buffer = [];
/**
* This stores a reference to the AppContext for the loader to make use of
*
* @var \WPGraphQL\AppContext
*/
protected $context;
/**
* AbstractDataLoader constructor.
*
* @param \WPGraphQL\AppContext $context
*/
public function __construct( AppContext $context ) {
$this->context = $context;
}
/**
* Given a Database ID, the particular loader will buffer it and resolve it deferred.
*
* @param mixed|int|string $database_id The database ID for a particular loader to load an object
*
* @return \GraphQL\Deferred|null
* @throws \Exception
*
* @phpstan-return ($database_id is int|string ? \GraphQL\Deferred : null)
*/
public function load_deferred( $database_id ) {
if ( empty( $database_id ) ) {
return null;
}
$database_id = absint( $database_id ) ? absint( $database_id ) : sanitize_text_field( $database_id );
$this->buffer( [ $database_id ] );
return new Deferred(
function () use ( $database_id ) {
return $this->load( $database_id );
}
);
}
/**
* Add keys to buffer to be loaded in single batch later.
*
* @param int[]|string[] $keys The keys of the objects to buffer
*
* @return $this
* @throws \Exception
*/
public function buffer( array $keys ) {
foreach ( $keys as $index => $key ) {
$key = $this->key_to_scalar( $key );
if ( ! is_scalar( $key ) ) {
throw new Exception(
static::class . '::buffer expects all keys to be scalars, but key ' .
'at position ' . esc_html( $index ) . ' is ' . esc_html(
Utils::printSafe( $keys ) . '. ' .
$this->get_scalar_key_hint( $key )
)
);
}
$this->buffer[ $key ] = 1;
}
return $this;
}
/**
* Loads a key and returns value represented by this key.
* Internally this method will load all currently buffered items and cache them locally.
*
* @param int|string|mixed $key
*
* @return ?TModel
* @throws \Exception
*/
public function load( $key ) {
$key = $this->key_to_scalar( $key );
if ( ! is_scalar( $key ) ) {
throw new Exception(
static::class . '::load expects key to be scalar, but got ' . esc_html(
Utils::printSafe( $key ) .
$this->get_scalar_key_hint( $key )
)
);
}
if ( ! $this->shouldCache ) {
$this->buffer = [];
}
$keys = [ $key ];
$this->buffer( $keys );
$result = $this->load_buffered();
return isset( $result[ $key ] ) ? $this->normalize_entry( $result[ $key ], $key ) : null;
}
/**
* Adds the provided key and value to the cache. If the key already exists, no
* change is made. Returns itself for method chaining.
*
* @param mixed $key
* @param mixed $value
*
* @return $this
* @throws \Exception
*/
public function prime( $key, $value ) {
$key = $this->key_to_scalar( $key );
if ( ! is_scalar( $key ) ) {
throw new Exception(
static::class . '::prime is expecting scalar $key, but got ' . esc_html(
Utils::printSafe( $key )
. $this->get_scalar_key_hint( $key )
)
);
}
if ( null === $value ) {
throw new Exception(
static::class . '::prime is expecting non-null $value, but got null. Double-check for null or ' .
' use `clear` if you want to clear the cache'
);
}
if ( ! $this->get_cached( $key ) ) {
/**
* For adding third-party caching support.
* Use this filter to store the queried value in a cache.
*
* @param mixed $value Queried object.
* @param mixed $key Object key.
* @param string $loader_class Loader classname. Use as a means of identified the loader.
* @param mixed $loader Loader instance.
*/
$this->set_cached( $key, $value );
}
return $this;
}
/**
* Clears the value at `key` from the cache, if it exists. Returns itself for
* method chaining.
*
* @param int[]|string[] $keys
*
* @return $this
*/
public function clear( array $keys ) {
foreach ( $keys as $key ) {
$key = $this->key_to_scalar( $key );
if ( isset( $this->cached[ $key ] ) ) {
unset( $this->cached[ $key ] );
}
}
return $this;
}
/**
* Clears the entire cache. To be used when some event results in unknown
* invalidations across this particular `DataLoader`. Returns itself for
* method chaining.
*
* @return \WPGraphQL\Data\Loader\AbstractDataLoader
*/
public function clear_all() {
$this->cached = [];
return $this;
}
/**
* Loads multiple keys. Returns generator where each entry directly corresponds to entry in
* $keys. If second argument $asArray is set to true, returns array instead of generator
*
* @param int[]|string[] $keys
* @param bool $asArray
*
* @return \Generator|array<int|string,mixed>
* @throws \Exception
*/
public function load_many( array $keys, $asArray = false ) {
if ( empty( $keys ) ) {
return [];
}
if ( ! $this->shouldCache ) {
$this->buffer = [];
}
$this->buffer( $keys );
$generator = $this->generate_many( $keys, $this->load_buffered() );
return $asArray ? iterator_to_array( $generator ) : $generator;
}
/**
* Given an array of keys, this yields the object from the cached results
*
* @param int[]|string[] $keys The keys to generate results for
* @param array<int|string,mixed> $result The results for all keys
*
* @return \Generator
*/
private function generate_many( array $keys, array $result ) {
foreach ( $keys as $key ) {
$key = $this->key_to_scalar( $key );
yield isset( $result[ $key ] ) ? $this->normalize_entry( $result[ $key ], $key ) : null;
}
}
/**
* This checks to see if any items are in the buffer, and if there are this
* executes the loaders `loadKeys` method to load the items and adds them
* to the cache if necessary
*
* @return array<int|string,mixed>
* @throws \Exception
*/
private function load_buffered(): array {
// Do not load previously-cached entries:
$keysToLoad = [];
foreach ( $this->buffer as $key => $unused ) {
if ( ! $this->get_cached( $key ) ) {
$keysToLoad[] = $key;
}
}
$result = [];
if ( ! empty( $keysToLoad ) ) {
try {
$loaded = $this->loadKeys( $keysToLoad );
} catch ( \Throwable $e ) {
throw new Exception(
'Method ' . static::class . '::loadKeys is expected to return array, but it threw: ' .
esc_html( $e->getMessage() ),
0,
$e // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
);
}
if ( ! is_array( $loaded ) ) {
throw new Exception(
'Method ' . static::class . '::loadKeys is expected to return an array with keys ' .
'but got: ' . esc_html( Utils::printSafe( $loaded ) )
);
}
if ( $this->shouldCache ) {
foreach ( $loaded as $key => $value ) {
$this->set_cached( $key, $value );
}
}
}
// Re-include previously-cached entries to result:
$result += array_intersect_key( $this->cached, $this->buffer );
$this->buffer = [];
return $result;
}
/**
* This helps to ensure null values aren't being loaded by accident.
*
* @param mixed $key
*/
private function get_scalar_key_hint( $key ): string {
if ( null === $key ) {
return ' Make sure to add additional checks for null values.';
} else {
return ' Try overriding ' . self::class . '::key_to_scalar if your keys are composite.';
}
}
/**
* For loaders that need to decode keys, this method can help with that.
* For example, if we wanted to accept a list of RELAY style global IDs and pass them
* to the loader, we could have the loader centrally decode the keys into their
* integer values in the PostObjectLoader by overriding this method.
*
* @param int|string|mixed $key
*
* @return int|string
*/
protected function key_to_scalar( $key ) {
return $key;
}
/**
* @param mixed $entry The entry loaded from the dataloader to be used to generate a Model
* @param mixed $key The Key used to identify the loaded entry
*
* @return TModel|null
*/
protected function normalize_entry( $entry, $key ) {
/**
* This filter allows the model generated by the DataLoader to be filtered.
*
* Returning anything other than null here will bypass the default model generation
* for an object.
*
* One example would be WooCommerce Products returning a custom Model for posts of post_type "product".
*
* @param null $model The filtered model to return. Default null
* @param mixed $entry The entry loaded from the dataloader to be used to generate a Model
* @param mixed $key The Key used to identify the loaded entry
* @param \WPGraphQL\Data\Loader\AbstractDataLoader $abstract_data_loader The AbstractDataLoader instance
*/
$model = null;
$pre_get_model = apply_filters( 'graphql_dataloader_pre_get_model', $model, $entry, $key, $this );
/**
* If a Model has been pre-loaded via filter, return it and skip the
*/
if ( ! empty( $pre_get_model ) ) {
$model = $pre_get_model;
} else {
$model = $this->get_model( $entry, $key );
}
if ( $model instanceof Model && 'private' === $model->get_visibility() ) {
return null;
}
/**
* Filter the model before returning.
*
* @param mixed $model The Model to be returned by the loader
* @param mixed $entry The entry loaded by dataloader that was used to create the Model
* @param mixed $key The Key that was used to load the entry
* @param \WPGraphQL\Data\Loader\AbstractDataLoader $loader The AbstractDataLoader Instance
*/
return apply_filters( 'graphql_dataloader_get_model', $model, $entry, $key, $this );
}
/**
* Returns a cached data object by key.
*
* @param int|string $key Key.
*
* @return mixed
*/
protected function get_cached( $key ) {
$value = null;
if ( isset( $this->cached[ $key ] ) ) {
$value = $this->cached[ $key ];
}
/**
* Use this filter to retrieving cached data objects from third-party caching system.
*
* @param mixed $value Value to be cached.
* @param int|string $key Key identifying object.
* @param string $loader_class Loader class name.
* @param mixed $loader Loader instance.
*/
$value = apply_filters(
'graphql_dataloader_get_cached',
$value,
$key,
static::class,
$this
);
if ( $value && ! isset( $this->cached[ $key ] ) ) {
$this->cached[ $key ] = $value;
}
return $value;
}
/**
* Caches a data object by key.
*
* @param int|string $key Key.
* @param mixed $value Data object.
*
* @return void
*/
protected function set_cached( $key, $value ) {
/**
* Use this filter to store entry in a third-party caching system.
*
* @param mixed $value Value to be cached.
* @param mixed $key Key identifying object.
* @param string $loader_class Loader class name.
* @param mixed $loader Loader instance.
*/
$this->cached[ $key ] = apply_filters(
'graphql_dataloader_set_cached',
$value,
$key,
static::class,
$this
);
}
/**
* If the loader needs to do any tweaks between getting raw data from the DB and caching,
* this can be overridden by the specific loader and used for transformations, etc.
*
* @param mixed $entry The entry data to be used to generate a Model.
* @param mixed $key The Key to identify the entry by.
*
* @return ?TModel
*/
protected function get_model( $entry, $key ) {
return $entry;
}
/**
* Given array of keys, loads and returns a map consisting of keys from `keys` array and loaded
* values
*
* Note that order of returned values must match exactly the order of keys.
* If some entry is not available for given key - it must include null for the missing key.
*
* For example:
* loadKeys(['a', 'b', 'c']) -> ['a' => 'value1, 'b' => null, 'c' => 'value3']
*
* @param int[]|string[] $keys
*
* @return array<int|string,mixed>
*/
abstract protected function loadKeys( array $keys ); // phpcs:ignore WordPress.NamingConventions.ValidFunctionName.MethodNameInvalid -- @todo deprecate for `::load_keys()`
/**
* @todo remove in 3.0.0
* @deprecated Use load_many instead
* @codeCoverageIgnore
*
* @param int[]|string[] $keys
* @param bool $asArray
*
* @return \Generator|array<int|string,mixed>
* @throws \Exception
*/
public function loadMany( array $keys, $asArray = false ) {
_doing_it_wrong(
__METHOD__,
sprintf(
// translators: %s is the method name
esc_html__( 'This method will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
static::class . '::load_many()'
),
'0.8.4'
);
return $this->load_many( $keys, $asArray );
}
/**
* @todo remove in 3.0.0
* @deprecated in favor of clear_all
* @codeCoverageIgnore
*
* @return \WPGraphQL\Data\Loader\AbstractDataLoader
*/
public function clearAll() {
_doing_it_wrong(
__METHOD__,
sprintf(
// translators: %s is the method name
esc_html__( 'This method will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
static::class . '::clear_all()'
),
'0.8.4'
);
return $this->clear_all();
}
/**
* @todo remove in 3.0.0
* @deprecated Use key_to_scalar instead
* @codeCoverageIgnore
*
* @param int|string|mixed $key
* @return int|string
*/
protected function keyToScalar( $key ) {
_doing_it_wrong(
__METHOD__,
sprintf(
// translators: %s is the method name
esc_html__( 'This method will be removed in the next major release. Use %s instead.', 'wp-graphql' ),
static::class . '::key_to_scalar()'
),
'0.8.4'
);
return $this->key_to_scalar( $key );
}
}