Completed
Push — master ( d056c8...3b3683 )
by Kévin
03:04
created

Bridge/NelmioApiDoc/Parser/ApiPlatformParser.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <[email protected]>
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\Core\Bridge\NelmioApiDoc\Parser;
15
16
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
17
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
18
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
19
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
20
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
21
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
22
use Nelmio\ApiDocBundle\DataTypes;
23
use Nelmio\ApiDocBundle\Parser\ParserInterface;
24
use Symfony\Component\PropertyInfo\Type;
25
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
26
27
/**
28
 * Extract input and output information for the NelmioApiDocBundle.
29
 *
30
 * @author Kévin Dunglas <[email protected]>
31
 * @author Teoh Han Hui <[email protected]>
32
 */
33
final class ApiPlatformParser implements ParserInterface
34
{
35
    const IN_PREFIX = 'api_platform_in';
36
    const OUT_PREFIX = 'api_platform_out';
37
    const TYPE_IRI = 'IRI';
38
    const TYPE_MAP = [
39
        Type::BUILTIN_TYPE_BOOL => DataTypes::BOOLEAN,
40
        Type::BUILTIN_TYPE_FLOAT => DataTypes::FLOAT,
41
        Type::BUILTIN_TYPE_INT => DataTypes::INTEGER,
42
        Type::BUILTIN_TYPE_STRING => DataTypes::STRING,
43
    ];
44
45
    private $resourceMetadataFactory;
46
    private $propertyNameCollectionFactory;
47
    private $propertyMetadataFactory;
48
    private $nameConverter;
49
50
    public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, NameConverterInterface $nameConverter = null)
51
    {
52
        $this->resourceMetadataFactory = $resourceMetadataFactory;
53
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
54
        $this->propertyMetadataFactory = $propertyMetadataFactory;
55
        $this->nameConverter = $nameConverter;
56
    }
57
58
    /**
59
     * {@inheritdoc}
60
     */
61
    public function supports(array $item)
62
    {
63
        $data = explode(':', $item['class'], 3);
64
        if (!in_array($data[0], [self::IN_PREFIX, self::OUT_PREFIX], true)) {
65
            return false;
66
        }
67
        if (!isset($data[1])) {
68
            return false;
69
        }
70
71
        try {
72
            $this->resourceMetadataFactory->create($data[1]);
73
74
            return true;
75
        } catch (ResourceClassNotFoundException $e) {
76
            // return false
77
        }
78
79
        return false;
80
    }
81
82
    /**
83
     * {@inheritdoc}
84
     */
85
    public function parse(array $item): array
86
    {
87
        list($io, $resourceClass, $operationName) = explode(':', $item['class'], 3);
88
        $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
89
90
        $classOperations = $this->getGroupsForItemAndCollectionOperation($resourceMetadata, $operationName, $io);
91
92
        if (!empty($classOperations['serializer_groups'])) {
93
            return $this->getPropertyMetadata($resourceMetadata, $resourceClass, $io, [], $classOperations);
94
        }
95
96
        return $this->parseResource($resourceMetadata, $resourceClass, $io);
97
    }
98
99
    /**
100
     * Parses a class.
101
     *
102
     * @param ResourceMetadata $resourceMetadata
103
     * @param string           $resourceClass
104
     * @param string           $io
105
     * @param string[]         $visited
106
     *
107
     * @return array
108
     */
109
    private function parseResource(ResourceMetadata $resourceMetadata, string $resourceClass, string $io, array $visited = []): array
110
    {
111
        $visited[] = $resourceClass;
112
113
        $options = [];
114
        $attributes = $resourceMetadata->getAttributes();
115
116 View Code Duplication
        if (isset($attributes['normalization_context']['groups'])) {
117
            $options['serializer_groups'] = $attributes['normalization_context']['groups'];
118
        }
119
120
        if (isset($attributes['denormalization_context']['groups'])) {
121 View Code Duplication
            if (isset($options['serializer_groups'])) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
122
                $options['serializer_groups'] += $attributes['denormalization_context']['groups'];
123
            } else {
124
                $options['serializer_groups'] = $attributes['denormalization_context']['groups'];
125
            }
126
        }
127
128
        return $this->getPropertyMetadata($resourceMetadata, $resourceClass, $io, $visited, $options);
129
    }
130
131
    private function getGroupsContext(ResourceMetadata $resourceMetadata, string $operationName, bool $isNormalization)
132
    {
133
        $groupsContext = $isNormalization ? 'normalization_context' : 'denormalization_context';
134
        $itemOperationAttribute = $resourceMetadata->getItemOperationAttribute($operationName, $groupsContext, ['groups' => []], true)['groups'];
135
        $collectionOperationAttribute = $resourceMetadata->getCollectionOperationAttribute($operationName, $groupsContext, ['groups' => []], true)['groups'];
136
137
        return [
138
            $groupsContext => [
139
                'groups' => array_merge($itemOperationAttribute ?? [], $collectionOperationAttribute ?? []),
140
            ],
141
        ];
142
    }
143
144
    /**
145
     * Returns groups of item & collection.
146
     *
147
     * @param ResourceMetadata $resourceMetadata
148
     * @param string           $operationName
149
     * @param string           $io
150
     *
151
     * @return array
152
     */
153
    private function getGroupsForItemAndCollectionOperation(ResourceMetadata $resourceMetadata, string $operationName, string $io): array
154
    {
155
        $operation = $this->getGroupsContext($resourceMetadata, $operationName, true);
156
        $operation += $this->getGroupsContext($resourceMetadata, $operationName, false);
157
158 View Code Duplication
        if (self::OUT_PREFIX === $io) {
159
            return [
160
                'serializer_groups' => !empty($operation['normalization_context']) ? $operation['normalization_context']['groups'] : [],
161
            ];
162
        }
163
164 View Code Duplication
        if (self::IN_PREFIX === $io) {
165
            return [
166
                'serializer_groups' => !empty($operation['denormalization_context']) ? $operation['denormalization_context']['groups'] : [],
167
            ];
168
        }
169
170
        return [];
171
    }
172
173
    /**
174
     * Returns a property metadata.
175
     *
176
     * @param ResourceMetadata $resourceMetadata
177
     * @param string           $resourceClass
178
     * @param string           $io
179
     * @param string[]         $visited
180
     * @param string[]         $options
181
     *
182
     * @return array
183
     */
184
    private function getPropertyMetadata(ResourceMetadata $resourceMetadata, string $resourceClass, string $io, array $visited, array $options): array
185
    {
186
        $data = [];
187
188
        foreach ($this->propertyNameCollectionFactory->create($resourceClass, $options) as $propertyName) {
189
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
190
            if (
191
                ($propertyMetadata->isReadable() && self::OUT_PREFIX === $io) ||
192
                ($propertyMetadata->isWritable() && self::IN_PREFIX === $io)
193
            ) {
194
                $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName) : $propertyName;
195
                $data[$normalizedPropertyName] = $this->parseProperty($resourceMetadata, $propertyMetadata, $io, null, $visited);
196
            }
197
        }
198
199
        return $data;
200
    }
201
202
    /**
203
     * Parses a property.
204
     *
205
     * @param ResourceMetadata $resourceMetadata
206
     * @param PropertyMetadata $propertyMetadata
207
     * @param string           $io
208
     * @param Type|null        $type
209
     * @param string[]         $visited
210
     *
211
     * @return array
212
     */
213
    private function parseProperty(ResourceMetadata $resourceMetadata, PropertyMetadata $propertyMetadata, $io, Type $type = null, array $visited = [])
214
    {
215
        $data = [
216
            'dataType' => null,
217
            'required' => $propertyMetadata->isRequired(),
218
            'description' => $propertyMetadata->getDescription(),
219
            'readonly' => !$propertyMetadata->isWritable(),
220
        ];
221
222
        if (null === $type && null === $type = $propertyMetadata->getType()) {
223
            // Default to string
224
            $data['dataType'] = DataTypes::STRING;
225
226
            return $data;
227
        }
228
229
        if ($type->isCollection()) {
230
            $data['actualType'] = DataTypes::COLLECTION;
231
232
            if ($collectionType = $type->getCollectionValueType()) {
233
                $subProperty = $this->parseProperty($resourceMetadata, $propertyMetadata, $io, $collectionType, $visited);
234
                if (self::TYPE_IRI === $subProperty['dataType']) {
235
                    $data['dataType'] = 'array of IRIs';
236
                    $data['subType'] = DataTypes::STRING;
237
238
                    return $data;
239
                }
240
241
                $data['subType'] = $subProperty['subType'] ?? null;
242
                if (isset($subProperty['children'])) {
243
                    $data['children'] = $subProperty['children'];
244
                }
245
            }
246
247
            return $data;
248
        }
249
250
        $builtinType = $type->getBuiltinType();
251
        if ('object' === $builtinType) {
252
            $className = $type->getClassName();
253
254
            if (is_subclass_of($className, \DateTimeInterface::class)) {
255
                $data['dataType'] = DataTypes::DATETIME;
256
                $data['format'] = sprintf('{DateTime %s}', \DateTime::RFC3339);
257
258
                return $data;
259
            }
260
261
            try {
262
                $this->resourceMetadataFactory->create($className);
263
            } catch (ResourceClassNotFoundException $e) {
264
                $data['actualType'] = DataTypes::MODEL;
265
                $data['subType'] = $className;
266
267
                return $data;
268
            }
269
270
            if (
271
                (self::OUT_PREFIX === $io && true !== $propertyMetadata->isReadableLink()) ||
272
                (self::IN_PREFIX === $io && true !== $propertyMetadata->isWritableLink())
273
            ) {
274
                $data['dataType'] = self::TYPE_IRI;
275
                $data['actualType'] = DataTypes::STRING;
276
277
                return $data;
278
            }
279
280
            $data['actualType'] = DataTypes::MODEL;
281
            $data['subType'] = $className;
282
            $data['children'] = in_array($className, $visited, true) ? [] : $this->parseResource($resourceMetadata, $className, $io, $visited);
283
284
            return $data;
285
        }
286
287
        $data['dataType'] = self::TYPE_MAP[$builtinType] ?? DataTypes::STRING;
288
289
        return $data;
290
    }
291
}
292