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

api-platform / schema-generator / 13569276979

27 Feb 2025 02:56PM UTC coverage: 79.019%. First build
13569276979

Pull #430

github

web-flow
Merge fa0bfaf80 into c50ccf54d
Pull Request #430: operation openapi property support

47 of 60 new or added lines in 1 file covered. (78.33%)

1789 of 2264 relevant lines covered (79.02%)

16.16 hits per line

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

87.83
/src/AttributeGenerator/ApiPlatformCoreAttributeGenerator.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\SchemaGenerator\AttributeGenerator;
15

16
use ApiPlatform\Core\Annotation\ApiProperty as OldApiProperty;
17
use ApiPlatform\Core\Annotation\ApiResource as OldApiResource;
18
use ApiPlatform\Metadata\ApiProperty;
19
use ApiPlatform\Metadata\ApiResource;
20
use ApiPlatform\Metadata\Delete;
21
use ApiPlatform\Metadata\Get;
22
use ApiPlatform\Metadata\GetCollection;
23
use ApiPlatform\Metadata\Patch;
24
use ApiPlatform\Metadata\Post;
25
use ApiPlatform\Metadata\Put;
26
use ApiPlatform\OpenApi\Model\Operation;
27
use ApiPlatform\OpenApi\Model\Parameter;
28
use ApiPlatform\OpenApi\Model\Response;
29
use ApiPlatform\SchemaGenerator\Model\Attribute;
30
use ApiPlatform\SchemaGenerator\Model\Class_;
31
use ApiPlatform\SchemaGenerator\Model\Property;
32
use ApiPlatform\SchemaGenerator\Model\Use_;
33
use Nette\PhpGenerator\Literal;
34
use Symfony\Component\OptionsResolver\OptionsResolver;
35

36
/**
37
 * Generates API Platform core attributes.
38
 *
39
 * @author Kévin Dunglas <dunglas@gmail.com>
40
 *
41
 * @see    https://github.com/api-platform/core
42
 */
43
final class ApiPlatformCoreAttributeGenerator extends AbstractAttributeGenerator
44
{
45
    /**
46
     * Hints for not typed array parameters.
47
     */
48
    private const PRAMETER_TYPE_HINTS = [
49
        Operation::class => [
50
            'responses' => Response::class.'[]',
51
            'parameters' => Parameter::class.'[]',
52
        ],
53
    ];
54

55
    /**
56
     * @var array<class-string, array<string, string|null>>
57
     */
58
    private static array $parameterTypes = [];
59

60
    public function generateClassAttributes(Class_ $class): array
61
    {
62
        if ($class->hasChild || $class->isEnum()) {
27✔
63
            return [];
7✔
64
        }
65

66
        $arguments = [];
25✔
67
        if ($class->name() !== $localName = $class->shortName()) {
25✔
68
            $arguments['shortName'] = $localName;
3✔
69
        }
70

71
        if ($class->rdfType()) {
25✔
72
            if ($this->config['apiPlatformOldAttributes']) {
25✔
73
                $arguments['iri'] = $class->rdfType();
2✔
74
            } else {
75
                $arguments['types'] = [$class->rdfType()];
23✔
76
            }
77
        }
78

79
        if ($class->operations) {
25✔
80
            if ($this->config['apiPlatformOldAttributes']) {
4✔
81
                $operations = $this->validateClassOperations($class->operations);
1✔
82
                foreach ($operations as $operationTarget => $targetOperations) {
1✔
83
                    $targetArguments = [];
1✔
84
                    foreach ($targetOperations ?? [] as $method => $methodConfig) {
1✔
85
                        $methodArguments = [];
1✔
86
                        if (!is_iterable($methodConfig)) {
1✔
87
                            continue;
×
88
                        }
89
                        foreach ($methodConfig as $key => $value) {
1✔
90
                            $methodArguments[$key] = $value;
1✔
91
                        }
92
                        $targetArguments[$method] = $methodArguments;
1✔
93
                    }
94
                    $arguments[\sprintf('%sOperations', $operationTarget)] = $targetArguments;
1✔
95
                }
96
            } else {
97
                $arguments['operations'] = [];
3✔
98
                foreach ($class->operations as $operationMetadataClass => $methodConfig) {
3✔
99
                    // https://github.com/api-platform/schema-generator/issues/405
100
                    if (\array_key_exists('class', $methodConfig ?? [])) {
3✔
101
                        /** @var string $operationMetadataClass */
102
                        $operationMetadataClass = $methodConfig['class'];
2✔
103
                        unset($methodConfig['class']);
2✔
104
                    }
105

106
                    if (\is_array($methodConfig['openapi'] ?? null)) {
3✔
107
                        $methodConfig['openapi'] = Literal::new(
1✔
108
                            'Operation',
1✔
109
                            self::extractParameters(Operation::class, $methodConfig['openapi'])
1✔
110
                        );
1✔
111
                        $class->addUse(new Use_(Operation::class));
1✔
112
                        array_walk_recursive(
1✔
113
                            self::$parameterTypes,
1✔
114
                            function (?string $type) use ($class): void {
1✔
115
                                if (null !== $type) {
1✔
116
                                    $class->addUse(new Use_(str_replace('[]', '', $type)));
1✔
117
                                }
118
                            }
1✔
119
                        );
1✔
120
                    }
121

122
                    $arguments['operations'][] = new Literal(\sprintf('new %s(...?:)',
3✔
123
                        $operationMetadataClass,
3✔
124
                    ), [$methodConfig ?? []]);
3✔
125
                }
126
            }
127
        }
128

129
        return [new Attribute('ApiResource', $arguments)];
25✔
130
    }
131

132
    /**
133
     * @param class-string $type
134
     * @param mixed[]      $values
135
     *
136
     * @return mixed[]
137
     */
138
    private static function extractParameters(string $type, array $values): array
139
    {
140
        $types = self::$parameterTypes[$type] ??=
1✔
141
            (static::PRAMETER_TYPE_HINTS[$type] ?? []) + array_reduce(
1✔
142
                (new \ReflectionClass($type))->getConstructor()?->getParameters() ?? [],
1✔
143
                static fn (array $types, \ReflectionParameter $refl): array => $types + [
1✔
144
                    $refl->getName() => $refl->getType() instanceof \ReflectionNamedType
1✔
145
                            && !$refl->getType()->isBuiltin()
1✔
146
                        ? $refl->getType()->getName()
1✔
147
                        : null,
148
                ],
1✔
149
                []
1✔
150
            );
1✔
151
        if (isset(self::$parameterTypes[$type])) {
1✔
152
            $types = self::$parameterTypes[$type];
1✔
153
        } else {
NEW
154
            $types = static::PRAMETER_TYPE_HINTS[$type] ?? [];
×
NEW
155
            $parameterRefls = (new \ReflectionClass($type))
×
NEW
156
                ->getConstructor()
×
NEW
157
                ?->getParameters() ?? [];
×
NEW
158
            foreach ($parameterRefls as $refl) {
×
NEW
159
                $paramName = $refl->getName();
×
NEW
160
                if (\array_key_exists($paramName, $types)) {
×
NEW
161
                    continue;
×
162
                }
NEW
163
                $paramType = $refl->getType();
×
NEW
164
                if ($paramType instanceof \ReflectionNamedType && !$paramType->isBuiltin()) {
×
NEW
165
                    $types[$paramName] = $paramType->getName();
×
166
                } else {
NEW
167
                    $types[$paramName] = null;
×
168
                }
169
            }
NEW
170
            self::$parameterTypes[$type] = $types;
×
171
        }
172

173
        $parameters = array_intersect_key($values, $types);
1✔
174
        foreach ($parameters as $name => $parameter) {
1✔
175
            $type = $types[$name];
1✔
176
            if (null === $type || !\is_array($parameter)) {
1✔
177
                continue;
1✔
178
            }
179
            $isArrayType = str_ends_with($type, '[]');
1✔
180
            /**
181
             * @var class-string
182
             */
183
            $type = $isArrayType ? substr($type, 0, -2) : $type;
1✔
184
            $shortName = (new \ReflectionClass($type))->getShortName();
1✔
185
            if ($isArrayType) {
1✔
186
                $parameters[$name] = [];
1✔
187
                foreach ($parameter as $key => $values) {
1✔
188
                    $parameters[$name][$key] = Literal::new(
1✔
189
                        $shortName,
1✔
190
                        self::extractParameters($type, $values)
1✔
191
                    );
1✔
192
                }
193
            } else {
194
                $parameters[$name] = Literal::new(
1✔
195
                    $shortName,
1✔
196
                    \ArrayObject::class === $type
1✔
197
                        ? [$parameter]
1✔
198
                        : self::extractParameters($type, $parameter)
1✔
199
                );
1✔
200
            }
201
        }
202

203
        return $parameters;
1✔
204
    }
205

206
    /**
207
     * Verifies that the operations' config is valid.
208
     *
209
     * @template T of array
210
     *
211
     * @param T $operations
212
     *
213
     * @return T
214
     */
215
    private function validateClassOperations(array $operations): array
216
    {
217
        $resolver = new OptionsResolver();
1✔
218
        $resolver->setDefaults(['item' => [], 'collection' => []]);
1✔
219
        $resolver->setAllowedTypes('item', 'array');
1✔
220
        $resolver->setAllowedTypes('collection', 'array');
1✔
221

222
        /**
223
         * @var T $operations
224
         */
225
        $operations = $resolver->resolve($operations);
1✔
226

227
        return $operations;
1✔
228
    }
229

230
    public function generatePropertyAttributes(Property $property, string $className): array
231
    {
232
        $arguments = [];
22✔
233

234
        if ($property->rdfType()) {
22✔
235
            if ($this->config['apiPlatformOldAttributes']) {
21✔
236
                $arguments['iri'] = $property->rdfType();
1✔
237
            } else {
238
                $arguments['types'] = [$property->rdfType()];
20✔
239
            }
240
        }
241

242
        return $property->isCustom ? [] : [new Attribute('ApiProperty', $arguments)];
22✔
243
    }
244

245
    public function generateUses(Class_ $class): array
246
    {
247
        if ($this->config['apiPlatformOldAttributes']) {
20✔
248
            // @phpstan-ignore-next-line
249
            return [new Use_(OldApiResource::class), new Use_(OldApiProperty::class)];
1✔
250
        }
251

252
        return [
20✔
253
            new Use_(ApiResource::class),
20✔
254
            new Use_(ApiProperty::class),
20✔
255
            new Use_(Get::class),
20✔
256
            new Use_(Put::class),
20✔
257
            new Use_(Patch::class),
20✔
258
            new Use_(Delete::class),
20✔
259
            new Use_(GetCollection::class),
20✔
260
            new Use_(Post::class),
20✔
261
        ];
20✔
262
    }
263
}
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