JsonDefinition   F
last analyzed

Complexity

Total Complexity 85

Size/Duplication

Total Lines 571
Duplicated Lines 0.53 %

Coupling/Cohesion

Components 2
Dependencies 12

Test Coverage

Coverage 60.54%

Importance

Changes 0
Metric Value
wmc 85
lcom 2
cbo 12
dl 3
loc 571
ccs 112
cts 185
cp 0.6054
rs 2
c 0
b 0
f 0

34 Methods

Rating   Name   Duplication   Size   Complexity  
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() 3 11 2
A isReadOnlyService() 0 8 2
A isVersionedService() 0 8 3
A hasFixtures() 0 4 1
A getFixtures() 0 8 2
A getFixtureOrder() 0 9 3
A getRouterBase() 0 17 5
A getBaseController() 0 9 3
A getParentService() 0 9 3
A getField() 0 5 2
B getFields() 0 40 5
A createFieldHierarchyRecursive() 0 15 4
A 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 getVariations() 0 8 2
A getSolrFields() 0 18 3
A isRecordOriginModifiable() 0 9 2
A isRecordOriginFlagSet() 0 11 4
A getServiceCollection() 0 12 3
A getIndexes() 0 8 2
A getSearchables() 0 12 4
A getTextIndexes() 0 12 4
A getAllTextIndexes() 0 7 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 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\Constraint;
5
use Graviton\GeneratorBundle\Definition\Schema\Service;
6
use Graviton\GeneratorBundle\Definition\Schema\Solr;
7
use Graviton\SchemaBundle\Constraint\VersionServiceConstraint;
8
9
/**
10
 * This class represents the json file that defines the structure
11
 * of a mongo collection that exists and serves as a base to generate
12
 * a bundle.
13
 *
14
 * @todo     if this json format serves in more places; move this class
15
 *
16
 * @author   List of contributors <https://github.com/libgraviton/graviton/graphs/contributors>
17
 * @license  https://opensource.org/licenses/MIT MIT License
18
 * @link     http://swisscom.ch
19
 */
20
class JsonDefinition
21
{
22
    /**
23
     * Schema
24
     *
25
     * @var Schema\Definition
26
     */
27
    private $def;
28
29
    /**
30
     * Composed namespace of this definition, must be explicitly set
31
     *
32
     * @var string
33
     */
34
    private $namespace;
35
36
    /**
37
     * Constructor
38
     *
39
     * @param Schema\Definition $definition
40
     */
41 64
    public function __construct(Schema\Definition $definition)
42
    {
43 64
        $this->def = $definition;
44 64
    }
45
46
    /**
47
     * @return Schema\Definition
48
     */
49
    public function getDef()
50
    {
51
        return $this->def;
52
    }
53
54
    /**
55
     * Returns this loads ID
56
     *
57
     * @return string ID
58
     */
59 24
    public function getId()
60
    {
61 24
        if ($this->def->getId() === null) {
62 2
            throw new \RuntimeException('No id found for document');
63
        }
64
65 22
        return $this->def->getId();
66
    }
67
68
    /**
69
     * Returns the description
70
     *
71
     * @return string Description
72
     */
73 2
    public function getDescription()
74
    {
75 2
        return $this->def->getDescription();
76
    }
77
78
    /**
79
     * Returns the title
80
     *
81
     * @return string Title
82
     */
83
    public function getTitle()
84
    {
85
        return $this->def->getTitle();
86
    }
87
88
    /**
89
     * Returns whether this definition requires the generation
90
     * of a controller. normally yes, but sometimes not ;-)
91
     *
92
     * @return bool true if yes, false if no
93
     */
94 4
    public function hasController()
95
    {
96 4
        return $this->def->getService() !== null &&
97 4
            $this->def->getService()->getRouterBase() !== null;
98
    }
99
100
    /**
101
     * This is a method that allows us to distinguish between a full json spec
102
     * and a hash defined in a full spec which was divided into a seperate Document (thus, a SubDocument).
103
     * To be aware what it is mainly serves for the generator to generate them as embedded documents,
104
     * as subdocuments are always embedded.
105
     *
106
     * @return bool true if yes, false if not
107
     */
108 6
    public function isSubDocument()
109
    {
110 6
        return $this->def->getIsSubDocument();
111
    }
112
113
    /**
114
     * Gets the namespace
115
     *
116
     * @return string namespace
117
     */
118 14
    public function getNamespace()
119
    {
120 14
        return $this->namespace;
121
    }
122
123
    /**
124
     * Sets the namespace
125
     *
126
     * @param string $namespace namespace
127
     *
128
     * @return void
129
     */
130 10
    public function setNamespace($namespace)
131
    {
132
        // normalize namespace
133 10
        $namespace = str_replace('/', '\\', $namespace);
134
135 10 View Code Duplication
        if (substr($namespace, -1) == '\\') {
136 2
            $namespace = substr($namespace, 0, -1);
137
        }
138
139 10
        $this->namespace = $namespace;
140 10
    }
141
142
    /**
143
     * Returns whether this service is read-only
144
     *
145
     * @return bool true if yes, false if not
146
     */
147 4
    public function isReadOnlyService()
148
    {
149 4
        if ($this->def->getService() === null) {
150
            return false;
151
        }
152
153 4
        return $this->def->getService()->getReadOnly();
154
    }
155
156
    /**
157
     * Returns whether this service is versioning
158
     *
159
     * @return bool true if yes, false if not
160
     */
161
    public function isVersionedService()
162
    {
163
        if ($this->def->getService() === null || !$this->def->getService()->getVersioning()) {
164
            return false;
165
        }
166
167
        return (boolean) $this->def->getService()->getVersioning();
168
    }
169
170
    /**
171
     * Returns whether this service has fixtures
172
     *
173
     * @return bool true if yes, false if not
174
     */
175 4
    public function hasFixtures()
176
    {
177 4
        return count($this->getFixtures()) > 0;
178
    }
179
180
    /**
181
     * Returns the fixtures or empty array if none
182
     *
183
     * @return array fixtures
184
     */
185 4
    public function getFixtures()
186
    {
187 4
        if ($this->def->getService() === null) {
188
            return [];
189
        }
190
191 4
        return $this->def->getService()->getFixtures();
192
    }
193
194
    /**
195
     * Returns the order number at which order this fixture should be loaded.
196
     * this is needed if we have relations/references between the fixtures..
197
     *
198
     * @return int order
199
     */
200 4
    public function getFixtureOrder()
201
    {
202 4
        if ($this->def->getService() === null ||
203 4
            $this->def->getService()->getFixtureOrder() === null) {
204 2
            return 100;
205
        }
206
207 2
        return $this->def->getService()->getFixtureOrder();
208
    }
209
210
    /**
211
     * Returns a router base path. false if default should be used.
212
     *
213
     * @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...
214
     */
215 6
    public function getRouterBase()
216
    {
217 6
        if ($this->def->getService() === null ||
218 6
            $this->def->getService()->getRouterBase() === null) {
219 2
            return false;
220
        }
221
222 4
        $routerBase = $this->def->getService()->getRouterBase();
223 4
        if (substr($routerBase, 0, 1) !== '/') {
224 2
            $routerBase = '/' . $routerBase;
225
        }
226 4
        if (substr($routerBase, -1) === '/') {
227 2
            $routerBase = substr($routerBase, 0, -1);
228
        }
229
230 4
        return $routerBase;
231
    }
232
233
    /**
234
     * Returns the Controller classname this services' controller shout inherit.
235
     * Defaults to the RestController of the RestBundle of course.
236
     *
237
     * @return string base controller
238
     */
239 4
    public function getBaseController()
240
    {
241 4
        if ($this->def->getService() === null ||
242 4
            $this->def->getService()->getBaseController() === null) {
243 2
            return 'RestController';
244
        }
245
246 2
        return $this->def->getService()->getBaseController();
247
    }
248
249
    /**
250
     * Returns the parent service to use when adding the service xml
251
     *
252
     * Defaults to graviton.rest.controller
253
     *
254
     * @return string base controller
255
     */
256
    public function getParentService()
257
    {
258
        if ($this->def->getService() === null ||
259
            $this->def->getService()->getParent() === null) {
260
            return 'graviton.rest.controller';
261
        }
262
263
        return $this->def->getService()->getParent();
264
    }
265
266
    /**
267
     * Returns a specific field or null
268
     *
269
     * @param string $name Field name
270
     *
271
     * @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...
272
     */
273 30
    public function getField($name)
274
    {
275 30
        $fields = $this->getFields();
276 30
        return isset($fields[$name]) ? $fields[$name] : null;
277
    }
278
279
    /**
280
     * Returns the field definition
281
     *
282
     * @return DefinitionElementInterface[] Fields
283
     */
284 32
    public function getFields()
285
    {
286 32
        $hierarchy = [];
287 32
        foreach ($this->def->getTarget()->getFields() as $field) {
288 30
            $hierarchy = array_merge_recursive(
289 30
                $hierarchy,
290 30
                $this->createFieldHierarchyRecursive($field, $field->getName())
291
            );
292
        }
293
294 32
        $fields = [];
295
296
        /*******
297
         * CONDITIONAL GENERATED FIELD AREA
298
         *
299
         * so simplify things, you can put fields here that should be conditionally created by Graviton.
300
         * @TODO refactor into a FieldBuilder* type of thing where different builders can add fields conditionally.
301
         */
302
303
        // Versioning field, for version control.
304 32
        if ($this->def->getService() && $this->def->getService()->getVersioning()) {
305
            $definition = new Schema\Field();
306
            $constraint = new Constraint();
307
            $constraint->setName('versioning');
308
            $definition->setName(VersionServiceConstraint::FIELD_NAME)->setTitle('Version')->setType('int')
309
                       ->setConstraints([$constraint])
310
                       ->setDescription('Document version. You need to send current version if you want to update.');
311
            $fields['version'] = $this->processSimpleField('version',  $definition);
0 ignored issues
show
Coding Style introduced by
Expected 1 space instead of 2 after comma in function call.
Loading history...
312
        }
313
314
        /*******
315
         * add fields as defined in the definition file.
316
         */
317
318 32
        foreach ($hierarchy as $name => $definition) {
319 30
            $fields[$name] = $this->processFieldHierarchyRecursive($name, $definition);
320
        }
321
322 32
        return $fields;
323
    }
324
325
    /**
326
     * @param Schema\Field $definition Raw field definition
327
     * @param string       $path       Relative field path
328
     * @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...
329
     * @throws \InvalidArgumentException
330
     */
331 30
    private function createFieldHierarchyRecursive(Schema\Field $definition, $path)
332
    {
333 30
        if (!preg_match('/^(?P<name>[^\.]+)(\.(?P<sub>.+))?$/', $path, $matches)) {
334
            throw new \InvalidArgumentException(sprintf('Invalid field name "%s" definition', $definition->getName()));
335
        }
336
337 30
        $name = ctype_digit($matches['name']) ? "\x00array" : $matches['name'];
338 30
        if (isset($matches['sub'])) {
339 30
            $definition = $this->createFieldHierarchyRecursive($definition, $matches['sub']);
340
        } else {
341 30
            $definition = ["\x00field" => $definition];
342
        }
343
344 30
        return [$name => $definition];
345
    }
346
347
    /**
348
     * @param string $name Field name
349
     * @param array  $data Field data
350
     *
351
     * @return DefinitionElementInterface
352
     */
353 30
    private function processFieldHierarchyRecursive($name, $data)
354
    {
355
        // array field
356 30
        if (isset($data["\x00array"])) {
357 28
            return new JsonDefinitionArray(
358 28
                $name,
359 28
                $this->processFieldHierarchyRecursive($name, $data["\x00array"])
360
            );
361
        }
362
363
        // simple field
364 30
        if (array_keys($data) === ["\x00field"]) {
365 30
            return $this->processSimpleField($name, $data["\x00field"]);
366
        }
367
368
369
        // hash field
370 30
        $fields = [];
371 30
        $definition = null;
372 30
        foreach ($data as $subname => $subdata) {
373 30
            if ($subname === "\x00field") {
374 6
                $definition = $subdata;
375
            } else {
376 30
                $fields[$subname] = $this->processFieldHierarchyRecursive($subname, $subdata);
377
            }
378
        }
379 30
        return new JsonDefinitionHash($name, $this, $fields, $definition);
380
    }
381
382
    /**
383
     * @param string       $name       Field name
384
     * @param Schema\Field $definition Field
385
     *
386
     * @return DefinitionElementInterface
387
     */
388 30
    private function processSimpleField($name, Schema\Field $definition)
389
    {
390 30
        if (strpos($definition->getType(), 'class:') === 0) {
391 24
            $field = new JsonDefinitionRel($name, $definition, $this->getRelation($name));
392
        } else {
393 28
            $field = new JsonDefinitionField($name, $definition);
394
        }
395
396 30
        if (substr($definition->getType(), -2) === '[]') {
397 24
            $field = new JsonDefinitionArray($name, $field);
398
        }
399
400 30
        return $field;
401
    }
402
403
    /**
404
     * Get target relations which are explictly defined
405
     *
406
     * @return Schema\Relation[] relations
407
     */
408 32
    public function getRelations()
409
    {
410 32
        if ($this->def->getTarget() === null) {
411
            return [];
412
        }
413
414 32
        $relations = [];
415 32
        foreach ($this->def->getTarget()->getRelations() as $relation) {
416 6
            $relations[$relation->getLocalProperty()] = $relation;
417
        }
418
419 32
        return $relations;
420
    }
421
422
    /**
423
     * Get relation by field name
424
     *
425
     * @param string $field Field name
426
     * @return Schema\Relation|null
427
     */
428 24
    private function getRelation($field)
429
    {
430 24
        $relations = $this->getRelations();
431 24
        return isset($relations[$field]) ? $relations[$field] : null;
432
    }
433
434
    /**
435
     * Provides the role set defined in the service section.
436
     *
437
     * @return array
438
     */
439 2
    public function getRoles()
440
    {
441 2
        if ($this->def->getService() === null) {
442
            return [];
443
        }
444
445 2
        return $this->def->getService()->getRoles();
446
    }
447
448
    /**
449
     * Provides the variations attribute from the service section
450
     *
451
     * @return array
452
     */
453
    public function getVariations()
454
    {
455
        if ($this->def->getService() === null) {
456
            return [];
457
        }
458
459
        return $this->def->getService()->getVariations();
460
    }
461
462
    /**
463
     * gets information about solr
464
     *
465
     * @return array|Schema\Solr
466
     */
467
    public function getSolrFields()
468
    {
469
        $solr = $this->def->getSolr();
470
        if (!$solr instanceof Solr) {
471
            return [];
472
        }
473
474
        $fields = [];
475
        foreach ($solr->getFields() as $field) {
476
            $fields[] = [
477
                'name' => $field->getName(),
478
                'type' => $field->getType(),
479
                'weight' => $field->getWeight()
480
            ];
481
        }
482
483
        return $fields;
484
    }
485
486
    /**
487
     * Can record origin be modified
488
     *
489
     * @return bool
490
     */
491
    public function isRecordOriginModifiable()
492
    {
493
        $retVal = false;
494
        if ($this->isRecordOriginFlagSet()) {
495
            $retVal = $this->def->getService()->getRecordOriginModifiable();
496
        }
497
498
        return $retVal;
499
    }
500
501
    /**
502
     * check if the RecordOriginModifiable flag is set
503
     *
504
     * @return bool
505
     */
506
    public function isRecordOriginFlagSet()
507
    {
508
        $retVal = false;
509
        if ($this->def->getService() !== null
510
            && is_object($this->def->getService())
511
            && $this->def->getService()->getRecordOriginModifiable() !== null) {
512
            $retVal = true;
513
        }
514
515
        return $retVal;
516
    }
517
518
    /**
519
     * @return string
520
     */
521
    public function getServiceCollection()
522
    {
523
        $collectionName = $this->getId();
524
525
        if ($this->def->getService() instanceof Service
526
            && $this->def->getService()->getCollectionName()) {
0 ignored issues
show
Coding Style introduced by
Blank line found at start of control structure
Loading history...
527
528
            $collectionName = $this->def->getService()->getCollectionName();
529
        }
530
531
        return $collectionName;
532
    }
533
534
    /**
535
     * @return string[]
536
     */
537 2
    public function getIndexes()
538
    {
539 2
        $indexes = [];
540 2
        if ($this->def->getTarget()->getIndexes()) {
541
            $indexes = $this->def->getTarget()->getIndexes();
542
        }
543 2
        return $indexes;
544
    }
545
546
    /**
547
     * @return string[]
548
     */
549
    public function getSearchables()
550
    {
551
        $indexes = [];
552
        if ($fields = $this->def->getTarget()->getFields()) {
553
            foreach ($fields as $field) {
554
                if ($value = (int) $field->getSearchable()) {
555
                    $indexes[$field->getName()] = $value;
556
                }
557
            }
558
        }
559
        return $indexes;
560
    }
561
562
    /**
563
     * @return string[]
564
     */
565 2
    public function getTextIndexes()
566
    {
567 2
        $indexes = [];
568 2
        if ($keys = $this->def->getTarget()->getTextIndexes()) {
569
            foreach ($keys as $key) {
570
                if ($value = (int) $key['weight']) {
571
                    $indexes[$key['field']] = $value;
572
                }
573
            }
574
        }
575 2
        return $indexes;
576
    }
577
578
    /**
579
     * Combine in one array the Search text indexes
580
     *
581
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use string[].

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...
582
     */
583
    public function getAllTextIndexes()
584
    {
585
        return array_merge(
586
            $this->getSearchables(),
587
            $this->getTextIndexes()
588
        );
589
    }
590
}
591