Files

223 lines
7.3 KiB
PHP

<?php
/**
* Copyright © Magento, Inc. All rights reserved.
* See COPYING.txt for license details.
*/
declare(strict_types=1);
namespace Magento\GraphQlResolverCache\Model\Plugin\Resolver;
use Magento\Framework\App\Cache\StateInterface as CacheState;
use Magento\Framework\GraphQl\Config\Element\Field;
use Magento\Framework\GraphQl\Query\Resolver\ContextInterface;
use Magento\Framework\GraphQl\Query\Resolver\Value;
use Magento\Framework\GraphQl\Query\ResolverInterface;
use Magento\Framework\GraphQl\Schema\Type\ResolveInfo;
use Magento\Framework\Serialize\SerializerInterface;
use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\CalculationException;
use Magento\GraphQlResolverCache\Model\Resolver\Result\CacheKey\Calculator\ProviderInterface;
use Magento\GraphQlResolverCache\Model\Resolver\Result\ResolverIdentityClassProvider;
use Magento\GraphQlResolverCache\Model\Resolver\Result\Type as GraphQlResolverCache;
use Magento\GraphQlResolverCache\Model\Resolver\Result\ValueProcessorInterface;
use Psr\Log\LoggerInterface;
/**
* Plugin to cache resolver result where applicable.
*
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
*/
class Cache
{
/**
* GraphQL Resolver cache type
*
* @var GraphQlResolverCache
*/
private $graphQlResolverCache;
/**
* @var SerializerInterface
*/
private $serializer;
/**
* @var CacheState
*/
private $cacheState;
/**
* @var ResolverIdentityClassProvider
*/
private $resolverIdentityClassProvider;
/**
* @var ValueProcessorInterface
*/
private ValueProcessorInterface $valueProcessor;
/**
* @var ProviderInterface
*/
private ProviderInterface $keyCalculatorProvider;
/**
* @var LoggerInterface
*/
private $logger;
/**
* @param GraphQlResolverCache $graphQlResolverCache
* @param SerializerInterface $serializer
* @param CacheState $cacheState
* @param ResolverIdentityClassProvider $resolverIdentityClassProvider
* @param ValueProcessorInterface $valueProcessor
* @param ProviderInterface $keyCalculatorProvider
* @param LoggerInterface $logger
*/
public function __construct(
GraphQlResolverCache $graphQlResolverCache,
SerializerInterface $serializer,
CacheState $cacheState,
ResolverIdentityClassProvider $resolverIdentityClassProvider,
ValueProcessorInterface $valueProcessor,
ProviderInterface $keyCalculatorProvider,
LoggerInterface $logger
) {
$this->graphQlResolverCache = $graphQlResolverCache;
$this->serializer = $serializer;
$this->cacheState = $cacheState;
$this->resolverIdentityClassProvider = $resolverIdentityClassProvider;
$this->valueProcessor = $valueProcessor;
$this->keyCalculatorProvider = $keyCalculatorProvider;
$this->logger = $logger;
}
/**
* Checks for cacheability of resolver's data, and, if cacheable, loads and persists cache entry for future use
*
* @param ResolverInterface $subject
* @param \Closure $proceed
* @param Field $field
* @param ContextInterface $context
* @param ResolveInfo $info
* @param array|null $value
* @param array|null $args
* @return mixed|Value
*/
public function aroundResolve(
ResolverInterface $subject,
\Closure $proceed,
Field $field,
$context,
ResolveInfo $info,
array $value = null,
array $args = null
) {
// even though a frontend access proxy is used to prevent saving/loading in $graphQlResolverCache when it is
// disabled, it's best to return as early as possible to avoid unnecessary processing
if (!$this->cacheState->isEnabled(GraphQlResolverCache::TYPE_IDENTIFIER)
|| $info->operation->operation !== 'query'
) {
return $proceed($field, $context, $info, $value, $args);
}
$identityProvider = $this->resolverIdentityClassProvider->getIdentityFromResolver($subject);
if (!$identityProvider) { // not cacheable; proceed
return $this->executeResolver($proceed, $field, $context, $info, $value, $args);
}
// Cache key provider may base cache key on the parent resolver value
// $value is processed on key calculation if needed
try {
$cacheKey = $this->prepareCacheIdentifier($subject, $args, $value);
} catch (CalculationException $e) {
$this->logger->warning(
sprintf(
"Unable to obtain cache key for %s resolver results, proceeding to invoke resolver."
. "Original exception message: %s ",
get_class($subject),
$e->getMessage()
)
);
return $this->executeResolver($proceed, $field, $context, $info, $value, $args);
}
$cachedResult = $this->graphQlResolverCache->load($cacheKey);
if ($cachedResult !== false) {
$returnValue = $this->serializer->unserialize($cachedResult);
$this->valueProcessor->processCachedValueAfterLoad($info, $subject, $cacheKey, $returnValue);
return $returnValue;
}
$returnValue = $this->executeResolver($proceed, $field, $context, $info, $value, $args);
// $value (parent value) is preprocessed (hydrated) on the previous step
$identities = $identityProvider->getIdentities($returnValue, $value);
if (count($identities)) {
$cachedValue = $returnValue;
$this->valueProcessor->preProcessValueBeforeCacheSave($subject, $cachedValue);
$this->graphQlResolverCache->save(
$this->serializer->serialize($cachedValue),
$cacheKey,
$identities,
false // use default lifetime directive
);
unset($cachedValue);
}
return $returnValue;
}
/**
* Call proceed method with context.
*
* @param \Closure $closure
* @param Field $field
* @param ContextInterface $context
* @param ResolveInfo $info
* @param array|null $value
* @param array|null $args
* @return mixed
*/
private function executeResolver(
\Closure $closure,
Field $field,
ContextInterface $context,
ResolveInfo $info,
array &$value = null,
array $args = null
) {
if (is_array($value)) {
$this->valueProcessor->preProcessParentValue($value);
}
return $closure($field, $context, $info, $value, $args);
}
/**
* Generate cache key incorporating factors from parameters.
*
* @param ResolverInterface $resolver
* @param array|null $args
* @param array|null $value
*
* @return string
* @throws CalculationException
*/
private function prepareCacheIdentifier(
ResolverInterface $resolver,
?array $args,
?array $value
): string {
$queryPayloadHash = sha1(get_class($resolver) . $this->serializer->serialize($args ?? []));
return GraphQlResolverCache::CACHE_TAG
. '_'
. $this->keyCalculatorProvider->getKeyCalculatorForResolver($resolver)->calculateCacheKey($value)
. '_'
. $queryPayloadHash;
}
}