Completed
Push — develop ( 1304ea...67848a )
by Jens
16:29
created

ClassAnnotator   C

Complexity

Total Complexity 57

Size/Duplication

Total Lines 319
Duplicated Lines 17.87 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 57
lcom 1
cbo 2
dl 57
loc 319
ccs 0
cts 263
cp 0
rs 6.433
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
C reflectFields() 0 50 11
A reflectReference() 0 21 3
C reflectElementType() 33 67 10
B reflectResultClass() 12 27 3
A generate() 0 10 2
A generateCurrentMethod() 0 9 2
A generateMapResponseMethod() 0 9 2
A isOptional() 0 10 4
D annotate() 0 51 11
B ignoreDocBlockLine() 12 17 7
A isPrimitive() 0 12 1

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 ClassAnnotator 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 ClassAnnotator, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @author @jayS-de <[email protected]>
4
 */
5
6
namespace Commercetools\Core\Helper\Annotate;
7
8
use Commercetools\Core\Model\Common\JsonObject;
9
use Commercetools\Core\Request\AbstractApiRequest;
10
11
class ClassAnnotator
12
{
13
    /**
14
     * @var ReflectedClass
15
     */
16
    protected $class;
17
18
    protected $fields;
19
20
    public function __construct($className)
21
    {
22
        $this->class = new ReflectedClass($className);
23
    }
24
25
    /**
26
     * @return array
27
     */
28
    protected function reflectFields()
29
    {
30
        $reflectionClass = new \ReflectionClass($this->class->getClassName());
31
        if (!$reflectionClass->hasMethod('fieldDefinitions')) {
32
            return;
33
        }
34
        $reflectionMethod = $reflectionClass->getMethod('fieldDefinitions');
35
36
        $classObject = $reflectionClass->newInstanceWithoutConstructor();
37
        $this->fields = $reflectionMethod->invoke($classObject);
38
39
        foreach ($this->fields as $fieldName => $field) {
40
            $fieldType = '';
41
            if (isset($field[JsonObject::TYPE])) {
42
                $fieldType = $field[JsonObject::TYPE];
43
            }
44
            if (isset($field[JsonObject::DECORATOR])) {
45
                $getReturnType = $field[JsonObject::DECORATOR];
46
                $this->class->addUse($field[JsonObject::DECORATOR]);
47
            } else {
48
                if (empty($fieldType)) {
49
                    $getReturnType = 'mixed';
50
                } else {
51
                    $getReturnType = $fieldType;
52
                }
53
            }
54
            $getReturnTypeParts = explode('\\', trim($getReturnType, '\\'));
55
            if (!$this->isPrimitive($fieldType) && count($getReturnTypeParts) > 1) {
56
                $getReturnClassName = array_pop($getReturnTypeParts);
57
            } else {
58
                $getReturnClassName = $getReturnType;
59
            }
60
            if ($this->isOptional($field)) {
61
                $optional = ' = null';
62
            } else {
63
                $optional = '';
64
            }
65
66
            $fieldTypeParts = explode('\\', trim($fieldType, '\\'));
67
            if (!$this->isPrimitive($fieldType) && count($fieldTypeParts) > 1) {
68
                $this->class->addUse($fieldType);
69
                $fieldType = array_pop($fieldTypeParts);
70
            }
71
72
            $args = [trim($fieldType . ' $' . $fieldName . $optional)];
73
74
            $this->class->addMagicGetSetMethod('get', $fieldName, [], $getReturnClassName);
75
            $this->class->addMagicGetSetMethod('set', $fieldName, $args, $this->class->getShortClassName());
76
        }
77
    }
78
79
    protected function reflectReference()
80
    {
81
        $reflectionClass = new \ReflectionClass($this->class->getClassName());
82
        if (!$reflectionClass->isSubclassOf('Commercetools\Core\Model\Common\Resource')) {
83
            return;
84
        }
85
86
        $referenceClass = $this->class->getClassName() . 'Reference';
87
        $referenceShortName = $this->class->getShortClassName() . 'Reference';
88
        if (class_exists($referenceClass)) {
89
            $this->class->addMagicMethod(
90
                'getReference',
91
                [],
92
                $referenceShortName,
93
                null,
94
                null,
95
                false,
96
                true
97
            );
98
        }
99
    }
100
101
    protected function reflectElementType()
102
    {
103
        $reflectionClass = new \ReflectionClass($this->class->getClassName());
104
        if (!$reflectionClass->hasMethod('getType')) {
105
            return;
106
        }
107
        $reflectionMethod = $reflectionClass->getMethod('getType');
108
109
        $classObject = $reflectionClass->newInstanceWithoutConstructor();
110
        $elementType = $reflectionMethod->invoke($classObject);
111
112
        if ($elementType && !$this->isPrimitive($elementType)) {
113
            $elementTypeClass = new \ReflectionClass($elementType);
114
            $this->class->addUse($elementType);
115
            $getAtMethod = $reflectionClass->getMethod('getAt');
116 View Code Duplication
            if ($getAtMethod->getDeclaringClass()->getName() != $this->class->getClassName()) {
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...
117
                $this->class->addMagicMethod(
118
                    'getAt',
119
                    ['$offset'],
120
                    $elementTypeClass->getShortName(),
121
                    null,
122
                    null,
123
                    false,
124
                    true
125
                );
126
            }
127
            $getByIdMethod = $reflectionClass->getMethod('getById');
128
            if ($getByIdMethod->getDeclaringClass()->getName() != $this->class->getClassName()) {
129
                $elementTypeObject = $elementTypeClass->newInstanceWithoutConstructor();
130
                if ($elementTypeObject instanceof JsonObject && isset($elementTypeObject->fieldDefinitions()['id'])) {
131
                    $this->class->addMagicMethod(
132
                        'getById',
133
                        ['$offset'],
134
                        $elementTypeClass->getShortName(),
135
                        null,
136
                        null,
137
                        false,
138
                        true
139
                    );
140
                }
141
            }
142
            $addMethod = $reflectionClass->getMethod('add');
143 View Code Duplication
            if ($addMethod->getDeclaringClass()->getName() != $this->class->getClassName()) {
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...
144
                $this->class->addMagicMethod(
145
                    'add',
146
                    [$elementTypeClass->getShortName() . ' $element'],
147
                    $reflectionClass->getShortName(),
148
                    null,
149
                    null,
150
                    false,
151
                    true
152
                );
153
            }
154
            $current = $reflectionClass->getMethod('current');
155 View Code Duplication
            if ($current->getDeclaringClass()->getName() != $this->class->getClassName()) {
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...
156
                $this->class->addMagicMethod(
157
                    'current',
158
                    [],
159
                    $elementTypeClass->getShortName(),
160
                    null,
161
                    null,
162
                    false,
163
                    true
164
                );
165
            }
166
        }
167
    }
168
169
    protected function reflectResultClass()
170
    {
171
        $reflectionClass = new \ReflectionClass($this->class->getClassName());
172
        if (!$reflectionClass->hasMethod('getResultClass')) {
173
            return;
174
        }
175
        $reflectionMethod = $reflectionClass->getMethod('getResultClass');
176
177
        $classObject = $reflectionClass->newInstanceWithoutConstructor();
178
        $resultClass = $reflectionMethod->invoke($classObject);
179
180
        $resultClassReflection = new \ReflectionClass($resultClass);
181
        $this->class->addUse($resultClass);
182
        $mapResponseMethod = $reflectionClass->getMethod('mapResponse');
183 View Code Duplication
        if ($mapResponseMethod->getDeclaringClass()->getName() != $this->class->getClassName()) {
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...
184
            $this->class->addUse('\Commercetools\Core\Response\ApiResponseInterface');
185
            $this->class->addMagicMethod(
186
                'mapResponse',
187
                ['ApiResponseInterface $response'],
188
                $resultClassReflection->getShortName(),
189
                null,
190
                null,
191
                false,
192
                true
193
            );
194
        }
195
    }
196
197
    /**
198
     *
199
     */
200
    public function generate()
201
    {
202
        if ($this->class->isAbstract()) {
203
            return;
204
        }
205
206
        $this->reflectFields();
207
        $this->reflectReference();
208
        $this->annotate();
209
    }
210
211
    public function generateCurrentMethod()
212
    {
213
        if ($this->class->isAbstract()) {
214
            return;
215
        }
216
217
        $this->reflectElementType();
218
        $this->annotate();
219
    }
220
221
    public function generateMapResponseMethod()
222
    {
223
        if ($this->class->isAbstract()) {
224
            return;
225
        }
226
227
        $this->reflectResultClass();
228
        $this->annotate();
229
    }
230
231
    /**
232
     * @param $field
233
     * @return bool
234
     */
235
    protected function isOptional($field)
236
    {
237
        if (!isset($field['optional'])) {
238
            return true;
239
        } elseif (isset($field['optional']) && $field['optional'] == true) {
240
            return true;
241
        }
242
243
        return false;
244
    }
245
246
247
    protected function annotate()
248
    {
249
        $classHead = [];
250
        $classHead[] = 'namespace ' . $this->class->getNamespace() . ';';
251
252
        if (count($this->class->getUses()) > 0) {
253
            $classHead[] = '';
254
        }
255
256
        foreach ($this->class->getUses() as $use) {
257
            $classHead[] = 'use ' . $use['class'] . (isset($use['alias']) ? ' as ' . $use['alias'] : '') . ';';
258
        }
259
        $classHead[] = '';
260
        $classHead[] = '/**';
261
        $classHead[] = ' * @package ' . $this->class->getNamespace();
262
        $docBlockLines = $this->class->getDocBlockLines();
263
        foreach ($docBlockLines as $lineNr => $line) {
264
            if ($this->ignoreDocBlockLine($lineNr, $docBlockLines)) {
265
                continue;
266
            }
267
            $classHead[] = ' *' . (empty($line) ? '' : ' ' . $line);
268
        }
269
270
        foreach ($this->class->getMagicGetSetMethods() as $magicMethod) {
271
            $method = (isset($magicMethod['static']) && $magicMethod['static'] ? 'static ' : '');
272
            $method.= $magicMethod['returnTypeHint'] . ' ' . $magicMethod['name'];
273
            $method.= '(' . implode(', ', $magicMethod['args']) . ')';
274
            $methodString = ' * @method ' . trim($method);
275
276
            if (strlen($methodString) >= 120) {
277
                $classHead[] = ' * @codingStandardsIgnoreStart';
278
                $classHead[] = $methodString;
279
                $classHead[] = ' * @codingStandardsIgnoreEnd';
280
            } else {
281
                $classHead[] = $methodString;
282
            }
283
        }
284
        $classHead[] = ' */';
285
286
        $fileName = $this->class->getFileName();
287
288
        $source = file_get_contents($fileName);
289
290
        $newSource = preg_replace(
291
            '~namespace(.*)class ' . $this->class->getShortClassName() . '~s',
292
            implode(PHP_EOL, $classHead) . PHP_EOL . 'class ' . $this->class->getShortClassName(),
293
            $source
294
        );
295
296
        file_put_contents($fileName, $newSource);
297
    }
298
299
    protected function ignoreDocBlockLine($lineNr, $lines)
300
    {
301 View Code Duplication
        if (isset($lines[$lineNr+1]) &&
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...
302
            strpos($lines[$lineNr], '@codingStandardsIgnoreStart') !== false &&
303
            strpos($lines[$lineNr+1], '@codingStandardsIgnoreEnd') !== false
304
        ) {
305
            return true;
306
        }
307 View Code Duplication
        if (isset($lines[$lineNr-1]) &&
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...
308
            strpos($lines[$lineNr], '@codingStandardsIgnoreEnd') !== false &&
309
            strpos($lines[$lineNr-1], '@codingStandardsIgnoreStart') !== false
310
        ) {
311
            return true;
312
        }
313
314
        return false;
315
    }
316
317
    protected function isPrimitive($type)
318
    {
319
        $primitives = [
320
            'bool' => 'is_bool',
321
            'int' => 'is_int',
322
            'string' => 'is_string',
323
            'float' => 'is_float',
324
            'array' => 'is_array'
325
        ];
326
327
        return isset($primitives[$type]);
328
    }
329
}
330