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

api-platform / schema-generator / 3828622569

pending completion
3828622569

push

github

GitHub
chore: bump deps (#402)

1738 of 2198 relevant lines covered (79.07%)

16.09 hits per line

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

0.0
/src/OpenApi/ClassGenerator.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\OpenApi;
15

16
use ApiPlatform\SchemaGenerator\CardinalitiesExtractor;
17
use ApiPlatform\SchemaGenerator\ClassMutator\AnnotationsAppender;
18
use ApiPlatform\SchemaGenerator\ClassMutator\AttributeAppender;
19
use ApiPlatform\SchemaGenerator\ClassMutator\ClassIdAppender;
20
use ApiPlatform\SchemaGenerator\ClassMutator\ClassInterfaceMutator;
21
use ApiPlatform\SchemaGenerator\ClassMutator\ClassPropertiesTypehintMutator;
22
use ApiPlatform\SchemaGenerator\Model\Class_;
23
use ApiPlatform\SchemaGenerator\Model\Use_;
24
use ApiPlatform\SchemaGenerator\OpenApi\ClassMutator\EnumClassMutator as OpenApiEnumClassMutator;
25
use ApiPlatform\SchemaGenerator\OpenApi\Model\Class_ as OpenApiClass;
26
use ApiPlatform\SchemaGenerator\OpenApi\PropertyGenerator\IdPropertyGenerator;
27
use ApiPlatform\SchemaGenerator\OpenApi\PropertyGenerator\PropertyGenerator;
28
use ApiPlatform\SchemaGenerator\PhpTypeConverterInterface;
29
use ApiPlatform\SchemaGenerator\PropertyGenerator\PropertyGeneratorInterface;
30
use cebe\openapi\spec\OpenApi;
31
use cebe\openapi\spec\RequestBody;
32
use cebe\openapi\spec\Schema;
33
use Doctrine\Common\Collections\ArrayCollection;
34
use Doctrine\Common\Collections\Collection;
35
use Psr\Log\LoggerAwareTrait;
36
use Symfony\Component\String\Inflector\InflectorInterface;
37

38
use function Symfony\Component\String\u;
39

40
final class ClassGenerator
41
{
42
    use LoggerAwareTrait;
43
    use SchemaTraversalTrait;
44

45
    private InflectorInterface $inflector;
46
    private PhpTypeConverterInterface $phpTypeConverter;
47
    private PropertyGeneratorInterface $propertyGenerator;
48

49
    public function __construct(InflectorInterface $inflector, PhpTypeConverterInterface $phpTypeConverter)
50
    {
51
        $this->inflector = $inflector;
×
52
        $this->phpTypeConverter = $phpTypeConverter;
×
53
        $this->propertyGenerator = new PropertyGenerator();
×
54
    }
55

56
    /**
57
     * @param Configuration $config
58
     *
59
     * @return Class_[]
60
     */
61
    public function generate(OpenApi $openApi, array $config): array
62
    {
63
        /** @var OpenApiClass[] $classes */
64
        $classes = [];
×
65

66
        foreach ($openApi->paths as $path => $pathItem) {
×
67
            // Matches only paths like /books/{id}.
68
            // Subresources and collection-only resources are not handled yet.
69
            if (!preg_match('@^[^{}]+/{[^{}]+}/?$@', $path)) {
×
70
                continue;
×
71
            }
72

73
            $explodedPath = explode('/', rtrim($path, '/'));
×
74
            $pathResourceName = $explodedPath[\count($explodedPath) - 2];
×
75
            $collectionResourceName = $this->inflector->pluralize($this->inflector->singularize($pathResourceName)[0])[0];
×
76
            $name = $this->inflector->singularize(u($pathResourceName)->camel()->title()->toString())[0];
×
77

78
            $showOperation = $pathItem->get;
×
79
            $editOperation = $pathItem->put ?? $pathItem->patch;
×
80
            if (null === $showOperation && null === $editOperation) {
×
81
                $this->logger ? $this->logger->warning(sprintf('No get, put or patch operation found for path "%s"', $path)) : null;
×
82
                continue;
×
83
            }
84

85
            $showSchema = null;
×
86
            if ($showOperation && $showOperation->responses && null !== $responseSchema = ($showOperation->responses[200]->content['application/json']->schema ?? null)) {
×
87
                $this->logger ? $this->logger->info(sprintf('Using show schema from get operation response for "%s" resource.', $name)) : null;
×
88
                $showSchema = $responseSchema;
×
89
            }
90
            if (!$showSchema && $openApi->components && isset($openApi->components->schemas[$name])) {
×
91
                $this->logger ? $this->logger->info(sprintf('Using "%s" show schema from components.', $name)) : null;
×
92
                $showSchema = $openApi->components->schemas[$name];
×
93
            }
94
            $editSchema = null;
×
95
            if ($editOperation && $editOperation->requestBody instanceof RequestBody && null !== $requestBodySchema = ($editOperation->requestBody->content['application/json']->schema ?? null)) {
×
96
                $this->logger ? $this->logger->info(sprintf('Using edit schema from put operation request body for "%s" resource.', $name)) : null;
×
97
                $editSchema = $requestBodySchema;
×
98
            }
99
            if (null === $showSchema && null === $editSchema) {
×
100
                $this->logger ? $this->logger->warning(sprintf('No schema found for path "%s"', $path)) : null;
×
101
                continue;
×
102
            }
103

104
            $showClass = null;
×
105
            if ($showSchema instanceof Schema) {
×
106
                $showClass = $this->buildClassFromSchema($showSchema, $name, $config);
×
107
                $classes = array_merge($this->buildEnumClasses($showSchema, $showClass, $config), $classes);
×
108
            }
109
            $editClass = null;
×
110
            if ($editSchema instanceof Schema) {
×
111
                $editClass = $this->buildClassFromSchema($editSchema, $name, $config);
×
112
                $classes = array_merge($this->buildEnumClasses($editSchema, $editClass, $config), $classes);
×
113
            }
114
            $class = $showClass ?? $editClass;
×
115
            if (!$class) {
×
116
                continue;
×
117
            }
118
            if ($showClass && $editClass) {
×
119
                $class = $this->mergeClasses($showClass, $editClass);
×
120
            }
121

122
            $putOperation = $pathItem->put;
×
123
            $patchOperation = $pathItem->patch;
×
124
            $deleteOperation = $pathItem->delete;
×
125
            $pathCollection = $openApi->paths->getPath(sprintf('/%s', $collectionResourceName));
×
126
            $listOperation = $pathCollection->get ?? null;
×
127
            $createOperation = $pathCollection->post ?? null;
×
128
            $class->operations = array_merge(
×
129
                $showOperation ? ['Get' => null] : [],
×
130
                $putOperation ? ['Put' => null] : [],
×
131
                $patchOperation ? ['Patch' => null] : [],
×
132
                $deleteOperation ? ['Delete' => null] : [],
×
133
                $listOperation ? ['GetCollection' => null] : [],
×
134
                $createOperation ? ['Post' => null] : [],
×
135
            );
×
136

137
            $classes[$name] = $class;
×
138
        }
139

140
        // Second pass
141
        $useInterface = $config['useInterface'];
×
142
        $generateId = $config['id']['generate'];
×
143
        foreach ($classes as $class) {
×
144
            if ($useInterface) {
×
145
                (new ClassInterfaceMutator($config['namespaces']['interface']))($class, []);
×
146
            }
147

148
            if ($generateId) {
×
149
                (new ClassIdAppender(new IdPropertyGenerator(), $config))($class, []);
×
150
            }
151

152
            // Try to guess the references from the property names.
153
            foreach ($class->properties() as $property) {
×
154
                if ($reference = $classes[preg_replace('/Ids?$/', '', $this->inflector->singularize(u($property->name())->title()->toString())[0])] ?? null) {
×
155
                    $property->reference = $reference;
×
156
                    $property->cardinality = $property->isNullable ? CardinalitiesExtractor::CARDINALITY_0_1 : CardinalitiesExtractor::CARDINALITY_1_1;
×
157
                    if ($property->isArray()) {
×
158
                        $property->cardinality = $property->isNullable ? CardinalitiesExtractor::CARDINALITY_0_N : CardinalitiesExtractor::CARDINALITY_1_N;
×
159
                    }
160
                }
161
            }
162
        }
163

164
        // Third pass
165
        foreach ($classes as $class) {
×
166
            (new ClassPropertiesTypehintMutator($this->phpTypeConverter, $config, $classes))($class, []);
×
167

168
            // Try to guess the mapped by from the references
169
            foreach ($class->properties() as $property) {
×
170
                if ($property->reference && $property->isArray()) {
×
171
                    $mappedByName = strtolower($class->name());
×
172
                    foreach ($property->reference->properties() as $referenceProperty) {
×
173
                        if ($mappedByName === $referenceProperty->name()) {
×
174
                            $property->mappedBy = $mappedByName;
×
175
                        }
176
                    }
177
                }
178
            }
179
        }
180

181
        // Initialize annotation generators
182
        $annotationGenerators = [];
×
183
        foreach ($config['annotationGenerators'] as $annotationGenerator) {
×
184
            $generator = new $annotationGenerator($this->phpTypeConverter, $this->inflector, $config, $classes);
×
185
            if (method_exists($generator, 'setLogger')) {
×
186
                $generator->setLogger($this->logger);
×
187
            }
188

189
            $annotationGenerators[] = $generator;
×
190
        }
191

192
        // Initialize attribute generators
193
        $attributeGenerators = [];
×
194
        foreach ($config['attributeGenerators'] as $attributeGenerator) {
×
195
            $generator = new $attributeGenerator($this->phpTypeConverter, $this->inflector, $config, $classes);
×
196
            if (method_exists($generator, 'setLogger')) {
×
197
                $generator->setLogger($this->logger);
×
198
            }
199

200
            $attributeGenerators[] = $generator;
×
201
        }
202

203
        $attributeAppender = new AttributeAppender($classes, $attributeGenerators);
×
204
        foreach ($classes as $class) {
×
205
            (new AnnotationsAppender($classes, $annotationGenerators, []))($class, []);
×
206
            $attributeAppender($class, []);
×
207
        }
208
        foreach ($classes as $class) {
×
209
            $attributeAppender->appendLate($class);
×
210
        }
211

212
        return $classes;
×
213
    }
214

215
    /**
216
     * @param Configuration $config
217
     */
218
    private function buildClassFromSchema(Schema $schema, string $name, array $config): OpenApiClass
219
    {
220
        $class = new OpenApiClass($name);
×
221

222
        $class->namespace = $config['namespaces']['entity'];
×
223

224
        if ($schema->description) {
×
225
            $class->setDescription($schema->description);
×
226
        }
227
        if ($schema->externalDocs) {
×
228
            $class->setRdfType($schema->externalDocs->url);
×
229
        }
230

231
        $schemaProperties = [];
×
232
        foreach ($this->getSchemaItem($schema) as $schemaItem) {
×
233
            $schemaProperties = array_merge($schemaProperties, $schemaItem->properties);
×
234
        }
235

236
        foreach ($schemaProperties as $propertyName => $schemaProperty) {
×
237
            \assert($schemaProperty instanceof Schema);
×
238
            $property = ($this->propertyGenerator)($propertyName, $config, $class, ['schema' => $schema, 'property' => $schemaProperty]);
×
239
            if ($property) {
×
240
                $class->addProperty($property);
×
241
            }
242
        }
243

244
        if ($config['doctrine']['useCollection']) {
×
245
            $class->addUse(new Use_(ArrayCollection::class));
×
246
            $class->addUse(new Use_(Collection::class));
×
247
        }
248

249
        return $class;
×
250
    }
251

252
    /**
253
     * @param Configuration $config
254
     *
255
     * @return OpenApiClass[]
256
     */
257
    private function buildEnumClasses(Schema $schema, OpenApiClass $class, array $config): array
258
    {
259
        $enumClasses = [];
×
260

261
        foreach ($schema->properties as $propertyName => $schemaProperty) {
×
262
            \assert($schemaProperty instanceof Schema);
×
263
            if ($schemaProperty->enum) {
×
264
                $name = $class->name().u($propertyName)->camel()->title()->toString();
×
265

266
                $enumClass = new OpenApiClass($name);
×
267
                (new OpenApiEnumClassMutator(
×
268
                    $this->phpTypeConverter,
×
269
                    $config['namespaces']['enum'],
×
270
                    $schemaProperty->enum
×
271
                ))($enumClass, []);
×
272
                $enumClasses[$name] = $enumClass;
×
273

274
                if ($classProperty = $class->getPropertyByName($propertyName)) {
×
275
                    $classProperty->reference = $enumClass;
×
276
                }
277
            }
278
        }
279

280
        return $enumClasses;
×
281
    }
282

283
    private function mergeClasses(OpenApiClass $classA, OpenApiClass $classB): OpenApiClass
284
    {
285
        foreach ($classB->properties() as $propertyB) {
×
286
            if (!$classA->getPropertyByName($propertyB->name())) {
×
287
                $classA->addProperty($propertyB);
×
288
            }
289
        }
290

291
        return $classA;
×
292
    }
293
}
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

© 2026 Coveralls, Inc