Completed
Push — master ( 611a24...34732e )
by John
04:07
created

OpenApiBuilder::build()   C

Complexity

Conditions 9
Paths 16

Size

Total Lines 46
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 46
rs 5.0942
c 0
b 0
f 0
cc 9
eloc 26
nc 16
nop 0
1
<?php declare(strict_types = 1);
2
/*
3
 * This file is part of the KleijnWeb\PhpApi\Descriptions package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
namespace KleijnWeb\PhpApi\Descriptions\Description\Builder;
9
10
use KleijnWeb\PhpApi\Descriptions\Description\ComplexType;
11
use KleijnWeb\PhpApi\Descriptions\Description\Description;
12
use KleijnWeb\PhpApi\Descriptions\Description\Operation;
13
use KleijnWeb\PhpApi\Descriptions\Description\Parameter;
14
use KleijnWeb\PhpApi\Descriptions\Description\Path;
15
use KleijnWeb\PhpApi\Descriptions\Description\Response;
16
use KleijnWeb\PhpApi\Descriptions\Description\Schema\ObjectSchema;
17
use KleijnWeb\PhpApi\Descriptions\Description\Schema\Schema;
18
use KleijnWeb\PhpApi\Descriptions\Description\Visitor\ClosureVisitor;
19
use KleijnWeb\PhpApi\Descriptions\Description\Visitor\ClosureVisitorScope;
20
21
/**
22
 * @author John Kleijn <[email protected]>
23
 */
24
class OpenApiBuilder extends Builder implements ClosureVisitorScope
25
{
26
    /**
27
     * @return Description
28
     */
29
    public function build(): Description
30
    {
31
        $definition = clone $this->document->getDefinition();
32
33
        $host       = isset($definition->host) ? $definition->host : null;
34
        $schemes    = isset($definition->schemes) ? $definition->schemes : [];
35
        $extensions = $this->extractExtensions($definition);
36
        $paths      = [];
37
        if (isset($definition->paths)) {
38
            $extensions = array_merge($extensions, $this->extractExtensions($definition->paths));
39
            foreach ($definition->paths as $path => $pathItem) {
40
                $paths[$path] = $this->createPath($path, $pathItem);
41
            }
42
        }
43
44
        $description = new Description($paths, [], $host, $schemes, $extensions, $this->document);
0 ignored issues
show
Compatibility introduced by
$this->document of type object<stdClass> is not a sub-type of object<KleijnWeb\PhpApi\...tion\Document\Document>. It seems like you assume a child class of the class stdClass to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
45
46
        /** @var ObjectSchema[] $typeDefinitions */
47
        $typeDefinitions = [];
48
49
        $description->accept(new ClosureVisitor($this, function ($schema) use (&$typeDefinitions) {
50
            if ($schema instanceof ObjectSchema) {
51
                if ($schema->isType(Schema::TYPE_OBJECT) && isset($schema->getDefinition()->{'x-ref-id'})) {
52
                    $typeName = substr(
53
                        $schema->getDefinition()->{'x-ref-id'},
54
                        strrpos($schema->getDefinition()->{'x-ref-id'}, '/') + 1
55
                    );
56
57
                    $typeDefinitions[$typeName] = $schema;
58
                }
59
            }
60
        }));
61
62
        foreach ($typeDefinitions as $name => $schema) {
63
            $type = new ComplexType($name, $schema);
64
            $schema->setComplexType($type);
65
            $complexTypes[] = $type;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$complexTypes was never initialized. Although not strictly required by PHP, it is generally a good practice to add $complexTypes = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
66
        }
67
68
        $description->accept(new ClosureVisitor($description, function () use (&$complexTypes) {
69
            /** @noinspection PhpUndefinedFieldInspection */
70
            $this->complexTypes = $complexTypes;
0 ignored issues
show
Bug introduced by
The property complexTypes does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
71
        }));
72
73
        return $description;
74
    }
75
76
    /**
77
     * @param string    $pathName
78
     * @param \stdClass $definition
79
     *
80
     * @return Path
81
     */
82 View Code Duplication
    protected function createPath(string $pathName, \stdClass $definition)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
83
    {
84
        $pathParameters = $this->extractParameters($definition);
85
86
        /** @var Operation[] $operations */
87
        $operations = [];
88
        foreach (self::$methodNames as $method) {
89
            if (isset($definition->$method)) {
90
                $operations[$method] = $this->createOperation(
91
                    $definition->$method,
92
                    $pathName,
93
                    $method,
94
                    $pathParameters
95
                );
96
            }
97
        }
98
99
        return new Path($pathName, $operations, $pathParameters, $this->extractExtensions($definition));
100
    }
101
102
    /**
103
     * @param \stdClass $definition
104
     * @param string    $path
105
     * @param string    $method
106
     * @param array     $pathParameters
107
     *
108
     * @return Operation
109
     */
110
    protected function createOperation(
111
        \stdClass $definition,
112
        string $path,
113
        string $method,
114
        array $pathParameters = []
115
    ): Operation {
116
    
117
        /** @var Parameter[] $parameters */
118
        $parameters = array_merge($pathParameters, self::extractParameters($definition));
119
        $responses  = [];
120
121
        if (isset($definition->responses)) {
122
            $hasOkResponse = false;
123
            foreach ($definition->responses as $code => $responseDefinition) {
124
                $code = (string)$code;
125
                if ($code === 'default' || $code[0] === '2') {
126
                    $hasOkResponse = true;
127
                }
128
                $code             = (int)$code;
129
                $responses[$code] = $this->createResponse($code, $responseDefinition);
130
            }
131
            if (!$hasOkResponse) {
132
                $responses[200] = $this->createResponse(200, (object)[]);
133
            }
134
        }
135
136
        $schemaDefinition = (object)[];
137
        if (!isset($definition->parameters)) {
138
            $schemaDefinition->type = 'null';
139
            $requestSchema          = $this->schemaFactory->create($schemaDefinition);
140
        } else {
141
            $schemaDefinition->type       = 'object';
142
            $schemaDefinition->required   = [];
143
            $schemaDefinition->properties = (object)[];
144
145 View Code Duplication
            foreach ($parameters as $parameter) {
0 ignored issues
show
Duplication introduced by
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...
146
                if ($parameter->isRequired()) {
147
                    $schemaDefinition->required[] = $parameter->getName();
148
                }
149
                $schemaDefinition->properties->{$parameter->getName()} = $parameter->getSchema()->getDefinition();
150
            }
151
152
            $requestSchema = $this->schemaFactory->create($schemaDefinition);
153
        }
154
        $id = isset($definition->operationId) ? $definition->operationId : "$path:$method";
155
156
        $isSecured = isset($definition->security);
157
158
        return new Operation(
159
            $id,
160
            $path,
161
            $method,
162
            $parameters,
163
            $requestSchema,
164
            $responses,
165
            $this->extractExtensions($definition),
166
            $isSecured
167
        );
168
    }
169
170
    /**
171
     * @param \stdClass $definition
172
     *
173
     * @return array
174
     */
175
    protected function extractParameters(\stdClass $definition)
176
    {
177
        $parameters = [];
178
179
        if (isset($definition->parameters)) {
180
            foreach ($definition->parameters as $parameterDefinition) {
181
                $parameters[] = $this->createParameter($parameterDefinition);
182
            }
183
        }
184
185
        return $parameters;
186
    }
187
188
    /**
189
     * @param \stdClass $definition
190
     *
191
     * @return Parameter
192
     */
193
    protected function createParameter(\stdClass $definition)
194
    {
195
        if ($definition->in === Parameter::IN_BODY) {
196
            $definition->schema       = isset($definition->schema) ? $definition->schema : (object)[];
197
            $definition->schema->type = $definition->schema->type ?: 'object';
198
        }
199
        if (isset($definition->schema)) {
200
            $schema = $this->schemaFactory->create($definition->schema);
201
        } else {
202
            $schema = $this->createParameterSchema($definition);
203
        }
204
205
        $required         = isset($definition->required) && $definition->required;
206
        $collectionFormat = isset($definition->collectionFormat) ? $definition->collectionFormat : null;
207
208
        return new Parameter($definition->name, $required, $schema, $definition->in, $collectionFormat);
209
    }
210
211
    /**
212
     * @param int       $code
213
     * @param \stdClass $definition
214
     *
215
     * @return Response
216
     */
217 View Code Duplication
    protected function createResponse(int $code, \stdClass $definition)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
218
    {
219
        return new Response(
220
            $code,
221
            $this->schemaFactory->create(isset($definition->schema) ? $definition->schema : null)
222
        );
223
    }
224
225
    /**
226
     * @param \stdClass $definition
227
     *
228
     * @return Schema
229
     */
230 View Code Duplication
    protected function createParameterSchema(\stdClass $definition): Schema
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
231
    {
232
        // Remove non-JSON-Schema properties
233
        $schemaDefinition     = clone $definition;
234
        $swaggerPropertyNames = [
235
            'name',
236
            'in',
237
            'description',
238
            'required',
239
            'allowEmptyValue',
240
            'collectionFormat',
241
        ];
242
        foreach ($swaggerPropertyNames as $propertyName) {
243
            if (property_exists($schemaDefinition, $propertyName)) {
244
                unset($schemaDefinition->$propertyName);
245
            }
246
        }
247
248
        return $this->schemaFactory->create($schemaDefinition);
249
    }
250
251
    /**
252
     * @param \stdClass $definition
253
     *
254
     * @return array
255
     */
256
    protected static function extractExtensions(\stdClass $definition): array
257
    {
258
        $extensions = [];
259
        foreach ($definition as $attribute => $value) {
0 ignored issues
show
Bug introduced by
The expression $definition of type object<stdClass> is not traversable.
Loading history...
260
            if (0 === strpos($attribute, 'x-')) {
261
                $extensions[substr($attribute, 2)] = $value;
262
                unset($definition->$attribute);
263
            }
264
        }
265
266
        return $extensions;
267
    }
268
}
269