Completed
Pull Request — master (#23)
by John
06:15
created

OpenApiBuilder   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 263
Duplicated Lines 17.49 %

Coupling/Cohesion

Components 1
Dependencies 12

Importance

Changes 0
Metric Value
wmc 43
lcom 1
cbo 12
dl 46
loc 263
rs 8.96
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
C build() 0 62 11
A createPath() 19 19 3
C createOperation() 0 61 11
A extractParameters() 0 12 3
B createParameter() 0 17 7
A createResponse() 7 7 2
A createParameterSchema() 20 20 3
A extractExtensions() 0 12 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like OpenApiBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use OpenApiBuilder, and based on these observations, apply Extract Interface, too.

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
9
namespace KleijnWeb\PhpApi\Descriptions\Description\Builder;
10
11
use KleijnWeb\PhpApi\Descriptions\Description\ComplexType;
12
use KleijnWeb\PhpApi\Descriptions\Description\Description;
13
use KleijnWeb\PhpApi\Descriptions\Description\Operation;
14
use KleijnWeb\PhpApi\Descriptions\Description\Parameter;
15
use KleijnWeb\PhpApi\Descriptions\Description\Path;
16
use KleijnWeb\PhpApi\Descriptions\Description\Response;
17
use KleijnWeb\PhpApi\Descriptions\Description\Schema\ObjectSchema;
18
use KleijnWeb\PhpApi\Descriptions\Description\Schema\Schema;
19
use KleijnWeb\PhpApi\Descriptions\Description\Visitor\ClosureVisitor;
20
use KleijnWeb\PhpApi\Descriptions\Description\Visitor\ClosureVisitorScope;
21
22
/**
23
 * @author John Kleijn <[email protected]>
24
 */
25
class OpenApiBuilder extends Builder implements ClosureVisitorScope
26
{
27
    /**
28
     * @return Description
29
     */
30
    public function build(): Description
31
    {
32
        $definition = clone $this->document->getDefinition();
33
34
        $host       = isset($definition->host) ? $definition->host : null;
35
        $schemes    = isset($definition->schemes) ? $definition->schemes : [];
36
        $extensions = $this->extractExtensions($definition);
37
        $paths      = [];
38
        if (isset($definition->paths)) {
39
            $extensions = array_merge($extensions, $this->extractExtensions($definition->paths));
40
            foreach ($definition->paths as $path => $pathItem) {
41
                $paths[$path] = $this->createPath($path, $pathItem);
42
            }
43
        }
44
45
        $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...
46
47
        /** @var ObjectSchema[] $typeDefinitions */
48
        $typeDefinitions = [];
49
50
        $description->accept(new ClosureVisitor($this, function ($schema) use (&$typeDefinitions) {
51
            if ($schema instanceof ObjectSchema) {
52
                if (isset($schema->getDefinition()->{'x-ref-id'})) {
53
                    $typeName = substr(
54
                        $schema->getDefinition()->{'x-ref-id'},
55
                        strrpos($schema->getDefinition()->{'x-ref-id'}, '/') + 1
56
                    );
57
58
                    $typeDefinitions[$typeName] = $schema;
59
                }
60
            }
61
        }));
62
63
        $this->document->apply(function ($composite, $attribute, $parent, $parentAttribute) use (&$typeDefinitions) {
64
            if ($parentAttribute === 'definitions') {
65
                $schema = $this->schemaFactory->create($composite);
66
                if ($schema instanceof ObjectSchema) {
67
                    $typeDefinitions[$attribute] = $schema;
68
                }
69
            }
70
        });
71
72
73
        if (null !== $this->classNameResolver) {
74
            foreach ($typeDefinitions as $name => $schema) {
75
                $type = new ComplexType(
76
                    $name,
77
                    $schema,
78
                    $this->classNameResolver->resolve($name)
79
                );
80
                $schema->setComplexType($type);
81
                $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...
82
            }
83
84
            $description->accept(new ClosureVisitor($description, function () use (&$complexTypes) {
85
                /** @noinspection PhpUndefinedFieldInspection */
86
                $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...
87
            }));
88
        }
89
90
        return $description;
91
    }
92
93
    /**
94
     * @param string    $pathName
95
     * @param \stdClass $definition
96
     *
97
     * @return Path
98
     */
99 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...
100
    {
101
        $pathParameters = $this->extractParameters($definition);
102
103
        /** @var Operation[] $operations */
104
        $operations = [];
105
        foreach (self::$methodNames as $method) {
106
            if (isset($definition->$method)) {
107
                $operations[$method] = $this->createOperation(
108
                    $definition->$method,
109
                    $pathName,
110
                    $method,
111
                    $pathParameters
112
                );
113
            }
114
        }
115
116
        return new Path($pathName, $operations, $pathParameters, $this->extractExtensions($definition));
117
    }
118
119
    /**
120
     * @param \stdClass $definition
121
     * @param string    $path
122
     * @param string    $method
123
     * @param array     $pathParameters
124
     *
125
     * @return Operation
126
     */
127
    protected function createOperation(
128
        \stdClass $definition,
129
        string $path,
130
        string $method,
131
        array $pathParameters = []
132
    ): Operation {
133
134
        /** @var Parameter[] $parameters */
135
        $parameters = array_merge($pathParameters, self::extractParameters($definition));
136
        $responses  = [];
137
138
        if (isset($definition->responses)) {
139
            $hasOkResponse = false;
140
            foreach ($definition->responses as $code => $responseDefinition) {
141
                $code = (string)$code;
142
                if ($code === 'default' || $code[0] === '2') {
143
                    $hasOkResponse = true;
144
                }
145
                $code             = (int)$code;
146
                $responses[$code] = $this->createResponse($code, $responseDefinition);
147
            }
148
            if (!$hasOkResponse) {
149
                $responses[200] = $this->createResponse(200, (object)[]);
150
            }
151
        }
152
153
        $schemaDefinition = (object)[];
154
        if (!isset($definition->parameters)) {
155
            $schemaDefinition->type = 'null';
156
            $requestSchema          = $this->schemaFactory->create($schemaDefinition);
157
        } else {
158
            $schemaDefinition->type       = 'object';
159
            $schemaDefinition->properties = (object)[];
160
161
            foreach ($parameters as $parameter) {
162
                if ($parameter->isRequired()) {
163
                    if (!isset($schemaDefinition->required)) {
164
                        $schemaDefinition->required = [];
165
                    }
166
                    $schemaDefinition->required[] = $parameter->getName();
167
                }
168
                $schemaDefinition->properties->{$parameter->getName()} = $parameter->getSchema()->getDefinition();
169
            }
170
171
            $requestSchema = $this->schemaFactory->create($schemaDefinition);
172
        }
173
        $id = isset($definition->operationId) ? $definition->operationId : "$path:$method";
174
175
        $isSecured = isset($definition->security);
176
177
        return new Operation(
178
            $id,
179
            $path,
180
            $method,
181
            $parameters,
182
            $requestSchema,
183
            $responses,
184
            $this->extractExtensions($definition),
185
            $isSecured
186
        );
187
    }
188
189
    /**
190
     * @param \stdClass $definition
191
     *
192
     * @return array
193
     */
194
    protected function extractParameters(\stdClass $definition)
195
    {
196
        $parameters = [];
197
198
        if (isset($definition->parameters)) {
199
            foreach ($definition->parameters as $parameterDefinition) {
200
                $parameters[] = $this->createParameter($parameterDefinition);
201
            }
202
        }
203
204
        return $parameters;
205
    }
206
207
    /**
208
     * @param \stdClass $definition
209
     *
210
     * @return Parameter
211
     */
212
    protected function createParameter(\stdClass $definition)
213
    {
214
        if ($definition->in === Parameter::IN_BODY) {
215
            $definition->schema       = isset($definition->schema) ? $definition->schema : (object)[];
216
            $definition->schema->type = $definition->schema->type ?: 'object';
217
        }
218
        if (isset($definition->schema)) {
219
            $schema = $this->schemaFactory->create($definition->schema);
220
        } else {
221
            $schema = $this->createParameterSchema($definition);
222
        }
223
224
        $required         = isset($definition->required) && $definition->required;
225
        $collectionFormat = isset($definition->collectionFormat) ? $definition->collectionFormat : null;
226
227
        return new Parameter($definition->name, $required, $schema, $definition->in, $collectionFormat);
228
    }
229
230
    /**
231
     * @param int       $code
232
     * @param \stdClass $definition
233
     *
234
     * @return Response
235
     */
236 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...
237
    {
238
        return new Response(
239
            $code,
240
            $this->schemaFactory->create(isset($definition->schema) ? $definition->schema : null)
241
        );
242
    }
243
244
    /**
245
     * @param \stdClass $definition
246
     *
247
     * @return Schema
248
     */
249 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...
250
    {
251
        // Remove non-JSON-Schema properties
252
        $schemaDefinition     = clone $definition;
253
        $swaggerPropertyNames = [
254
            'name',
255
            'in',
256
            'description',
257
            'required',
258
            'allowEmptyValue',
259
            'collectionFormat',
260
        ];
261
        foreach ($swaggerPropertyNames as $propertyName) {
262
            if (property_exists($schemaDefinition, $propertyName)) {
263
                unset($schemaDefinition->$propertyName);
264
            }
265
        }
266
267
        return $this->schemaFactory->create($schemaDefinition);
268
    }
269
270
    /**
271
     * @param \stdClass $definition
272
     *
273
     * @return array
274
     */
275
    protected static function extractExtensions(\stdClass $definition): array
276
    {
277
        $extensions = [];
278
        foreach ($definition as $attribute => $value) {
0 ignored issues
show
Bug introduced by
The expression $definition of type object<stdClass> is not traversable.
Loading history...
279
            if (0 === strpos($attribute, 'x-')) {
280
                $extensions[substr($attribute, 2)] = $value;
281
                unset($definition->$attribute);
282
            }
283
        }
284
285
        return $extensions;
286
    }
287
}
288