diff --git a/Classes/AbstractResolver.php b/Classes/AbstractResolver.php deleted file mode 100644 index 9d0c71b..0000000 --- a/Classes/AbstractResolver.php +++ /dev/null @@ -1,46 +0,0 @@ - $config) { - $resolveMethod = [$this, $name]; - if (is_callable($resolveMethod)) { - $config['resolve'] = $resolveMethod; - } - - $resolvedFields[$name] = $config; - } - - return $resolvedFields; - }; - - return $typeConfig; - } -} diff --git a/Classes/AbstractScalarResolver.php b/Classes/AbstractScalarResolver.php deleted file mode 100644 index d3a29e6..0000000 --- a/Classes/AbstractScalarResolver.php +++ /dev/null @@ -1,37 +0,0 @@ -object = $object; - } - - /** - * @return object - */ - public function getObject() - { - return $this->object; - } - - /** - * @param string $propertyName - * @return bool - */ - public function offsetExists($propertyName) - { - if ($this->object === null) { - return false; - } - if (preg_match('/^(is|has)([A-Z])/', $propertyName) === 1) { - return is_callable([$this->object, $propertyName]); - } - return ObjectAccess::isPropertyGettable($this->object, $propertyName); - } - - /** - * @param string $propertyName - * @return mixed - */ - public function offsetGet($propertyName) - { - if ($this->object === null) { - return null; - } - if (preg_match('/^(is|has)([A-Z])/', $propertyName) === 1) { - return (boolean)call_user_func([$this->object, $propertyName]); - } - $result = ObjectAccess::getProperty($this->object, $propertyName); - if ($result instanceof \IteratorAggregate) { - return new IterableAccessibleObject($result->getIterator()); - } - if (is_array($result) || $result instanceof \Iterator) { - return new IterableAccessibleObject($result); - } - if ($result instanceof \DateTimeInterface) { - return $result; - } - if (is_object($result)) { - return new self($result); - } - return $result; - } - - /** - * @param string $offset - * @param mixed $value - */ - public function offsetSet($offset, $value) - { - throw new \RuntimeException('The AccessibleObject wrapper does not allow for mutation!', 1460895624); - } - - /** - * @param string $offset - */ - public function offsetUnset($offset) - { - throw new \RuntimeException('The AccessibleObject wrapper does not allow for mutation!', 1460895625); - } - - /** - * This is required in order to implicitly cast wrapped string types for example - * - * @return string - */ - function __toString() - { - return (string)$this->object; - } - - -} \ No newline at end of file diff --git a/Classes/Controller/StandardController.php b/Classes/Controller/StandardController.php deleted file mode 100644 index 0948c18..0000000 --- a/Classes/Controller/StandardController.php +++ /dev/null @@ -1,73 +0,0 @@ - GraphQlView::class]; - - /** - * @param string $endpoint The GraphQL endpoint, to allow for providing multiple APIs (this value is set from the routing usually) - * @return void - */ - public function indexAction($endpoint) - { - $this->schemaService->verifySettings($endpoint); - $this->view->assign('endpoint', $endpoint); - } - - /** - * @param string $endpoint The GraphQL endpoint, to allow for providing multiple APIs (this value is set from the routing usually) - * @param string $query The GraphQL query string (see GraphQL::execute()) - * @param array $variables list of variables (if any, see GraphQL::execute()). Note: The variables can be JSON-serialized to a string or a "real" array - * @param string $operationName The operation to execute (if multiple, see GraphQL::execute()) - * @return void - * @Flow\SkipCsrfProtection - * @throws NoSuchArgumentException - */ - public function queryAction($endpoint, $query, $variables = null, $operationName = null) - { - if ($variables !== null && is_string($this->request->getArgument('variables'))) { - $variables = json_decode($this->request->getArgument('variables'), true); - } - - $schema = $this->schemaService->getSchemaForEndpoint($endpoint); - $context = new GraphQLContext($this->request->getHttpRequest()); - GraphQL::setDefaultFieldResolver([SchemaService::class, 'defaultFieldResolver']); - $result = GraphQL::executeQuery($schema, $query, null, $context, $variables, $operationName); - $this->view->assign('result', $result); - } - -} diff --git a/Classes/GraphQLContext.php b/Classes/GraphQLContext.php deleted file mode 100644 index d5c9012..0000000 --- a/Classes/GraphQLContext.php +++ /dev/null @@ -1,32 +0,0 @@ -httpRequest = $httpRequest; - } - - /** - * @return ServerRequestInterface - */ - public function getHttpRequest(): ServerRequestInterface - { - return $this->httpRequest; - } - -} diff --git a/Classes/GraphQLMiddleware.php b/Classes/GraphQLMiddleware.php new file mode 100644 index 0000000..ccc2839 --- /dev/null +++ b/Classes/GraphQLMiddleware.php @@ -0,0 +1,155 @@ +url + if (!\in_array($request->getMethod(), ['POST', 'OPTIONS'], true) || $request->getUri()->getPath() !== $this->uriPath) { + return $handler->handle($request); + } + if ($this->simulateControllerObjectName !== null) { + $mockActionRequest = ActionRequest::fromHttpRequest($request); + // Simulate a request to the specified controller to trigger authentication + $mockActionRequest->setControllerObjectName($this->simulateControllerObjectName); + $this->securityContext->setRequest($mockActionRequest); + } + $response = $this->responseFactory->createResponse(); + $response = $this->addCorsHeaders($response); + if ($request->getMethod() === 'POST') { + $response = $this->handlePostRequest($request, $response); + } + return $response; + } + + private function handlePostRequest(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $api = $this->serviceLocator->get($this->apiObjectName); + $config = ServerConfig::create() + ->setSchema($this->getSchema()) + ->setFieldResolver(new Resolver( + $api, + $this->typeNamespaces === [] ? [(new ReflectionClass($api))->getNamespaceName()] : $this->typeNamespaces, + ))->setErrorsHandler($this->handleGraphQLErrors(...)); + if ($this->debugMode) { + $config->setDebugFlag(); + } + $server = new StandardServer($config); + try { + $request = $this->parseRequestBody($request); + } catch (\JsonException $_) { + return new Response(400, [], 'Invalid JSON request'); + } + + $bodyStream = $this->streamFactory->createStream(); + $newResponse = $server->processPsrRequest($request, $response, $bodyStream); + // For some reason we need to rewind the stream in order to prevent an empty response body + $bodyStream->rewind(); + return $newResponse; + } + + /** + * @throws \JsonException + */ + private function parseRequestBody(ServerRequestInterface $request): ServerRequestInterface + { + if (!empty($request->getParsedBody())) { + return $request; + } + $parsedBody = json_decode($request->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR); + return $request->withParsedBody($parsedBody); + } + + private function addCorsHeaders(ResponseInterface $response): ResponseInterface + { + return $response + ->withHeader('Access-Control-Allow-Origin', $this->corsOrigin) + ->withHeader('Access-Control-Allow-Methods', 'POST,OPTIONS') + ->withHeader('Access-Control-Allow-Headers', 'Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range'); + } + + private function handleGraphQLErrors(array $errors, callable $formatter): array + { + return array_map(function(Throwable $error) use ($formatter) { + if (!$error instanceof ClientAware || !$error->isClientSafe()) { + $this->throwableStorage->logThrowable($error); + } + return $formatter($error); + }, $errors); + } + + private function getSchema(): Schema + { + $cacheKey = md5($this->apiObjectName); + if ($this->schemaCache->has($cacheKey)) { + $documentNode = AST::fromArray($this->schemaCache->get($cacheKey)); + } else { + /** @var GraphQLGenerator $generator */ + $generator = $this->serviceLocator->get(GraphQLGenerator::class); + $schema = $generator->generate($this->apiObjectName)->render(); + try { + $documentNode = Parser::parse($schema); + } catch (SyntaxError $e) { + throw new \RuntimeException(sprintf('Failed to parse GraphQL Schema: %s', $e->getMessage()), 1652975280, $e); + } + try { + $this->schemaCache->set($cacheKey, AST::toArray($documentNode)); + } catch (Exception $e) { + throw new \RuntimeException(sprintf('Failed to store parsed GraphQL Scheme in cache: %s', $e->getMessage()), 1652975323, $e); + } + } + + return BuildSchema::build($documentNode); + } +} diff --git a/Classes/GraphQLMiddlewareFactory.php b/Classes/GraphQLMiddlewareFactory.php new file mode 100644 index 0000000..8cf5762 --- /dev/null +++ b/Classes/GraphQLMiddlewareFactory.php @@ -0,0 +1,50 @@ +debugMode, + $this->corsOrigin, + $this->streamFactory, + $this->responseFactory, + $this->schemaCache, + $this->throwableStorage, + $this->securityContext, + $this->objectManager, + ); + } +} diff --git a/Classes/Http/HttpOptionsMiddleware.php b/Classes/Http/HttpOptionsMiddleware.php deleted file mode 100644 index 0032ab3..0000000 --- a/Classes/Http/HttpOptionsMiddleware.php +++ /dev/null @@ -1,46 +0,0 @@ - skip - if ($request->getMethod() !== 'OPTIONS') { - return $next->handle($request); - } - $endpoint = ltrim($request->getUri()->getPath(), '\/'); - // no matching graphQL endpoint configured => skip - if (!isset($this->endpoints[$endpoint])) { - return $next->handle($request); - } - return $this->responseFactory->createResponse()->withHeader('Allow', 'GET, POST'); - } -} diff --git a/Classes/IterableAccessibleObject.php b/Classes/IterableAccessibleObject.php deleted file mode 100644 index 10ca1fa..0000000 --- a/Classes/IterableAccessibleObject.php +++ /dev/null @@ -1,112 +0,0 @@ -innerIterator = $object; - } elseif (is_array($object)) { - $this->innerIterator = new \ArrayIterator($object); - } else { - throw new \InvalidArgumentException('The IterableAccessibleObject only works on arrays or objects implementing the Iterator interface', 1460895979); - } - } - - /** - * @return \Iterator - */ - public function getIterator() - { - return $this->innerIterator; - } - - /** - * @return AccessibleObject - */ - public function current() - { - $currentElement = $this->innerIterator->current(); - if (is_object($currentElement)) { - return new AccessibleObject($currentElement); - } - return $currentElement; - } - - /** - * @return void - */ - public function next() - { - $this->innerIterator->next(); - } - - /** - * @return string - */ - public function key() - { - return $this->innerIterator->key(); - } - - /** - * @return bool - */ - public function valid() - { - return $this->innerIterator->valid(); - } - - /** - * @return void - */ - public function rewind() - { - $this->innerIterator->rewind(); - } -} \ No newline at end of file diff --git a/Classes/Package.php b/Classes/Package.php deleted file mode 100644 index 39658f6..0000000 --- a/Classes/Package.php +++ /dev/null @@ -1,72 +0,0 @@ -getSignalSlotDispatcher(); - $applicationContext = $bootstrap->getContext(); - - $dispatcher->connect(CacheCommandController::class, 'warmupCaches', SchemaService::class, 'warmupCaches'); - if ($applicationContext->isProduction()) { - return; - } - $dispatcher->connect(Sequence::class, 'afterInvokeStep', function (Step $step) use ($bootstrap, $dispatcher) { - if ($step->getIdentifier() !== 'neos.flow:systemfilemonitor') { - return; - } - $graphQlFileMonitor = FileMonitor::createFileMonitorAtBoot(self::FILE_MONITOR_IDENTIFIER, $bootstrap); - $configurationManager = $bootstrap->getEarlyInstance(ConfigurationManager::class); - $endpointsConfiguration = $configurationManager->getConfiguration(ConfigurationManager::CONFIGURATION_TYPE_SETTINGS, 'Wwwision.GraphQL.endpoints'); - $packageManager = $bootstrap->getEarlyInstance(PackageManager::class); - - foreach($endpointsConfiguration as $endpointConfiguration) { - if (!isset($endpointConfiguration['schema'])) { - continue; - } - $resourceUriParts = Functions::parse_url($endpointConfiguration['schema']); - if (isset($resourceUriParts['scheme']) && $resourceUriParts['scheme'] === 'resource') { - $package = $packageManager->getPackage($resourceUriParts['host']); - $schemaPathAndFilename = Files::concatenatePaths([$package->getResourcesPath(), $resourceUriParts['path']]); - } else { - $schemaPathAndFilename = $endpointConfiguration['schema']; - } - $graphQlFileMonitor->monitorFile($schemaPathAndFilename); - } - - $graphQlFileMonitor->detectChanges(); - $graphQlFileMonitor->shutdownObject(); - }); - - $dispatcher->connect(FileMonitor::class, 'filesHaveChanged', function(string $fileMonitorIdentifier, array $changedFiles) use ($bootstrap) { - if ($fileMonitorIdentifier !== self::FILE_MONITOR_IDENTIFIER || $changedFiles === []) { - return; - } - $schemaCache = $bootstrap->getObjectManager()->get(CacheManager::class)->getCache('Wwwision_GraphQL_Schema'); - $schemaCache->flush(); - }); - } -} diff --git a/Classes/Resolver.php b/Classes/Resolver.php new file mode 100644 index 0000000..d319f9a --- /dev/null +++ b/Classes/Resolver.php @@ -0,0 +1,114 @@ + $typeNamespaces + */ + public function __construct( + private readonly object $api, + private readonly array $typeNamespaces, + ) { + } + + /** + * @param array|null> $args + */ + public function __invoke(object|string|null $objectValue, array $args, mixed $contextValue, ResolveInfo $info): mixed + { + $fieldName = $info->fieldName; + $objectValue ??= $this->api; + + if (method_exists($objectValue, $fieldName)) { + $objectValue = $objectValue->{$fieldName}(...$this->convertArguments($args, $info->fieldDefinition)); + } elseif (property_exists($objectValue, $fieldName)) { + $objectValue = $objectValue->{$fieldName}; + } else { + return null; + } + if ($objectValue instanceof BackedEnum) { + $objectValue = $objectValue->value; + } + return $objectValue; + } + + /** + * @param array|null> $arguments + * @return array|object|null> + */ + private function convertArguments(array $arguments, FieldDefinition $fieldDefinition): array + { + $result = []; + foreach ($arguments as $name => $value) { + $argumentDefinition = $fieldDefinition->getArg($name); + $result[$name] = $this->convertArgument($value, $argumentDefinition); + } + return $result; + } + + /** + * @param string|bool|int|array|null $argument + * @return string|bool|int|array|object|null + */ + private function convertArgument(string|bool|int|array|null $argument, ?Argument $argumentDefinition): string|bool|int|array|object|null + { + if ($argument === null) { + return null; + } + $type = $argumentDefinition?->getType(); + if ($type instanceof NonNull) { + $type = $type->getWrappedType(); + } + $argumentType = $type->name; + if ($type instanceof ListOfType) { + $type = $type->ofType; + if ($type instanceof NonNull) { + $type = $type->getWrappedType(); + } + $argumentType = $type->name . 's'; + } + if (str_ends_with($argumentType, 'Input')) { + $argumentType = substr($argumentType, 0, -5); + } + + $className = $this->resolveClassName($argumentType); + if ($className !== null) { + try { + return instantiate($className, $argument); + } catch (InvalidArgumentException $e) { + throw new RequestError(sprintf('Validation error for %s: %s', $argumentType, $e->getMessage()), 1688654808, $e); + } + } + return $argument; + } + + /** + * @param string $argumentType + * @return class-string|null + */ + private function resolveClassName(string $argumentType): ?string + { + foreach ($this->typeNamespaces as $namespace) { + $className = rtrim($namespace, '\\') . '\\' . $argumentType; + if (class_exists($className)) { + return $className; + } + } + return null; + } +} diff --git a/Classes/ResolverInterface.php b/Classes/ResolverInterface.php deleted file mode 100644 index 2e4d7c5..0000000 --- a/Classes/ResolverInterface.php +++ /dev/null @@ -1,11 +0,0 @@ -verifySettings($endpoint); - $endpointConfiguration = $this->endpointsConfiguration[$endpoint]; - - if (isset($endpointConfiguration['querySchema'])) { - $schemaConfig = SchemaConfig::create() - ->setQuery($this->typeResolver->get($endpointConfiguration['querySchema'])); - if (isset($endpointConfiguration['mutationSchema'])) { - $schemaConfig->setMutation($this->typeResolver->get($endpointConfiguration['mutationSchema'])); - } - if (isset($endpointConfiguration['subscriptionSchema'])) { - $schemaConfig->setSubscription($this->typeResolver->get($endpointConfiguration['subscriptionSchema'])); - } - if (isset($endpointConfiguration['configTypes'])) { - $configTypes = $endpointConfiguration['configTypes']; - array_walk($configTypes, function (&$configType) { - $configType = $this->typeResolver->get($configType); - }); - $schemaConfig->setTypes($configTypes); - } - return new Schema($schemaConfig); - } - - $cacheKey = urlencode($endpoint); - if ($this->schemaCache->has($cacheKey)) { - $documentNode = $this->schemaCache->get($cacheKey); - } else { - $schemaPathAndFilename = $endpointConfiguration['schema']; - $content = Files::getFileContents($schemaPathAndFilename); - $documentNode = Parser::parse($content); - $this->schemaCache->set($cacheKey, $documentNode); - } - - $resolverConfiguration = $endpointConfiguration['resolvers'] ?? []; - $resolverPathPattern = $endpointConfiguration['resolverPathPattern'] ?? null; - /** @var ResolverInterface[] $resolvers */ - $resolvers = []; - return BuildSchema::build($documentNode, function ($config) use (&$resolvers, $resolverConfiguration, $resolverPathPattern) { - $name = $config['name']; - - if (!isset($resolvers[$name])) { - if (isset($resolverConfiguration[$name])) { - $resolvers[$name] = $this->objectManager->get($resolverConfiguration[$name]); - } elseif ($resolverPathPattern !== null) { - $possibleResolverClassName = str_replace('{Type}', $name, $resolverPathPattern); - if ($this->objectManager->isRegistered($possibleResolverClassName)) { - $resolvers[$name] = $this->objectManager->get($possibleResolverClassName); - } - } - } - if (isset($resolvers[$name])) { - return $resolvers[$name]->decorateTypeConfig($config); - } - return $config; - }); - } - - /** - * Verifies the settings for a given $endpoint and throws an exception if they are not valid - * - * @param string $endpoint - * @return void - * @throws \InvalidArgumentException if the settings are incorrect - */ - public function verifySettings(string $endpoint) - { - if (!isset($this->endpointsConfiguration[$endpoint])) { - throw new \InvalidArgumentException(sprintf('The endpoint "%s" is not configured.', $endpoint), 1461435428); - } - - if (!isset($this->endpointsConfiguration[$endpoint]['schema']) && !isset($this->endpointsConfiguration[$endpoint]['querySchema'])) { - throw new \InvalidArgumentException(sprintf('There is no root query schema configured for endpoint "%s".', $endpoint), 1461435432); - } - - if (isset($this->endpointsConfiguration[$endpoint]['schema']) && !file_exists($this->endpointsConfiguration[$endpoint]['schema'])) { - throw new \InvalidArgumentException(sprintf('The Schema file configured for endpoint "%s" does not exist at: "%s".', $endpoint, $this->endpointsConfiguration[$endpoint]['schema']), 1516719329); - } - } - - /** - * @param string|object|array $source - * @param array $args - * @param GraphQLContext $context - * @param ResolveInfo $info - * @return mixed - */ - public static function defaultFieldResolver($source, array $args, GraphQLContext $context, ResolveInfo $info) - { - $fieldName = $info->fieldName; - $property = null; - - if (is_array($source) || $source instanceof \ArrayAccess) { - if (isset($source[$fieldName])) { - $property = $source[$fieldName]; - } - } else if (is_object($source)) { - if (ObjectAccess::isPropertyGettable($source, $fieldName)) { - $property = ObjectAccess::getProperty($source, $fieldName); - } - } - - if ($property instanceof \Closure) { - return $property($source, $args, $context, $info); - } - - return $property; - } - - - /** - * Builds the schema for the given $endpoint and saves it in the cache - * - * @param string $endpoint - * @return DocumentNode - */ - private function buildSchemaForEndpoint(string $endpoint) - { - $schemaPathAndFilename = $this->endpointsConfiguration[$endpoint]['schema']; - $content = Files::getFileContents($schemaPathAndFilename); - $documentNode = Parser::parse($content); - $this->schemaCache->set(urlencode($endpoint), $documentNode, [md5($schemaPathAndFilename)]); - return $documentNode; - } - - /** - * @return void - */ - public function warmupCaches() - { - foreach($this->endpointsConfiguration as $endpoint => $endpointConfiguration) { - if (!isset($endpointConfiguration['schema'])) { - continue; - } - $this->buildSchemaForEndpoint($endpoint); - } - } -} diff --git a/Classes/TypeResolver.php b/Classes/TypeResolver.php deleted file mode 100644 index f7f6d80..0000000 --- a/Classes/TypeResolver.php +++ /dev/null @@ -1,49 +0,0 @@ -get(SomeClass::class)) - * - * @Flow\Scope("singleton") - */ -class TypeResolver -{ - private const INITIALIZING = '__initializing__'; - - /** - * @var ObjectType[] - */ - private $types = []; - - /** - * @param string $typeClassName - * @return ObjectType - */ - public function get($typeClassName) - { - if (!is_string($typeClassName)) { - throw new \InvalidArgumentException(sprintf('Expected string, got "%s"', is_object($typeClassName) ? get_class($typeClassName) : gettype($typeClassName)), 1460065671); - } - if (!is_subclass_of($typeClassName, Type::class)) { - throw new \InvalidArgumentException(sprintf('The TypeResolver can only resolve types extending "GraphQL\Type\Definition\Type", got "%s"', $typeClassName), 1461436398); - } - if (!array_key_exists($typeClassName, $this->types)) { - // The following code seems weird but it is a way to detect circular dependencies (see - $this->types[$typeClassName] = self::INITIALIZING; - $this->types[$typeClassName] = new $typeClassName($this); - } - if ($this->types[$typeClassName] === self::INITIALIZING) { - throw new \RuntimeException(sprintf('The GraphQL Type "%s" seems to have circular dependencies. Please define the fields as callbacks to prevent this error.', $typeClassName), 1554382971); - } - return $this->types[$typeClassName]; - } -} diff --git a/Classes/View/GraphQlView.php b/Classes/View/GraphQlView.php deleted file mode 100644 index 33bcb95..0000000 --- a/Classes/View/GraphQlView.php +++ /dev/null @@ -1,86 +0,0 @@ -variables['result'])) { - throw new FlowException(sprintf('The GraphQlView expects a variable "result" of type "%s", non given!', ExecutionResult::class), 1469545196); - } - $result = $this->variables['result']; - if (!$result instanceof ExecutionResult) { - throw new FlowException(sprintf('The GraphQlView expects a variable "result" of type "%s", "%s" given!', ExecutionResult::class, is_object($result) ? get_class($result) : gettype($result)), 1469545198); - } - - $response = $this->controllerContext->getResponse(); - $response->setContentType('application/json'); - - return json_encode($this->formatResult($result)); - } - - /** - * Formats the result of the GraphQL execution, converting Flow exceptions by hiding the original exception message - * and adding status- and referenceCode. - * - * @param ExecutionResult $executionResult - * @return array - */ - private function formatResult(ExecutionResult $executionResult) - { - $convertedResult = [ - 'data' => $executionResult->data, - ]; - if (!empty($executionResult->errors)) { - $convertedResult['errors'] = array_map(function(Error $error) { - $errorResult = [ - 'message' => $error->message, - 'locations' => $error->getLocations() - ]; - $exception = $error->getPrevious(); - if ($exception instanceof FlowException) { - $errorResult['message'] = ResponseInformationHelper::getStatusMessageByCode($exception->getStatusCode()); - $errorResult['_exceptionCode'] = $exception->getCode(); - $errorResult['_statusCode'] = $exception->getStatusCode(); - $errorResult['_referenceCode'] = $exception->getReferenceCode(); - } - if ($exception instanceof \Exception) { - $message = $this->throwableStorage->logThrowable($exception); - $this->logger->error($message); - } - return $errorResult; - }, $executionResult->errors); - } - if (!empty($executionResult->extensions)) { - $convertedResult['extensions'] = (array)$executionResult->extensions; - } - return $convertedResult; - } -} diff --git a/Configuration/Caches.yaml b/Configuration/Caches.yaml index 3e448be..de59c41 100644 --- a/Configuration/Caches.yaml +++ b/Configuration/Caches.yaml @@ -1,3 +1,6 @@ -Wwwision_GraphQL_Schema: +Wwwision_GraphQL_GraphQLSchemaCache: frontend: Neos\Cache\Frontend\VariableFrontend - backend: Neos\Cache\Backend\FileBackend + backend: Neos\Cache\Backend\SimpleFileBackend + backendOptions: + # 0 = don't expire + defaultLifetime: 0 diff --git a/Configuration/Development/Caches.yaml b/Configuration/Development/Caches.yaml new file mode 100644 index 0000000..363a771 --- /dev/null +++ b/Configuration/Development/Caches.yaml @@ -0,0 +1,3 @@ +Wwwision_GraphQL_GraphQLSchemaCache: + # disable GraphQL schema caching in development mode + backend: Neos\Cache\Backend\NullBackend diff --git a/Configuration/Development/Settings.yaml b/Configuration/Development/Settings.yaml new file mode 100644 index 0000000..e09b1ba --- /dev/null +++ b/Configuration/Development/Settings.yaml @@ -0,0 +1,3 @@ +Wwwision: + GraphQL: + debugMode: true diff --git a/Configuration/Objects.yaml b/Configuration/Objects.yaml index f7972bb..0050867 100644 --- a/Configuration/Objects.yaml +++ b/Configuration/Objects.yaml @@ -1,9 +1,13 @@ -Wwwision\GraphQL\SchemaService: - properties: - 'schemaCache': +Wwwision\GraphQL\GraphQLMiddlewareFactory: + arguments: + 1: + setting: Wwwision.GraphQL.debugMode + 2: + setting: Wwwision.GraphQL.corsOrigin + 3: object: - factoryObjectName: Neos\Flow\Cache\CacheManager - factoryMethodName: getCache + factoryObjectName: 'Neos\Flow\Cache\CacheManager' + factoryMethodName: 'getCache' arguments: 1: - value: Wwwision_GraphQL_Schema + value: 'Wwwision_GraphQL_GraphQLSchemaCache' diff --git a/Configuration/Routes.yaml b/Configuration/Routes.yaml deleted file mode 100644 index a0fe159..0000000 --- a/Configuration/Routes.yaml +++ /dev/null @@ -1,22 +0,0 @@ -- - name: 'graphql - GraphQL Playground' - uriPattern: '' - defaults: - '@package': 'Wwwision.GraphQL' - '@controller': 'Standard' - '@action': 'index' - '@format': 'html' - 'endpoint': '' - appendExceedingArguments: true - httpMethods: [GET] - -- - name: 'graphql - endpoint' - uriPattern: '' - defaults: - '@package': 'Wwwision.GraphQL' - '@controller': 'Standard' - '@action': 'query' - '@format': 'json' - 'endpoint': '' - httpMethods: [POST] diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml index d0bcbf2..94434f4 100644 --- a/Configuration/Settings.yaml +++ b/Configuration/Settings.yaml @@ -1,14 +1,5 @@ -Neos: - Flow: - http: - middlewares: - 'Wwwision.GraphQL:Options': - position: 'before routing' - middleware: 'Wwwision\GraphQL\Http\HttpOptionsMiddleware' - Wwwision: GraphQL: - endpoints: [] -# 'some-endpoint': -# 'queryType': 'Some\Fully\Qualified\Namespace' -# 'mutationType': 'Some\Fully\Qualified\Namespace' + debugMode: false + # The "Access-Control-Allow-Origin" response header + corsOrigin: '*' diff --git a/README.md b/README.md index c7aeeaa..156d463 100644 --- a/README.md +++ b/README.md @@ -1,289 +1,122 @@ # Wwwision.GraphQL -Easily create GraphQL APIs with Neos and Flow. +Easily create GraphQL APIs with [https://www.neos.io/](Neos) and [https://flow.neos.io/](Flow). ## Background This package is a small collection of tools that'll make it easier to provide [GraphQL](http://graphql.org/) endpoints with Neos and Flow. -It is a wrapper for the [PHP port of webonyx](https://github.com/webonyx/graphql-php) that comes with following additions: +It is a wrapper for the [PHP port of webonyx](https://github.com/webonyx/graphql-php) that comes with automatic Schema generation from PHP code (using [wwwision/types](https://github.com/bwaidelich/types)) +and an easy-to-configure [PSR-15](https://www.php-fig.org/psr/psr-15/) compatible HTTP middleware. -* A `TypeResolver` that allows for easy interdependency between complex GraphQL type definitions -* The `AccessibleObject` and `IterableAccessibleObject` wrappers that make it possible to expose arbitrary objects to - the GraphQL API -* A `StandardController` that renders the [GraphQL Playground](https://github.com/prismagraphql/graphql-playground) and acts as dispatcher - for API calls -* A HTTP Component that responds to `OPTIONS` requests correctly (required for CORS preflight requests for example) -* A custom `GraphQLContext` that is available in all resolvers and allows access to the current HTTP Request +## Usage -## Installation +Install via [composer](https://getcomposer.org/doc/): ``` composer require wwwision/graphql ``` -(Refer to the [composer documentation](https://getcomposer.org/doc/) for more details) +### Simple tutorial -## Simple tutorial - -Create a simple Root Query definition within any Flow package: - -`ExampleRootQuery.php`: +Create a class containing at least one public method with a `Query` attribute (see [wwwision/types-graphql](https://github.com/bwaidelich/types-graphql) for more details): ```php 'ExampleRootQuery', - 'fields' => [ - 'ping' => [ - 'type' => Type::string(), - 'resolve' => function () { - return 'pong'; - }, - ], - ], - ]); + #[Query] + public function ping(string $name): string { + return strtoupper($name); } } ``` -Now register this endpoint like so: - -`Settings.yaml`: +Now define a [virtual object](https://flowframework.readthedocs.io/en/stable/TheDefinitiveGuide/PartIII/ObjectManagement.html#sect-virtual-objects) for the [HTTP middleware](https://flowframework.readthedocs.io/en/stable/TheDefinitiveGuide/PartIII/Http.html#middlewares-chain) +in some `Objects.yaml` configuration: ```yaml -Wwwision: - GraphQL: - endpoints: - 'test': - 'querySchema': 'Your\Package\ExampleRootQuery' +'Your.Package:GraphQLMiddleware': + className: 'Wwwision\GraphQL\GraphQLMiddleware' + scope: singleton + factoryObjectName: Wwwision\GraphQL\GraphQLMiddlewareFactory + arguments: + 1: + value: '/graphql' + 2: + value: 'Your\Package\YourApi' ``` -And, lastly, activate the corresponding routes: - -`Settings.yaml`: +And, lastly, register that custom middleware in `Settings.yaml`: ```yaml Neos: Flow: - mvc: - routes: - 'Wwwision.GraphQL': - variables: - 'endpoint': 'test' -``` - -This will make the endpoint "test" available under `/test`. - -Note: If you already have more specific routes in place, or want to provide multiple GraphQL endpoints you can as well -activate routes in your global `Routes.yaml` file: - -```yaml -- - name: 'GraphQL API' - uriPattern: '' - subRoutes: - 'GraphQLSubroutes': - package: 'Wwwision.GraphQL' - variables: - 'endpoint': 'test' + http: + middlewares: + 'Your.Package:GraphQL': + position: 'before routing' + middleware: 'Your.Package:GraphQLMiddleware' ``` -**Congratulations**, your first GraphQL API is done and you should be able to invoke the GraphQL Playground by browsing to `/test`: - -![](playground.png) - -For a more advanced example, have a look at the [Neos Content Repository implementation](https://github.com/bwaidelich/Wwwision.Neos.GraphQL) - -## Custom context +And with that, a working GraphQL API is accessible underneath `/graphql`. -Resolvers should be as simple and self-contained as possible. But sometimes it's useful to have access to the current -HTTP request. For example in order to do explicit authentication or to render URLs. -With v2.1+ there's a new `GraphQLContext` accessible to all resolvers that allows to access the current HTTP request: +### Complex types -```php - function ($value, array $args, GraphQLContext $context) { - $baseUri = $context->getHttpRequest()->getBaseUri(); - // ... - }, -``` - -`$value` is the object containing the field. Its value is `null` on the root mutation/query. -`$args` is the array of arguments specified for that field. It's an empty array if no arguments have been specified. -`$context` is an instance of the `GraphQLContext` with a getter for the current HTTP request. - -## Circular Dependencies - -Sometimes GraphQL types reference themselves. -For example a type `Person` could have a field `friends` that is a list of `Person`-types itself. - -The following code won't work with the latest version of this package: - -```php - 'Person', - 'fields' => [ - 'name' => ['type' => Type::string()], - 'friends' => [ - // THIS WON'T WORK! - 'type' => Type::listOf($typeResolver->get(self::class)), - 'resolve' => function () { - // ... - }, - ], - ], - ]); - } -} -``` -To solve this, the fields can be configured as closure like described in the [graphql-php documentation](https://webonyx.github.io/graphql-php/type-system/object-types/#recurring-and-circular-types): +By default, all types with the *same namespace* as the specified API class will be resolved automatically, so you could do: ```php - 'Person', - 'fields' => function() use ($typeResolver) { - return [ - 'name' => ['type' => Type::string()], - 'friends' => [ - 'type' => Type::listOf($typeResolver->get(self::class)), - 'resolve' => function () { - // ... - }, - ], - ]; - } - ]); - } +#[Query] +public function ping(Name $name): Name { + return strtoupper($name); } ``` - -Alternatively the schema can be defined via a `*.graphql` file: - -## Define Schemas using the GraphQL Schema language - -Since version 3.0 schemas can be defined using the [GraphQL Schema language](https://graphql.org/learn/schema/). - -Routes are configured like above, but in the endpoint settings instead of the `querySchema` the schema file and so called -`resolvers` are configured like so: +as long as there is a suitable `Name` object in the same namespace (`Your\Package`). +To support types from _different_ namespaces, those can be specified as third argument of the `GraphQLMiddlewareFactory`: ```yaml -Wwwision: - GraphQL: - endpoints: - 'test': - schema: 'resource://Wwwision.Test/Private/GraphQL/schema.graphql' - resolvers: - 'Query': 'Wwwision\Test\ExampleResolver' +'Your.Package:GraphQLMiddleware': + # ... + arguments: + # ... + # Look for classes in the following namespaces when resolving types: + 3: + value: + - 'Your\Package\Types' + - 'SomeOther\Package\Commands' ``` -The corresponding schema could look like: +### Authentication -```graphql -schema { - query: Query -} - -type Query { - # some description - ping(name: String!): String -} -``` +Commonly the GraphQL middleware is executed before the routing middleware. So the `Security\Context` is not yet initialized. +This package allows you to "simulate" the request to an MVC controller thought in order to initialize security. +This is done with the fourth argument of the `GraphQLMiddlewareFactory`: -And the resolver like: - -```php -indexAction())' - 'Wwwision.GraphQL:Api.Blacklist': - matcher: 'method(Wwwision\GraphQL\Controller\StandardController->queryAction())' - 'Wwwision.GraphQL:Playground': - matcher: 'method(Wwwision\GraphQL\Controller\StandardController->indexAction(endpoint == "{parameters.endpoint}"))' - parameters: - 'endpoint': - className: 'Neos\Flow\Security\Authorization\Privilege\Parameter\StringPrivilegeParameter' - 'Wwwision.GraphQL:Api': - matcher: 'method(Wwwision\GraphQL\Controller\StandardController->queryAction(endpoint == "{parameters.endpoint}"))' - parameters: - 'endpoint': - className: 'Neos\Flow\Security\Authorization\Privilege\Parameter\StringPrivilegeParameter' -``` - -With the two blacklist privileges calls to the endpoints are forbidden by default (this is not required -if you use this package with Neos because that already blocks controller actions by default). -The other two privileges allow you to selectively grant access to a given endpoint like so: +Contributions in the form of [issues](https://github.com/bwaidelich/Wwwision.GraphQL/issues) or [pull requests](https://github.com/bwaidelich/Wwwision.GraphQL/pulls) are highly appreciated +## License -```yaml -roles: - 'Neos.Flow:Everybody': - privileges: - - - privilegeTarget: 'Wwwision.GraphQL:Api' - parameters: - 'endpoint': 'test' - permission: GRANT -``` - -This would re-enable the GraphQL API (POST requests) for any user, but keep the Playground blocked. +See [LICENSE](./LICENSE) diff --git a/Resources/Private/Templates/Standard/Index.html b/Resources/Private/Templates/Standard/Index.html deleted file mode 100644 index 61bbdad..0000000 --- a/Resources/Private/Templates/Standard/Index.html +++ /dev/null @@ -1,540 +0,0 @@ - - - - - - - - GraphQL Playground - - - - - - - - - - -
- -
Loading - GraphQL Playground -
-
- -
- - - diff --git a/Tests/Unit/AccessibleObjectTest.php b/Tests/Unit/AccessibleObjectTest.php deleted file mode 100644 index 55aa41a..0000000 --- a/Tests/Unit/AccessibleObjectTest.php +++ /dev/null @@ -1,183 +0,0 @@ -accessibleObject = new AccessibleObject(new ExampleObject('Foo')); - } - - /** - * @test - */ - public function getObjectReturnsTheUnalteredObject() - { - $object = new \stdClass(); - $accessibleObject = new AccessibleObject($object); - $this->assertSame($object, $accessibleObject->getObject()); - } - - /** - * @test - */ - public function offsetExistsReturnsFalseIfObjectIsNotSet() - { - $accessibleObject = new AccessibleObject(null); - $this->assertFalse($accessibleObject->offsetExists('foo')); - } - - public function simpleOffsetGetDataProvider() - { - return [ - ['someString', 'Foo'], - ['isFoo', true], - ['hasBar', false], - - - // is* and has* can be omitted (ObjectAccess behavior) - ['foo', true], - ['bar', false], - - // The following tests show that property resolving works case insensitive - // like the underlying ObjectAccess. I think it would be less error prone if they didn't - // But that's the way it currently works, so these tests merely document that behavior - ['SomeString', 'Foo'], - ['somestring', 'Foo'], - ['SoMeStRiNg', 'Foo'], - ]; - } - - /** - * @test - * @dataProvider simpleOffsetGetDataProvider - * @param string $propertyName - * @param mixed $expectedValue - */ - public function simpleOffsetGetTests($propertyName, $expectedValue) - { - $this->assertSame($expectedValue, $this->accessibleObject[$propertyName]); - } - - /** - * @test - * @expectedException \Neos\Utility\Exception\PropertyNotAccessibleException - */ - public function offsetGetThrowsExceptionForUnknownProperties() - { - $this->accessibleObject['unknown']; - } - - /** - * @test - */ - public function offsetGetWrapsSimpleArrayProperties() - { - /** @var \Iterator $arrayIterator */ - $arrayIterator = $this->accessibleObject['someArray']; - $this->assertInstanceOf(IterableAccessibleObject::class, $arrayIterator); - $firstArrayValue = $arrayIterator->current(); - $firstArrayKey = $arrayIterator->key(); - $arrayIterator->next(); - $secondArrayValue = $arrayIterator->current(); - $secondArrayKey = $arrayIterator->key(); - $this->assertSame('string', $firstArrayKey); - $this->assertSame('neos', $secondArrayKey); - $this->assertSame('Foo', $firstArrayValue); - $this->assertSame('rocks', $secondArrayValue); - } - - /** - * @test - */ - public function offsetGetReturnsDateTimeProperties() - { - /** @var \DateTimeInterface $date */ - $date = $this->accessibleObject['someDate']; - $this->assertInstanceOf(\DateTimeInterface::class, $date); - $this->assertSame('13.12.1980', $date->format('d.m.Y')); - } - - /** - * @test - */ - public function offsetGetWrapsArraySubObjects() - { - /** @var \Iterator $subObjectsIterator */ - $subObjectsIterator = $this->accessibleObject['someSubObjectsArray']; - $this->assertInstanceOf(IterableAccessibleObject::class, $subObjectsIterator); - $firstSubObject = $subObjectsIterator->current(); - $subObjectsIterator->next(); - $secondSubObject = $subObjectsIterator->current(); - $this->assertInstanceOf(AccessibleObject::class, $firstSubObject); - $this->assertInstanceOf(AccessibleObject::class, $secondSubObject); - $this->assertSame('Foo nested a', $firstSubObject['someString']); - $this->assertSame('Foo nested b', $secondSubObject['someString']); - } - - /** - * @test - */ - public function offsetGetWrapsIterableSubObjects() - { - /** @var \Iterator $subObjectsIterator */ - $subObjectsIterator = $this->accessibleObject['someSubObjectsIterator']; - $this->assertInstanceOf(IterableAccessibleObject::class, $subObjectsIterator); - $firstSubObject = $subObjectsIterator->current(); - $subObjectsIterator->next(); - $secondSubObject = $subObjectsIterator->current(); - $this->assertInstanceOf(AccessibleObject::class, $firstSubObject); - $this->assertInstanceOf(AccessibleObject::class, $secondSubObject); - $this->assertSame('Foo nested a', $firstSubObject['someString']); - $this->assertSame('Foo nested b', $secondSubObject['someString']); - } - - /** - * @test - */ - public function offsetGetWrapsSubObjects() - { - $this->assertInstanceOf(AccessibleObject::class, $this->accessibleObject['someSubObject']); - $this->assertInstanceOf(AccessibleObject::class, $this->accessibleObject['someSubObject']['someSubObject']); - $this->assertSame('Foo nested nested', $this->accessibleObject['someSubObject']['someSubObject']['someString']); - } - - /** - * @test - * @expectedException \RuntimeException - */ - public function offsetSetThrowsException() - { - $this->accessibleObject['someString'] = 'This must not be possible'; - } - - /** - * @test - * @expectedException \RuntimeException - */ - public function offsetUnsetThrowsException() - { - unset($this->accessibleObject['someString']); - } - - /** - * @test - */ - public function toStringReturnsCastedObject() - { - $this->assertSame('ExampleObject (string-casted)', (string)$this->accessibleObject); - } -} \ No newline at end of file diff --git a/Tests/Unit/Fixtures/ExampleObject.php b/Tests/Unit/Fixtures/ExampleObject.php deleted file mode 100644 index 4406036..0000000 --- a/Tests/Unit/Fixtures/ExampleObject.php +++ /dev/null @@ -1,66 +0,0 @@ -string = $string; - } - - public function getSomeString() - { - return $this->string; - } - - public function getSomeArray() - { - return ['string' => $this->string, 'neos' => 'rocks']; - } - - public function isFoo() - { - return true; - } - - public function hasBar() - { - return false; - } - - public function getSomeDate() - { - return new \DateTimeImmutable('1980-12-13'); - } - - public function getSomeSubObject() - { - return new self($this->string . ' nested'); - } - - public function getSomeSubObjectsArray() - { - return [ - new self($this->string . ' nested a'), - new self($this->string . ' nested b') - ]; - } - - public function getSomeSubObjectsIterator() - { - return new \ArrayIterator($this->getSomeSubObjectsArray()); - } - - public function __toString() - { - return 'ExampleObject (string-casted)'; - } -} \ No newline at end of file diff --git a/Tests/Unit/Fixtures/InvalidExampleType.php b/Tests/Unit/Fixtures/InvalidExampleType.php deleted file mode 100644 index 015f6f7..0000000 --- a/Tests/Unit/Fixtures/InvalidExampleType.php +++ /dev/null @@ -1,29 +0,0 @@ - 'InvalidExampleType', - 'fields' => [ - 'someString' => ['type' => Type::string()], - // for circular dependencies fields must be declared as callback, see https://webonyx.github.io/graphql-php/type-system/object-types/#recurring-and-circular-types - 'selfReference' => ['type' => $typeResolver->get(self::class)], - ], - ]); - } - -} diff --git a/Tests/Unit/Fixtures/ValidExampleType.php b/Tests/Unit/Fixtures/ValidExampleType.php deleted file mode 100644 index c20cdb1..0000000 --- a/Tests/Unit/Fixtures/ValidExampleType.php +++ /dev/null @@ -1,30 +0,0 @@ - 'ValidExampleType', - 'fields' => function() use ($typeResolver) { - return [ - 'someString' => ['type' => Type::string()], - 'selfReference' => ['type' => $typeResolver->get(self::class)], - ]; - } - ]); - } - -} diff --git a/Tests/Unit/GraphQLContextTest.php b/Tests/Unit/GraphQLContextTest.php deleted file mode 100644 index 0cfe3e9..0000000 --- a/Tests/Unit/GraphQLContextTest.php +++ /dev/null @@ -1,34 +0,0 @@ -mockHttpRequest = $this->getMockBuilder(Request::class)->disableOriginalConstructor()->getMock(); - $this->graphQLContext = new GraphQLContext($this->mockHttpRequest); - } - - /** - * @test - */ - public function getHttpRequestReturnsTheSpecifiedRequestInstance() - { - $this->assertSame($this->mockHttpRequest, $this->graphQLContext->getHttpRequest()); - } -} \ No newline at end of file diff --git a/Tests/Unit/Http/HttpOptionsComponentTest.php b/Tests/Unit/Http/HttpOptionsComponentTest.php deleted file mode 100644 index d8bbfc7..0000000 --- a/Tests/Unit/Http/HttpOptionsComponentTest.php +++ /dev/null @@ -1,111 +0,0 @@ -httpOptionsComponent = new HttpOptionsComponent(); - - $this->mockComponentContext = $this->getMockBuilder(ComponentContext::class)->disableOriginalConstructor()->getMock(); - - $this->mockHttpRequest = $this->getMockBuilder(Request::class)->disableOriginalConstructor()->getMock(); - $this->mockComponentContext->expects($this->any())->method('getHttpRequest')->will($this->returnValue($this->mockHttpRequest)); - - $this->mockHttpResponse = $this->getMockBuilder(Response::class)->disableOriginalConstructor()->getMock(); - $this->mockComponentContext->expects($this->any())->method('getHttpResponse')->will($this->returnValue($this->mockHttpResponse)); - } - - /** - * @test - */ - public function handleSkipsNonOptionRequests() - { - $mockGraphQLEndpoints = [ - 'existing-endpoint' => ['querySchema' => 'Foo'], - ]; - $this->inject($this->httpOptionsComponent, 'endpoints', $mockGraphQLEndpoints); - $this->mockHttpRequest->expects($this->atLeastOnce())->method('getMethod')->will($this->returnValue('GET')); - $this->mockHttpRequest->expects($this->never())->method('getRelativePath'); - - $this->mockHttpResponse->expects($this->never())->method('setHeader'); - $this->mockComponentContext->expects($this->never())->method('setParameter'); - $this->httpOptionsComponent->handle($this->mockComponentContext); - } - - /** - * @test - */ - public function handleSkipsOptionsRequestsThatDontMatchConfiguredEndpoints() - { - $mockGraphQLEndpoints = [ - 'existing-endpoint' => ['querySchema' => 'Foo'], - ]; - $this->inject($this->httpOptionsComponent, 'endpoints', $mockGraphQLEndpoints); - $this->mockHttpRequest->expects($this->atLeastOnce())->method('getMethod')->will($this->returnValue('OPTIONS')); - $this->mockHttpRequest->expects($this->atLeastOnce())->method('getRelativePath')->will($this->returnValue('non-existing-endpoint')); - - $this->mockHttpResponse->expects($this->never())->method('setHeader'); - $this->mockComponentContext->expects($this->never())->method('setParameter'); - $this->httpOptionsComponent->handle($this->mockComponentContext); - } - - /** - * @test - */ - public function handleSetsAllowHeaderForMatchingOptionsRequests() - { - $mockGraphQLEndpoints = [ - 'existing-endpoint' => ['querySchema' => 'Foo'], - ]; - $this->inject($this->httpOptionsComponent, 'endpoints', $mockGraphQLEndpoints); - $this->mockHttpRequest->expects($this->atLeastOnce())->method('getMethod')->will($this->returnValue('OPTIONS')); - $this->mockHttpRequest->expects($this->atLeastOnce())->method('getRelativePath')->will($this->returnValue('existing-endpoint')); - - $this->mockHttpResponse->expects($this->exactly(1))->method('setHeader')->with('Allow', 'GET, POST'); - $this->httpOptionsComponent->handle($this->mockComponentContext); - } - - /** - * @test - */ - public function handleCancelsComponentChainForMatchingOptionsRequests() - { - $mockGraphQLEndpoints = [ - 'existing-endpoint' => ['querySchema' => 'Foo'], - ]; - $this->inject($this->httpOptionsComponent, 'endpoints', $mockGraphQLEndpoints); - $this->mockHttpRequest->expects($this->atLeastOnce())->method('getMethod')->will($this->returnValue('OPTIONS')); - $this->mockHttpRequest->expects($this->atLeastOnce())->method('getRelativePath')->will($this->returnValue('existing-endpoint')); - - $this->mockComponentContext->expects($this->exactly(1))->method('setParameter')->with(ComponentChain::class, 'cancel', true); - $this->httpOptionsComponent->handle($this->mockComponentContext); - } -} \ No newline at end of file diff --git a/Tests/Unit/IterableAccessibleObjectTest.php b/Tests/Unit/IterableAccessibleObjectTest.php deleted file mode 100644 index 5b1872c..0000000 --- a/Tests/Unit/IterableAccessibleObjectTest.php +++ /dev/null @@ -1,86 +0,0 @@ -iterableAccessibleObject = new IterableAccessibleObject([ - new ExampleObject('Foo'), - new ExampleObject('Bar') - ]); - } - - /** - * @test - * @expectedException \InvalidArgumentException - */ - public function constructorThrowsExceptionWhenPassedNull() - { - new IterableAccessibleObject(null); - } - - /** - * @test - * @expectedException \InvalidArgumentException - */ - public function constructorThrowsExceptionWhenPassedNonIterableObject() - { - /** @noinspection PhpParamsInspection */ - new IterableAccessibleObject(new \stdClass()); - } - - /** - * @test - */ - public function constructorConvertsArraysToArrayIterator() - { - $someArray = ['foo' => 'Foo', 'bar' => 'Bar']; - $iterableAccessibleObject = new IterableAccessibleObject($someArray); - $this->assertInstanceOf(\ArrayIterator::class, $iterableAccessibleObject->getIterator()); - $this->assertSame($someArray, iterator_to_array($iterableAccessibleObject->getIterator())); - } - - /** - * @test - */ - public function getIteratorReturnsTheUnalteredInnerIterator() - { - $someIterator = new \ArrayIterator(['foo' => 'Foo', 'bar' => 'Bar']); - $iterableAccessibleObject = new IterableAccessibleObject($someIterator); - $this->assertSame($someIterator, $iterableAccessibleObject->getIterator()); - } - - /** - * @test - */ - public function currentObjectElementsAreWrapped() - { - $this->assertInstanceOf(AccessibleObject::class, $this->iterableAccessibleObject->current()); - $this->assertSame('Foo', $this->iterableAccessibleObject->current()['someString']); - } - - /** - * @test - */ - public function currentScalarElementsAreNotWrapped() - { - $arrayProperty = ['foo' => 'Foo', 'bar' => 'Bar']; - $iterableAccessibleObject = new IterableAccessibleObject([$arrayProperty]); - - $this->assertSame($arrayProperty, $iterableAccessibleObject->current()); - } -} \ No newline at end of file diff --git a/Tests/Unit/TypeResolverTest.php b/Tests/Unit/TypeResolverTest.php deleted file mode 100644 index a293f3a..0000000 --- a/Tests/Unit/TypeResolverTest.php +++ /dev/null @@ -1,67 +0,0 @@ -typeResolver = new TypeResolver(); - } - - /** - * @test - * @expectedException \InvalidArgumentException - */ - public function getThrowsExceptionIfTypeClassNameIsNoString() - { - $this->typeResolver->get(123); - } - - /** - * @test - * @expectedException \InvalidArgumentException - */ - public function getThrowsExceptionIfTypeClassNameIsNoValidTypeDefinition() - { - $this->typeResolver->get('stdClass'); - } - - /** - * @test - */ - public function getThrowsExceptionIfTypeHasCircularDependenciesWithoutCallbacks() - { - $this->expectException(\RuntimeException::class); - $this->typeResolver->get(InvalidExampleType::class); - } - - /** - * @test - */ - public function getSupportsRecursiveTypes() - { - $exampleType = $this->typeResolver->get(ValidExampleType::class); - $this->assertSame('ValidExampleType', $exampleType->name); - } - - /** - * @test - */ - public function getReturnsTheSameInstancePerType() - { - $exampleType1 = $this->typeResolver->get(ValidExampleType::class); - $exampleType2 = $this->typeResolver->get(ValidExampleType::class); - $this->assertSame($exampleType1, $exampleType2); - } -} diff --git a/composer.json b/composer.json index 0f7f00d..50526e1 100644 --- a/composer.json +++ b/composer.json @@ -4,19 +4,18 @@ "name": "wwwision/graphql", "license": "MIT", "require": { - "neos/flow": "^7.0", - "webonyx/graphql-php": "^0.13.0" + "neos/flow": "^7.3 || ^8.0 || ^8.1 || 8.2 || ^8.3", + "webonyx/graphql-php": "^15", + "psr/http-message": "^2" + }, + "require-dev": { + "roave/security-advisories": "dev-latest" }, "autoload": { "psr-4": { "Wwwision\\GraphQL\\": "Classes" } }, - "autoload-dev": { - "psr-4": { - "Wwwision\\GraphQL\\Tests\\": "Tests" - } - }, "extra": { "neos": { "package-key": "Wwwision.GraphQL" diff --git a/playground.png b/playground.png deleted file mode 100644 index f7942b9..0000000 Binary files a/playground.png and /dev/null differ