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

api-platform / core / 18757138067

23 Oct 2025 05:45PM UTC coverage: 0.0% (-24.5%) from 24.544%
18757138067

Pull #7481

github

web-flow
Merge 860b858c4 into 15ea6d8f7
Pull Request #7481: fix(jsonSchema): Declare properties as required for Output

0 of 1 new or added line in 1 file covered. (0.0%)

13853 existing lines in 452 files now uncovered.

0 of 56436 relevant lines covered (0.0%)

0.0 hits per line

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

0.0
/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 NormalizerInterface $normalizer,
45
        private readonly ErrorHandlerInterface $errorHandler,
46
        bool $debug = false,
47
        private readonly bool $graphiqlEnabled = false,
48
        private readonly ?string $defaultIde = null,
49
        ?Negotiator $negotiator = null,
50
    ) {
UNCOV
51
        $this->debug = $debug ? DebugFlag::INCLUDE_DEBUG_MESSAGE | DebugFlag::INCLUDE_TRACE : DebugFlag::NONE;
×
UNCOV
52
        $this->negotiator = $negotiator ?? new Negotiator();
×
53
    }
54

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

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

UNCOV
67
            [$query, $operationName, $variables] = $this->parseRequest($request);
×
UNCOV
68
            if (null === $query) {
×
69
                throw new BadRequestHttpException('GraphQL query is not valid.');
×
70
            }
71

UNCOV
72
            $executionResult = $this->executor
×
UNCOV
73
                ->executeQuery($this->schemaBuilder->getSchema(), $query, null, null, $variables, $operationName)
×
UNCOV
74
                ->setErrorsHandler($this->errorHandler)
×
UNCOV
75
                ->setErrorFormatter($this->normalizer->normalize(...));
×
76
        } catch (\Exception $exception) {
×
77
            $executionResult = (new ExecutionResult(null, [new Error($exception->getMessage(), null, null, [], null, $exception)]))
×
78
                ->setErrorsHandler($this->errorHandler)
×
79
                ->setErrorFormatter($this->normalizer->normalize(...));
×
80
        }
81

UNCOV
82
        return new JsonResponse($executionResult->toArray($this->debug));
×
83
    }
84

85
    /**
86
     * @throws BadRequestHttpException
87
     *
88
     * @return array{0: array<string, mixed>|null, 1: string, 2: array<string, mixed>}
89
     */
90
    private function parseRequest(Request $request): array
91
    {
UNCOV
92
        $queryParameters = $request->query->all();
×
UNCOV
93
        $query = $queryParameters['query'] ?? null;
×
UNCOV
94
        $operationName = $queryParameters['operationName'] ?? null;
×
UNCOV
95
        if ($variables = $queryParameters['variables'] ?? []) {
×
96
            $variables = $this->decodeVariables($variables);
×
97
        }
98

UNCOV
99
        if (!$request->isMethod('POST')) {
×
100
            return [$query, $operationName, $variables];
×
101
        }
102

UNCOV
103
        $contentType = method_exists(Request::class, 'getContentTypeFormat') ? $request->getContentTypeFormat() : $request->getContentType();
×
UNCOV
104
        if ('json' === $contentType) {
×
UNCOV
105
            return $this->parseData($query, $operationName, $variables, $request->getContent());
×
106
        }
107

108
        if ('graphql' === $contentType) {
×
109
            $query = $request->getContent();
×
110
        }
111

112
        if (\in_array($contentType, ['multipart', 'form'], true)) {
×
113
            return $this->parseMultipartRequest($query, $operationName, $variables, $request->request->all(), $request->files->all());
×
114
        }
115

116
        return [$query, $operationName, $variables];
×
117
    }
118

119
    /**
120
     * @param array<string,mixed> $variables
121
     *
122
     * @throws BadRequestHttpException
123
     *
124
     * @return array{0: array<string, mixed>, 1: string, 2: array<string, mixed>}
125
     */
126
    private function parseData(?string $query, ?string $operationName, array $variables, string $jsonContent): array
127
    {
UNCOV
128
        if (!\is_array($data = json_decode($jsonContent, true, 512, \JSON_ERROR_NONE))) {
×
129
            throw new BadRequestHttpException('GraphQL data is not valid JSON.');
×
130
        }
131

UNCOV
132
        if (isset($data['query'])) {
×
UNCOV
133
            $query = $data['query'];
×
134
        }
135

UNCOV
136
        if (isset($data['variables'])) {
×
UNCOV
137
            $variables = \is_array($data['variables']) ? $data['variables'] : $this->decodeVariables($data['variables']);
×
138
        }
139

UNCOV
140
        if (isset($data['operationName'])) {
×
141
            $operationName = $data['operationName'];
×
142
        }
143

UNCOV
144
        return [$query, $operationName, $variables];
×
145
    }
146

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

162
        [$query, $operationName, $variables] = $this->parseData($query, $operationName, $variables, $operations);
×
163

164
        /** @var string $map */
165
        if (!\is_array($decodedMap = json_decode($map, true, 512, \JSON_ERROR_NONE))) {
×
166
            throw new BadRequestHttpException('GraphQL multipart request map is not valid JSON.');
×
167
        }
168

169
        $variables = $this->applyMapToVariables($decodedMap, $variables, $files);
×
170

171
        return [$query, $operationName, $variables];
×
172
    }
173

174
    /**
175
     * @param array<string,mixed> $map
176
     * @param array<string,mixed> $variables
177
     * @param array<string,mixed> $files
178
     *
179
     * @throws BadRequestHttpException
180
     */
181
    private function applyMapToVariables(array $map, array $variables, array $files): array
182
    {
183
        foreach ($map as $key => $value) {
×
184
            if (null === $file = $files[$key] ?? null) {
×
185
                throw new BadRequestHttpException('GraphQL multipart request file has not been sent correctly.');
×
186
            }
187

188
            foreach ($value as $mapValue) {
×
189
                $path = explode('.', (string) $mapValue);
×
190

191
                if ('variables' !== $path[0]) {
×
192
                    throw new BadRequestHttpException('GraphQL multipart request path in map is invalid.');
×
193
                }
194

195
                unset($path[0]);
×
196

197
                $mapPathExistsInVariables = array_reduce($path, static fn (array $inVariables, string $pathElement) => \array_key_exists($pathElement, $inVariables) ? $inVariables[$pathElement] : false, $variables);
×
198

199
                if (false === $mapPathExistsInVariables) {
×
200
                    throw new BadRequestHttpException('GraphQL multipart request path in map does not match the variables.');
×
201
                }
202

203
                $variableFileValue = &$variables;
×
204
                foreach ($path as $pathValue) {
×
205
                    $variableFileValue = &$variableFileValue[$pathValue];
×
206
                }
207
                $variableFileValue = $file;
×
208
            }
209
        }
210

211
        return $variables;
×
212
    }
213

214
    /**
215
     * @throws BadRequestHttpException
216
     *
217
     * @return array<string, mixed>
218
     */
219
    private function decodeVariables(string $variables): array
220
    {
221
        if (!\is_array($decoded = json_decode($variables, true, 512, \JSON_ERROR_NONE))) {
×
222
            throw new BadRequestHttpException('GraphQL variables are not valid JSON.');
×
223
        }
224

225
        return $decoded;
×
226
    }
227
}
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