• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

api-platform / core / 14635100171

24 Apr 2025 06:39AM UTC coverage: 8.271% (+0.02%) from 8.252%
14635100171

Pull #6904

github

web-flow
Merge c9cefd82e into a3e5e53ea
Pull Request #6904: feat(graphql): added support for graphql subscriptions to work for actions

0 of 73 new or added lines in 3 files covered. (0.0%)

1999 existing lines in 144 files now uncovered.

13129 of 158728 relevant lines covered (8.27%)

13.6 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

83.1
/src/GraphQl/Action/EntrypointAction.php
1
<?php
2

3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <dunglas@gmail.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
declare(strict_types=1);
13

14
namespace ApiPlatform\GraphQl\Action;
15

16
use ApiPlatform\GraphQl\Error\ErrorHandlerInterface;
17
use ApiPlatform\GraphQl\ExecutorInterface;
18
use ApiPlatform\GraphQl\Type\SchemaBuilderInterface;
19
use ApiPlatform\Metadata\Util\ContentNegotiationTrait;
20
use GraphQL\Error\DebugFlag;
21
use GraphQL\Error\Error;
22
use GraphQL\Executor\ExecutionResult;
23
use Negotiation\Negotiator;
24
use Symfony\Component\HttpFoundation\JsonResponse;
25
use Symfony\Component\HttpFoundation\Request;
26
use Symfony\Component\HttpFoundation\Response;
27
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
28
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
29

30
/**
31
 * GraphQL API entrypoint.
32
 *
33
 * @author Alan Poulain <contact@alanpoulain.eu>
34
 */
35
final class EntrypointAction
36
{
37
    use ContentNegotiationTrait;
38
    private int $debug;
39

40
    public function __construct(
41
        private readonly SchemaBuilderInterface $schemaBuilder,
42
        private readonly ExecutorInterface $executor,
43
        private readonly ?GraphiQlAction $graphiQlAction,
44
        private readonly ?GraphQlPlaygroundAction $graphQlPlaygroundAction,
45
        private readonly NormalizerInterface $normalizer,
46
        private readonly ErrorHandlerInterface $errorHandler,
47
        bool $debug = false,
48
        private readonly bool $graphiqlEnabled = false,
49
        private readonly bool $graphQlPlaygroundEnabled = false,
50
        private readonly ?string $defaultIde = null,
51
        ?Negotiator $negotiator = null,
52
    ) {
53
        $this->debug = $debug ? DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE : DebugFlag::NONE;
160✔
54
        $this->negotiator = $negotiator ?? new Negotiator();
160✔
55
    }
56

57
    public function __invoke(Request $request): Response
58
    {
59
        $formats = ['json' => ['application/json'], 'html' => ['text/html']];
160✔
60
        $format = $this->getRequestFormat($request, $formats, false);
160✔
61

62
        try {
63
            if ($request->isMethod('GET') && 'html' === $format) {
160✔
UNCOV
64
                if ('graphiql' === $this->defaultIde && $this->graphiqlEnabled && $this->graphiQlAction) {
1✔
UNCOV
65
                    return ($this->graphiQlAction)($request);
1✔
66
                }
67

68
                if ('graphql-playground' === $this->defaultIde && $this->graphQlPlaygroundEnabled && $this->graphQlPlaygroundAction) {
×
69
                    return ($this->graphQlPlaygroundAction)($request);
×
70
                }
71
            }
72

73
            [$query, $operationName, $variables] = $this->parseRequest($request);
159✔
74
            if (null === $query) {
159✔
UNCOV
75
                throw new BadRequestHttpException('GraphQL query is not valid.');
1✔
76
            }
77

78
            $executionResult = $this->executor
158✔
79
                ->executeQuery($this->schemaBuilder->getSchema(), $query, null, null, $variables, $operationName)
158✔
80
                ->setErrorsHandler($this->errorHandler)
158✔
81
                ->setErrorFormatter($this->normalizer->normalize(...));
158✔
UNCOV
82
        } catch (\Exception $exception) {
1✔
UNCOV
83
            $executionResult = (new ExecutionResult(null, [new Error($exception->getMessage(), null, null, [], null, $exception)]))
1✔
UNCOV
84
                ->setErrorsHandler($this->errorHandler)
1✔
UNCOV
85
                ->setErrorFormatter($this->normalizer->normalize(...));
1✔
86
        }
87

88
        return new JsonResponse($executionResult->toArray($this->debug));
159✔
89
    }
90

91
    /**
92
     * @throws BadRequestHttpException
93
     *
94
     * @return array{0: array<string, mixed>|null, 1: string, 2: array<string, mixed>}
95
     */
96
    private function parseRequest(Request $request): array
97
    {
98
        $queryParameters = $request->query->all();
159✔
99
        $query = $queryParameters['query'] ?? null;
159✔
100
        $operationName = $queryParameters['operationName'] ?? null;
159✔
101
        if ($variables = $queryParameters['variables'] ?? []) {
159✔
UNCOV
102
            $variables = $this->decodeVariables($variables);
3✔
103
        }
104

105
        if (!$request->isMethod('POST')) {
159✔
UNCOV
106
            return [$query, $operationName, $variables];
131✔
107
        }
108

109
        $contentType = method_exists(Request::class, 'getContentTypeFormat') ? $request->getContentTypeFormat() : $request->getContentType();
28✔
110
        if ('json' === $contentType) {
28✔
111
            return $this->parseData($query, $operationName, $variables, $request->getContent());
26✔
112
        }
113

UNCOV
114
        if ('graphql' === $contentType) {
2✔
115
            $query = $request->getContent();
×
116
        }
117

UNCOV
118
        if (\in_array($contentType, ['multipart', 'form'], true)) {
2✔
UNCOV
119
            return $this->parseMultipartRequest($query, $operationName, $variables, $request->request->all(), $request->files->all());
2✔
120
        }
121

122
        return [$query, $operationName, $variables];
×
123
    }
124

125
    /**
126
     * @param array<string,mixed> $variables
127
     *
128
     * @throws BadRequestHttpException
129
     *
130
     * @return array{0: array<string, mixed>, 1: string, 2: array<string, mixed>}
131
     */
132
    private function parseData(?string $query, ?string $operationName, array $variables, string $jsonContent): array
133
    {
134
        if (!\is_array($data = json_decode($jsonContent, true, 512, \JSON_ERROR_NONE))) {
28✔
135
            throw new BadRequestHttpException('GraphQL data is not valid JSON.');
×
136
        }
137

138
        if (isset($data['query'])) {
28✔
139
            $query = $data['query'];
28✔
140
        }
141

142
        if (isset($data['variables'])) {
28✔
143
            $variables = \is_array($data['variables']) ? $data['variables'] : $this->decodeVariables($data['variables']);
18✔
144
        }
145

146
        if (isset($data['operationName'])) {
28✔
147
            $operationName = $data['operationName'];
×
148
        }
149

150
        return [$query, $operationName, $variables];
28✔
151
    }
152

153
    /**
154
     * @param array<string,mixed> $variables
155
     * @param array<string,mixed> $bodyParameters
156
     * @param array<string,mixed> $files
157
     *
158
     * @throws BadRequestHttpException
159
     *
160
     * @return array{0: array<string, mixed>, 1: string, 2: array<string, mixed>}
161
     */
162
    private function parseMultipartRequest(?string $query, ?string $operationName, array $variables, array $bodyParameters, array $files): array
163
    {
UNCOV
164
        if ((null === $operations = $bodyParameters['operations'] ?? null) || (null === $map = $bodyParameters['map'] ?? null)) {
2✔
165
            throw new BadRequestHttpException('GraphQL multipart request does not respect the specification.');
×
166
        }
167

UNCOV
168
        [$query, $operationName, $variables] = $this->parseData($query, $operationName, $variables, $operations);
2✔
169

170
        /** @var string $map */
UNCOV
171
        if (!\is_array($decodedMap = json_decode($map, true, 512, \JSON_ERROR_NONE))) {
2✔
172
            throw new BadRequestHttpException('GraphQL multipart request map is not valid JSON.');
×
173
        }
174

UNCOV
175
        $variables = $this->applyMapToVariables($decodedMap, $variables, $files);
2✔
176

UNCOV
177
        return [$query, $operationName, $variables];
2✔
178
    }
179

180
    /**
181
     * @param array<string,mixed> $map
182
     * @param array<string,mixed> $variables
183
     * @param array<string,mixed> $files
184
     *
185
     * @throws BadRequestHttpException
186
     */
187
    private function applyMapToVariables(array $map, array $variables, array $files): array
188
    {
UNCOV
189
        foreach ($map as $key => $value) {
2✔
UNCOV
190
            if (null === $file = $files[$key] ?? null) {
2✔
191
                throw new BadRequestHttpException('GraphQL multipart request file has not been sent correctly.');
×
192
            }
193

UNCOV
194
            foreach ($value as $mapValue) {
2✔
UNCOV
195
                $path = explode('.', (string) $mapValue);
2✔
196

UNCOV
197
                if ('variables' !== $path[0]) {
2✔
198
                    throw new BadRequestHttpException('GraphQL multipart request path in map is invalid.');
×
199
                }
200

UNCOV
201
                unset($path[0]);
2✔
202

UNCOV
203
                $mapPathExistsInVariables = array_reduce($path, static fn (array $inVariables, string $pathElement) => \array_key_exists($pathElement, $inVariables) ? $inVariables[$pathElement] : false, $variables);
2✔
204

UNCOV
205
                if (false === $mapPathExistsInVariables) {
2✔
206
                    throw new BadRequestHttpException('GraphQL multipart request path in map does not match the variables.');
×
207
                }
208

UNCOV
209
                $variableFileValue = &$variables;
2✔
UNCOV
210
                foreach ($path as $pathValue) {
2✔
UNCOV
211
                    $variableFileValue = &$variableFileValue[$pathValue];
2✔
212
                }
UNCOV
213
                $variableFileValue = $file;
2✔
214
            }
215
        }
216

UNCOV
217
        return $variables;
2✔
218
    }
219

220
    /**
221
     * @throws BadRequestHttpException
222
     *
223
     * @return array<string, mixed>
224
     */
225
    private function decodeVariables(string $variables): array
226
    {
UNCOV
227
        if (!\is_array($decoded = json_decode($variables, true, 512, \JSON_ERROR_NONE))) {
3✔
228
            throw new BadRequestHttpException('GraphQL variables are not valid JSON.');
×
229
        }
230

UNCOV
231
        return $decoded;
3✔
232
    }
233
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc