Completed
Push — master ( 1b0d8b...4b53bc )
by John
03:07
created

OpenApiBuilder::extractExtensions()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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