Completed
Push — feature/EVO-5751-text-search-j... ( 4dd2c3 )
by
unknown
66:00 queued 03:58
created

JsonDefinition::createFieldHierarchyRecursive()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 15
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.2
c 0
b 0
f 0
cc 4
eloc 9
nc 5
nop 2
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
    public function __construct(Schema\Definition $definition)
40
    {
41
        $this->def = $definition;
42
    }
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
    public function getId()
58
    {
59
        if ($this->def->getId() === null) {
60
            throw new \RuntimeException('No id found for document');
61
        }
62
63
        return $this->def->getId();
64
    }
65
66
    /**
67
     * Returns the description
68
     *
69
     * @return string Description
70
     */
71
    public function getDescription()
72
    {
73
        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
    public function hasController()
93
    {
94
        return $this->def->getService() !== null &&
95
            $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
    public function isSubDocument()
107
    {
108
        return $this->def->getIsSubDocument();
109
    }
110
111
    /**
112
     * Gets the namespace
113
     *
114
     * @return string namespace
115
     */
116
    public function getNamespace()
117
    {
118
        return $this->namespace;
119
    }
120
121
    /**
122
     * Sets the namespace
123
     *
124
     * @param string $namespace namespace
125
     *
126
     * @return void
127
     */
128
    public function setNamespace($namespace)
129
    {
130
        // normalize namespace
131
        $namespace = str_replace('/', '\\', $namespace);
132
133
        if (substr($namespace, -1) == '\\') {
134
            $namespace = substr($namespace, 0, -1);
135
        }
136
137
        $this->namespace = $namespace;
138
    }
139
140
    /**
141
     * Returns whether this service is read-only
142
     *
143
     * @return bool true if yes, false if not
144
     */
145
    public function isReadOnlyService()
146
    {
147
        if ($this->def->getService() === null) {
148
            return false;
149
        }
150
151
        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
    public function hasFixtures()
160
    {
161
        return count($this->getFixtures()) > 0;
162
    }
163
164
    /**
165
     * Returns the fixtures or empty array if none
166
     *
167
     * @return array fixtures
168
     */
169
    public function getFixtures()
170
    {
171
        if ($this->def->getService() === null) {
172
            return [];
173
        }
174
175
        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
    public function getFixtureOrder()
185
    {
186
        if ($this->def->getService() === null ||
187
            $this->def->getService()->getFixtureOrder() === null) {
188
            return 100;
189
        }
190
191
        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
    public function getRouterBase()
200
    {
201
        if ($this->def->getService() === null ||
202
            $this->def->getService()->getRouterBase() === null) {
203
            return false;
204
        }
205
206
        $routerBase = $this->def->getService()->getRouterBase();
207
        if (substr($routerBase, 0, 1) !== '/') {
208
            $routerBase = '/' . $routerBase;
209
        }
210
        if (substr($routerBase, -1) === '/') {
211
            $routerBase = substr($routerBase, 0, -1);
212
        }
213
214
        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
    public function getBaseController()
224
    {
225
        if ($this->def->getService() === null ||
226
            $this->def->getService()->getBaseController() === null) {
227
            return 'RestController';
228
        }
229
230
        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
    public function getField($name)
258
    {
259
        $fields = $this->getFields();
260
        return isset($fields[$name]) ? $fields[$name] : null;
261
    }
262
263
    /**
264
     * Returns the field definition
265
     *
266
     * @return DefinitionElementInterface[] Fields
267
     */
268
    public function getFields()
269
    {
270
        $hierarchy = [];
271
        foreach ($this->def->getTarget()->getFields() as $field) {
272
            $hierarchy = array_merge_recursive(
273
                $hierarchy,
274
                $this->createFieldHierarchyRecursive($field, $field->getName())
275
            );
276
        }
277
278
        $fields = [];
279
        foreach ($hierarchy as $name => $definition) {
280
            $fields[$name] = $this->processFieldHierarchyRecursive($name, $definition);
281
        }
282
283
        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
    private function createFieldHierarchyRecursive(Schema\Field $definition, $path)
293
    {
294
        if (!preg_match('/^(?P<name>[^\.]+)(\.(?P<sub>.+))?$/', $path, $matches)) {
295
            throw new \InvalidArgumentException(sprintf('Invalid field name "%s" definition', $definition->getName()));
296
        }
297
298
        $name = ctype_digit($matches['name']) ? "\x00array" : $matches['name'];
299
        if (isset($matches['sub'])) {
300
            $definition = $this->createFieldHierarchyRecursive($definition, $matches['sub']);
301
        } else {
302
            $definition = ["\x00field" => $definition];
303
        }
304
305
        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
    private function processFieldHierarchyRecursive($name, $data)
315
    {
316
        // array field
317
        if (isset($data["\x00array"])) {
318
            return new JsonDefinitionArray(
319
                $name,
320
                $this->processFieldHierarchyRecursive($name, $data["\x00array"])
321
            );
322
        }
323
324
        // simple field
325
        if (array_keys($data) === ["\x00field"]) {
326
            return $this->processSimpleField($name, $data["\x00field"]);
327
        }
328
329
330
        // hash field
331
        $fields = [];
332
        $definition = null;
333
        foreach ($data as $subname => $subdata) {
334
            if ($subname === "\x00field") {
335
                $definition = $subdata;
336
            } else {
337
                $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
        }
340
        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
    private function processSimpleField($name, Schema\Field $definition)
350
    {
351
        if (strpos($definition->getType(), 'class:') === 0) {
352
            $field = new JsonDefinitionRel($name, $definition, $this->getRelation($name));
353
        } else {
354
            $field = new JsonDefinitionField($name, $definition);
355
        }
356
357
        if (substr($definition->getType(), -2) === '[]') {
358
            $field = new JsonDefinitionArray($name, $field);
359
        }
360
361
        return $field;
362
    }
363
364
    /**
365
     * Get target relations which are explictly defined
366
     *
367
     * @return Schema\Relation[] relations
368
     */
369
    public function getRelations()
370
    {
371
        if ($this->def->getTarget() === null) {
372
            return [];
373
        }
374
375
        $relations = [];
376
        foreach ($this->def->getTarget()->getRelations() as $relation) {
377
            $relations[$relation->getLocalProperty()] = $relation;
378
        }
379
380
        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
    private function getRelation($field)
390
    {
391
        $relations = $this->getRelations();
392
        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
    public function getRoles()
401
    {
402
        if ($this->def->getService() === null) {
403
            return [];
404
        }
405
406
        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
    public function getIndexes()
461
    {
462
        $indexes = [];
463
        if ($this->def->getTarget()->getIndexes()) {
464
            $indexes = $this->def->getTarget()->getIndexes();
465
        }
466
        return $indexes;
467
    }
468
469
    /**
470
     * @return string[]
471
     */
472
    public function getSearchables()
473
    {
474
        $indexes = [];
475
        if ($fields = $this->def->getTarget()->getFields()) {
476
            foreach ($fields as $field) {
477
                if ($value = (int) $field->getSearchable()) {
478
                    $indexes[$field->getName()] = $value;
479
                }
480
            }
481
        }
482
        return $indexes;
483
    }
484
}
485