vendor/overblog/graphql-bundle/src/Resolver/AccessResolver.php line 147

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Overblog\GraphQLBundle\Resolver;
  4. use GraphQL\Executor\Promise\Adapter\SyncPromise;
  5. use GraphQL\Executor\Promise\Promise;
  6. use GraphQL\Executor\Promise\PromiseAdapter;
  7. use GraphQL\Type\Definition\ListOfType;
  8. use GraphQL\Type\Definition\ResolveInfo;
  9. use Overblog\GraphQLBundle\Error\UserError;
  10. use Overblog\GraphQLBundle\Error\UserWarning;
  11. use Overblog\GraphQLBundle\Relay\Connection\Output\Connection;
  12. use Overblog\GraphQLBundle\Relay\Connection\Output\Edge;
  13. use function array_map;
  14. use function call_user_func_array;
  15. use function is_iterable;
  16. class AccessResolver
  17. {
  18. private PromiseAdapter $promiseAdapter;
  19. public function __construct(PromiseAdapter $promiseAdapter)
  20. {
  21. $this->promiseAdapter = $promiseAdapter;
  22. }
  23. /**
  24. * @return Promise|mixed|Connection
  25. */
  26. public function resolve(callable $accessChecker, callable $resolveCallback, array $resolveArgs = [], bool $useStrictAccess = false)
  27. {
  28. if ($useStrictAccess || self::isMutationRootField($resolveArgs[3])) {
  29. return $this->checkAccessForStrictMode($accessChecker, $resolveCallback, $resolveArgs);
  30. }
  31. $resultOrPromise = call_user_func_array($resolveCallback, $resolveArgs);
  32. if ($this->isThenable($resultOrPromise)) {
  33. return $this->createPromise(
  34. $resultOrPromise,
  35. fn ($result) => $this->processFilter($result, $accessChecker, $resolveArgs)
  36. );
  37. }
  38. return $this->processFilter($resultOrPromise, $accessChecker, $resolveArgs);
  39. }
  40. private static function isMutationRootField(ResolveInfo $info): bool
  41. {
  42. return 'mutation' === $info->operation->operation && $info->parentType === $info->schema->getMutationType();
  43. }
  44. /**
  45. * @return Promise|mixed
  46. */
  47. private function checkAccessForStrictMode(callable $accessChecker, callable $resolveCallback, array $resolveArgs = [])
  48. {
  49. $promiseOrHasAccess = $this->hasAccess($accessChecker, $resolveArgs);
  50. $callback = function ($hasAccess) use ($resolveArgs, $resolveCallback) {
  51. if (true === $hasAccess) {
  52. return call_user_func_array($resolveCallback, $resolveArgs);
  53. }
  54. $exceptionClassName = self::isMutationRootField($resolveArgs[3]) ? UserError::class : UserWarning::class;
  55. throw new $exceptionClassName('Access denied to this field.');
  56. };
  57. if ($this->isThenable($promiseOrHasAccess)) {
  58. return $this->createPromise($promiseOrHasAccess, $callback);
  59. } else {
  60. return $callback($promiseOrHasAccess);
  61. }
  62. }
  63. /**
  64. * @param iterable|object|Connection $result
  65. *
  66. * @return Connection|iterable
  67. */
  68. private function processFilter($result, callable $accessChecker, array $resolveArgs)
  69. {
  70. /** @var ResolveInfo $resolveInfo */
  71. $resolveInfo = $resolveArgs[3];
  72. if (is_iterable($result) && $resolveInfo->returnType instanceof ListOfType) {
  73. foreach ($result as $i => $object) {
  74. $result[$i] = $this->hasAccess($accessChecker, $resolveArgs, $object) ? $object : null; // @phpstan-ignore-line
  75. }
  76. } elseif ($result instanceof Connection) {
  77. $result->setEdges(array_map(
  78. function (Edge $edge) use ($accessChecker, $resolveArgs) {
  79. $edge->setNode($this->hasAccess($accessChecker, $resolveArgs, $edge->getNode()) ? $edge->getNode() : null);
  80. return $edge;
  81. },
  82. $result->getEdges()
  83. ));
  84. } elseif (!$this->hasAccess($accessChecker, $resolveArgs, $result)) {
  85. throw new UserWarning('Access denied to this field.');
  86. }
  87. return $result; // @phpstan-ignore-line
  88. }
  89. /**
  90. * @param mixed $object
  91. *
  92. * @return mixed
  93. */
  94. private function hasAccess(callable $accessChecker, array $resolveArgs = [], $object = null)
  95. {
  96. $resolveArgs[] = $object;
  97. $accessOrPromise = $accessChecker(...$resolveArgs);
  98. return $accessOrPromise;
  99. }
  100. /**
  101. * @param mixed $object
  102. */
  103. private function isThenable($object): bool
  104. {
  105. $object = $this->extractAdoptedPromise($object);
  106. return $this->promiseAdapter->isThenable($object) || $object instanceof SyncPromise;
  107. }
  108. /**
  109. * @param mixed $object
  110. *
  111. * @return SyncPromise|mixed|\React\Promise\Promise
  112. */
  113. private function extractAdoptedPromise($object)
  114. {
  115. if ($object instanceof Promise) {
  116. $object = $object->adoptedPromise;
  117. }
  118. return $object;
  119. }
  120. /**
  121. * @param mixed $promise
  122. */
  123. private function createPromise($promise, callable $onFulfilled = null): Promise
  124. {
  125. return $this->promiseAdapter->then(
  126. new Promise($this->extractAdoptedPromise($promise), $this->promiseAdapter),
  127. $onFulfilled
  128. );
  129. }
  130. }