Completed
Push — master ( 11b317...37df4d )
by Lucas
09:27
created

JsonDefinition   C

Complexity

Total Complexity 66

Size/Duplication

Total Lines 451
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 9

Test Coverage

Coverage 76.43%

Importance

Changes 1
Bugs 0 Features 1
Metric Value
wmc 66
c 1
b 0
f 1
lcom 2
cbo 9
dl 0
loc 451
ccs 120
cts 157
cp 0.7643
rs 5.7474

28 Methods

Rating   Name   Duplication   Size   Complexity  
A getIndexes() 0 8 2
A __construct() 0 4 1
A getDef() 0 4 1
A getId() 0 8 2
A getDescription() 0 4 1
A getTitle() 0 4 1
A hasController() 0 5 2
A isSubDocument() 0 4 1
A getNamespace() 0 4 1
A setNamespace() 0 11 2
A isReadOnlyService() 0 8 2
A hasFixtures() 0 4 1
A getFixtures() 0 8 2
A getFixtureOrder() 0 9 3
B getRouterBase() 0 17 5
A getBaseController() 0 9 3
A getParentService() 0 9 3
A getField() 0 5 2
A getFields() 0 17 3
A createFieldHierarchyRecursive() 0 15 4
B processFieldHierarchyRecursive() 0 28 5
A processSimpleField() 0 14 3
A getRelations() 0 13 3
A getRelation() 0 5 2
A getRoles() 0 8 2
A isRecordOriginModifiable() 0 9 2
A isRecordOriginFlagSet() 0 11 4
A getServiceCollection() 0 12 3

How to fix   Complexity   

Complex Class

Complex classes like JsonDefinition 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 JsonDefinition, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Graviton\GeneratorBundle\Definition;
3
4
use Graviton\GeneratorBundle\Definition\Schema\Service;
5
6
/**
7
 * This class represents the json file that defines the structure
8
 * of a mongo collection that exists and serves as a base to generate
9
 * a bundle.
10
 *
11
 * @todo     if this json format serves in more places; move this class
12
 * @todo     validate json
13
 *
14
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
15
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
16
 * @link     http://swisscom.ch
17
 */
18
class JsonDefinition
19
{
20
    /**
21
     * Schema
22
     *
23
     * @var Schema\Definition
24
     */
25
    private $def;
26
27
    /**
28
     * Composed namespace of this definition, must be explicitly set
29
     *
30
     * @var string
31
     */
32
    private $namespace;
33
34
    /**
35
     * Constructor
36
     *
37
     * @param Schema\Definition $definition
38
     */
39 62
    public function __construct(Schema\Definition $definition)
40
    {
41 62
        $this->def = $definition;
42 62
    }
43
44
    /**
45
     * @return Schema\Definition
46
     */
47
    public function getDef()
48
    {
49
        return $this->def;
50
    }
51
52
    /**
53
     * Returns this loads ID
54
     *
55
     * @return string ID
56
     */
57 24
    public function getId()
58
    {
59 24
        if ($this->def->getId() === null) {
60 2
            throw new \RuntimeException('No id found for document');
61
        }
62
63 22
        return $this->def->getId();
64
    }
65
66
    /**
67
     * Returns the description
68
     *
69
     * @return string Description
70
     */
71 2
    public function getDescription()
72
    {
73 2
        return $this->def->getDescription();
74
    }
75
76
    /**
77
     * Returns the title
78
     *
79
     * @return string Title
80
     */
81
    public function getTitle()
82
    {
83
        return $this->def->getTitle();
84
    }
85
86
    /**
87
     * Returns whether this definition requires the generation
88
     * of a controller. normally yes, but sometimes not ;-)
89
     *
90
     * @return bool true if yes, false if no
91
     */
92 4
    public function hasController()
93
    {
94 4
        return $this->def->getService() !== null &&
95 4
            $this->def->getService()->getRouterBase() !== null;
96
    }
97
98
    /**
99
     * This is a method that allows us to distinguish between a full json spec
100
     * and a hash defined in a full spec which was divided into a seperate Document (thus, a SubDocument).
101
     * To be aware what it is mainly serves for the generator to generate them as embedded documents,
102
     * as subdocuments are always embedded.
103
     *
104
     * @return bool true if yes, false if not
105
     */
106 6
    public function isSubDocument()
107
    {
108 6
        return $this->def->getIsSubDocument();
109
    }
110
111
    /**
112
     * Gets the namespace
113
     *
114
     * @return string namespace
115
     */
116 14
    public function getNamespace()
117
    {
118 14
        return $this->namespace;
119
    }
120
121
    /**
122
     * Sets the namespace
123
     *
124
     * @param string $namespace namespace
125
     *
126
     * @return void
127
     */
128 10
    public function setNamespace($namespace)
129
    {
130
        // normalize namespace
131 10
        $namespace = str_replace('/', '\\', $namespace);
132
133 10
        if (substr($namespace, -1) == '\\') {
134 2
            $namespace = substr($namespace, 0, -1);
135 1
        }
136
137 10
        $this->namespace = $namespace;
138 10
    }
139
140
    /**
141
     * Returns whether this service is read-only
142
     *
143
     * @return bool true if yes, false if not
144
     */
145 4
    public function isReadOnlyService()
146
    {
147 4
        if ($this->def->getService() === null) {
148
            return false;
149
        }
150
151 4
        return $this->def->getService()->getReadOnly();
152
    }
153
154
    /**
155
     * Returns whether this service has fixtures
156
     *
157
     * @return bool true if yes, false if not
158
     */
159 4
    public function hasFixtures()
160
    {
161 4
        return count($this->getFixtures()) > 0;
162
    }
163
164
    /**
165
     * Returns the fixtures or empty array if none
166
     *
167
     * @return array fixtures
168
     */
169 4
    public function getFixtures()
170
    {
171 4
        if ($this->def->getService() === null) {
172
            return [];
173
        }
174
175 4
        return $this->def->getService()->getFixtures();
176
    }
177
178
    /**
179
     * Returns the order number at which order this fixture should be loaded.
180
     * this is needed if we have relations/references between the fixtures..
181
     *
182
     * @return int order
183
     */
184 4
    public function getFixtureOrder()
185
    {
186 4
        if ($this->def->getService() === null ||
187 4
            $this->def->getService()->getFixtureOrder() === null) {
188 2
            return 100;
189
        }
190
191 2
        return $this->def->getService()->getFixtureOrder();
192
    }
193
194
    /**
195
     * Returns a router base path. false if default should be used.
196
     *
197
     * @return string router base, i.e. /bundle/name/
0 ignored issues
show
Documentation introduced by
Should the return type not be false|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
198
     */
199 6
    public function getRouterBase()
200
    {
201 6
        if ($this->def->getService() === null ||
202 6
            $this->def->getService()->getRouterBase() === null) {
203 2
            return false;
204
        }
205
206 4
        $routerBase = $this->def->getService()->getRouterBase();
207 4
        if (substr($routerBase, 0, 1) !== '/') {
208 2
            $routerBase = '/' . $routerBase;
209 1
        }
210 4
        if (substr($routerBase, -1) === '/') {
211 2
            $routerBase = substr($routerBase, 0, -1);
212 1
        }
213
214 4
        return $routerBase;
215
    }
216
217
    /**
218
     * Returns the Controller classname this services' controller shout inherit.
219
     * Defaults to the RestController of the RestBundle of course.
220
     *
221
     * @return string base controller
222
     */
223 4
    public function getBaseController()
224
    {
225 4
        if ($this->def->getService() === null ||
226 4
            $this->def->getService()->getBaseController() === null) {
227 2
            return 'RestController';
228
        }
229
230 2
        return $this->def->getService()->getBaseController();
231
    }
232
233
    /**
234
     * Returns the parent service to use when adding the service xml
235
     *
236
     * Defaults to graviton.rest.controller
237
     *
238
     * @return string base controller
239
     */
240
    public function getParentService()
241
    {
242
        if ($this->def->getService() === null ||
243
            $this->def->getService()->getParent() === null) {
244
            return 'graviton.rest.controller';
245
        }
246
247
        return $this->def->getService()->getParent();
248
    }
249
250
    /**
251
     * Returns a specific field or null
252
     *
253
     * @param string $name Field name
254
     *
255
     * @return DefinitionElementInterface The field
0 ignored issues
show
Documentation introduced by
Should the return type not be DefinitionElementInterface|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
256
     */
257 30
    public function getField($name)
258
    {
259 30
        $fields = $this->getFields();
260 30
        return isset($fields[$name]) ? $fields[$name] : null;
261
    }
262
263
    /**
264
     * Returns the field definition
265
     *
266
     * @return DefinitionElementInterface[] Fields
267
     */
268 32
    public function getFields()
269
    {
270 32
        $hierarchy = [];
271 32
        foreach ($this->def->getTarget()->getFields() as $field) {
272 30
            $hierarchy = array_merge_recursive(
273 15
                $hierarchy,
274 30
                $this->createFieldHierarchyRecursive($field, $field->getName())
275 15
            );
276 16
        }
277
278 32
        $fields = [];
279 32
        foreach ($hierarchy as $name => $definition) {
280 30
            $fields[$name] = $this->processFieldHierarchyRecursive($name, $definition);
281 16
        }
282
283 32
        return $fields;
284
    }
285
286
    /**
287
     * @param Schema\Field $definition Raw field definition
288
     * @param string       $path       Relative field path
289
     * @return mixed
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,array<strin...a\Field>|Schema\Field>>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
290
     * @throws \InvalidArgumentException
291
     */
292 30
    private function createFieldHierarchyRecursive(Schema\Field $definition, $path)
293
    {
294 30
        if (!preg_match('/^(?P<name>[^\.]+)(\.(?P<sub>.+))?$/', $path, $matches)) {
295
            throw new \InvalidArgumentException(sprintf('Invalid field name "%s" definition', $definition->getName()));
296
        }
297
298 30
        $name = ctype_digit($matches['name']) ? "\x00array" : $matches['name'];
299 30
        if (isset($matches['sub'])) {
300 30
            $definition = $this->createFieldHierarchyRecursive($definition, $matches['sub']);
301 15
        } else {
302 30
            $definition = ["\x00field" => $definition];
303
        }
304
305 30
        return [$name => $definition];
306
    }
307
308
    /**
309
     * @param string $name Field name
310
     * @param array  $data Field data
311
     *
312
     * @return DefinitionElementInterface
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use JsonDefinitionArray|Json...ield|JsonDefinitionHash.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
313
     */
314 30
    private function processFieldHierarchyRecursive($name, $data)
315
    {
316
        // array field
317 30
        if (isset($data["\x00array"])) {
318 28
            return new JsonDefinitionArray(
319 14
                $name,
320 28
                $this->processFieldHierarchyRecursive($name, $data["\x00array"])
321 14
            );
322
        }
323
324
        // simple field
325 30
        if (array_keys($data) === ["\x00field"]) {
326 30
            return $this->processSimpleField($name, $data["\x00field"]);
327
        }
328
329
330
        // hash field
331 30
        $fields = [];
332 30
        $definition = null;
333 30
        foreach ($data as $subname => $subdata) {
334 30
            if ($subname === "\x00field") {
335 6
                $definition = $subdata;
336 3
            } else {
337 30
                $fields[$subname] = $this->processFieldHierarchyRecursive($subname, $subdata);
0 ignored issues
show
Documentation introduced by
$subdata is of type null, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
338
            }
339 15
        }
340 30
        return new JsonDefinitionHash($name, $this, $fields, $definition);
341
    }
342
343
    /**
344
     * @param string       $name       Field name
345
     * @param Schema\Field $definition Field
346
     *
347
     * @return DefinitionElementInterface
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use JsonDefinitionArray|JsonDefinitionField.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
348
     */
349 30
    private function processSimpleField($name, Schema\Field $definition)
350
    {
351 30
        if (strpos($definition->getType(), 'class:') === 0) {
352 24
            $field = new JsonDefinitionRel($name, $definition, $this->getRelation($name));
353 12
        } else {
354 28
            $field = new JsonDefinitionField($name, $definition);
355
        }
356
357 30
        if (substr($definition->getType(), -2) === '[]') {
358 24
            $field = new JsonDefinitionArray($name, $field);
359 12
        }
360
361 30
        return $field;
362
    }
363
364
    /**
365
     * Get target relations which are explictly defined
366
     *
367
     * @return Schema\Relation[] relations
368
     */
369 32
    public function getRelations()
370
    {
371 32
        if ($this->def->getTarget() === null) {
372
            return [];
373
        }
374
375 32
        $relations = [];
376 32
        foreach ($this->def->getTarget()->getRelations() as $relation) {
377 6
            $relations[$relation->getLocalProperty()] = $relation;
378 16
        }
379
380 32
        return $relations;
381
    }
382
383
    /**
384
     * Get relation by field name
385
     *
386
     * @param string $field Field name
387
     * @return Schema\Relation|null
388
     */
389 24
    private function getRelation($field)
390
    {
391 24
        $relations = $this->getRelations();
392 24
        return isset($relations[$field]) ? $relations[$field] : null;
393
    }
394
395
    /**
396
     * Provides the role set defined in the service section.
397
     *
398
     * @return array
399
     */
400 2
    public function getRoles()
401
    {
402 2
        if ($this->def->getService() === null) {
403
            return [];
404
        }
405
406 2
        return $this->def->getService()->getRoles();
407
    }
408
409
    /**
410
     * Can record origin be modified
411
     *
412
     * @return bool
413
     */
414
    public function isRecordOriginModifiable()
415
    {
416
        $retVal = false;
417
        if ($this->isRecordOriginFlagSet()) {
418
            $retVal = $this->def->getService()->getRecordOriginModifiable();
419
        }
420
421
        return $retVal;
422
    }
423
424
    /**
425
     * check if the RecordOriginModifiable flag is set
426
     *
427
     * @return bool
428
     */
429
    public function isRecordOriginFlagSet()
430
    {
431
        $retVal = false;
432
        if ($this->def->getService() !== null
433
            && is_object($this->def->getService())
434
            && $this->def->getService()->getRecordOriginModifiable() !== null) {
435
            $retVal = true;
436
        }
437
438
        return $retVal;
439
    }
440
441
    /**
442
     * @return string
443
     */
444
    public function getServiceCollection()
445
    {
446
        $collectionName = $this->getId();
447
448
        if ($this->def->getService() instanceof Service
449
            && $this->def->getService()->getCollectionName()) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
450
451
            $collectionName = $this->def->getService()->getCollectionName();
452
        }
453
454
        return $collectionName;
455
    }
456
457
    /**
458
     * @return string[]
459
     */
460 2
    public function getIndexes()
461
    {
462 2
        $indexes = [];
463 2
        if ($this->def->getTarget()->getIndexes()) {
464
            $indexes = $this->def->getTarget()->getIndexes();
465
        }
466 2
        return $indexes;
467
    }
468
}
469