JsonDbStructure   C
last analyzed

Complexity

Total Complexity 61

Size/Duplication

Total Lines 441
Duplicated Lines 6.58 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 8
Bugs 4 Features 2
Metric Value
wmc 61
c 8
b 4
f 2
lcom 1
cbo 1
dl 29
loc 441
rs 6.018

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 2
A setJsynDirectory() 0 4 1
A setSqlVendor() 0 4 1
A guessJsynFileName() 0 10 2
A getObjectFromJsonFile() 0 6 1
A getProvidedTopLevelObject() 0 10 3
A isValidTopLevelObject() 0 10 3
A objectIdentifierToString() 0 4 1
C generateSqlFromStructure() 16 56 15
C generateSqlFromObjectDefiner() 13 58 16
A enclosed() 0 8 3
D parseStructure() 0 39 9
A isAnotherObjectPresent() 0 8 3
A getGeneratedSql() 0 4 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 JsonDbStructure 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 JsonDbStructure, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the samshal/scripd package.
5
 *
6
 * (c) Samuel Adeshina <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Samshal\Scripd;
13
14
/**
15
 * A robust SQL Generator. Parses database structures defined in json based on the
16
 * jsyn file format and generates corresponding sql queries.
17
 *
18
 * @since  1.0
19
 *
20
 * @author Samuel Adeshina <[email protected]>
21
 */
22
final class JsonDbStructure
23
{
24
    /**
25
     * @var array
26
     *
27
     * Names of database objects that can be manipulated
28
     * using major DDL keywords such as 'create', 'alter'
29
     * and 'drop'
30
     */
31
    private $topLevelObjects = [
32
        ':database',
33
        ':table',
34
        ':table-group',
35
        ':view',
36
        ':index',
37
        ':trigger',
38
        ':function',
39
        ':stored-procedure',
40
        ':storage',
41
        ':security',
42
    ];
43
44
    /**
45
     * @var array
46
     *            An array of object definers.
47
     *
48
     * Object Definers are Special keywords that accepts array values
49
     * in a json structure file definition
50
     */
51
    private $objectDefiners = [
52
        'columns',
53
        'add-column',
54
        'foreign-key',
55
    ];
56
57
    /**
58
     * @var array
59
     *            Special Characters used in jsyn files.
60
     *
61
     * Characters which have a special meaning such as braces and
62
     * square brackets are listed in this array
63
     */
64
    private $specialCharacters = [
65
        'left-curly-brace'     => '{',
66
        'right-curly-brace'    => '}',
67
        'left-square-bracket'  => '[',
68
        'right-square-bracket' => ']',
69
        'left-bracket'         => '(',
70
        'right-bracket'        => ')',
71
    ];
72
73
    /**
74
     * @var string
75
     */
76
    private $crudActionKeyword = ':crud-action';
77
78
    /**
79
     * @var string
80
     */
81
    private $objectGroupKeyword = '-group';
82
83
    /**
84
     * @var string
85
     */
86
    private $jsynExtension = '.jsyn';
87
88
    /**
89
     * @var string
90
     */
91
    private $jsynDirectory = __DIR__.'/bin/';
92
93
    /**
94
     * @var null | array
95
     */
96
    private $jsonStructure;
97
98
    /**
99
     * @var null | string
100
     */
101
    private $sqlVendor;
102
103
    /**
104
     * @var array
105
     */
106
    private $generatedSql = [];
107
108
    /**
109
     * @param $jsonStructureFile PathUtil | string | Array
110
     * @param $sqlVendor string
111
     */
112
    public function __construct($jsonStructureFile, $sqlVendor = 'default')
113
    {
114
        if (is_array($jsonStructureFile)) {
115
            $this->jsonStructure = $jsonStructureFile;
0 ignored issues
show
Documentation Bug introduced by
It seems like $jsonStructureFile of type array is incompatible with the declared type null of property $jsonStructure.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
116
        } else {
117
            $this->jsonStructure = self::getObjectFromJsonFile($jsonStructureFile);
0 ignored issues
show
Documentation Bug introduced by
It seems like self::getObjectFromJsonFile($jsonStructureFile) of type array is incompatible with the declared type null of property $jsonStructure.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
118
        }
119
        $this->sqlVendor = $sqlVendor;
0 ignored issues
show
Documentation Bug introduced by
It seems like $sqlVendor of type string is incompatible with the declared type null of property $sqlVendor.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
120
    }
121
122
    /**
123
     * @param $jsynDirectory string
124
     *
125
     * @return void
126
     */
127
    public function setJsynDirectory($jsynDirectory)
128
    {
129
        $this->jsynDirectory = $jsynDirectory;
130
    }
131
132
    /**
133
     * @param $sqlVendor string
134
     *
135
     * @return void
136
     */
137
    public function setSqlVendor($sqlVendor)
138
    {
139
        $this->sqlVendor = $sqlVendor;
140
    }
141
142
    /**
143
     * @param $topLevelObject string
144
     * @param $crudAction string
145
     *
146
     * Based on the values provided in the $topLevelObject and $crudAction
147
     * variables, this method tries to derive the name of the jsyn file to use
148
     * for parsing.
149
     *
150
     * @return string | bool
151
     */
152
    private function guessJsynFileName($topLevelObject, $crudAction)
153
    {
154
        if (in_array($topLevelObject, $this->topLevelObjects)) {
155
            $this->crudAction = strtolower($crudAction);
0 ignored issues
show
Bug introduced by
The property crudAction does not seem to exist. Did you mean crudActionKeyword?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
156
157
            return $this->crudAction.'-'.self::objectIdentifierToString($topLevelObject).$this->jsynExtension;
0 ignored issues
show
Bug introduced by
The property crudAction does not seem to exist. Did you mean crudActionKeyword?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
158
        }
159
160
        return false;
161
    }
162
163
    /**
164
     * @param $jsonFile PathUtil | string
165
     *
166
     * Gets the content of a json file, decodes it and
167
     * returns an array of the decoded json.
168
     *
169
     * @return array
170
     */
171
    private function getObjectFromJsonFile($jsonFile)
172
    {
173
        $jsonStructure = file_get_contents($jsonFile);
174
175
        return json_decode($jsonStructure, JSON_FORCE_OBJECT);
176
    }
177
178
    /**
179
     * @param $jsonStructure array
180
     *
181
     * Tries to get the top level object from an array of
182
     * a json structure, returns false if no top level object
183
     * is found.
184
     *
185
     * @return string | bool
186
     */
187
    private function getProvidedTopLevelObject($jsonStructure)
188
    {
189
        foreach ($this->topLevelObjects as $topLevelObject) {
190
            if (isset($jsonStructure[$topLevelObject])) {
191
                return $topLevelObject;
192
            }
193
        }
194
195
        return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by Samshal\Scripd\JsonDbStr...tProvidedTopLevelObject of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
196
    }
197
198
    /**
199
     * @param $jsonStructure array
200
     *
201
     * Determines if a top level object is a valid one by checking
202
     * the $topLevelObjects array to see if its present.
203
     *
204
     * @return bool
205
     */
206
    private function isValidTopLevelObject($jsonStructure)
207
    {
208
        foreach ($this->topLevelObjects as $topLevelObject) {
209
            if (isset($jsonStructure[$topLevelObject])) {
210
                return true;
211
            }
212
        }
213
214
        return false;
215
    }
216
217
    /**
218
     * @param $objectIdentifier string
219
     *
220
     * Strips a supplied $objectIdentifier string variable of
221
     * special characters and returns a new string with only alphanumeric
222
     * characters.
223
     *
224
     * @return string
225
     */
226
    private function objectIdentifierToString($objectIdentifier)
227
    {
228
        return substr($objectIdentifier, 1, strlen($objectIdentifier) - 1);
229
    }
230
231
    /**
232
     * @param $jsonStructure array
233
     *
234
     * Converts a $jsonStructure array into a string containing valid
235
     * sql statements.
236
     *
237
     * @return string
238
     */
239
    public function generateSqlFromStructure($jsonStructure)
240
    {
241
        $topLevelObject = self::getProvidedTopLevelObject($jsonStructure);
242
        $crudAction = $jsonStructure[$topLevelObject][$this->crudActionKeyword];
243
244
        $jsynFileName = self::guessJsynFileName($topLevelObject, $crudAction);
245
246
        $jsynExtractor = new JsynExtractor($this->jsynDirectory.$jsynFileName, $this->sqlVendor);
247
        $jsynExtractor->formatJsyn();
248
        $jsyn = $jsynExtractor->getJsyn();
249
250
        $count = count($jsyn);
251
        for ($i = 0; $i < $count; ++$i) {
252
            $string = $jsyn[$i];
253
            $toSetValue = false;
254
            $isConstant = false;
255
256
            if (self::enclosed($this->specialCharacters['left-square-bracket'], $this->specialCharacters['right-square-bracket'], $string)) {
257
                $string = str_replace($this->specialCharacters['left-square-bracket'], null, str_replace($this->specialCharacters['right-square-bracket'], null, $string));
258 View Code Duplication
                if (self::enclosed($this->specialCharacters['left-curly-brace'], $this->specialCharacters['right-curly-brace'], $string)) {
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...
259
                    $string = str_replace($this->specialCharacters['left-curly-brace'], null, str_replace($this->specialCharacters['right-curly-brace'], null, $string));
260
                    $toSetValue = true;
261
                }
262 View Code Duplication
            } elseif (self::enclosed($this->specialCharacters['left-curly-brace'], $this->specialCharacters['right-curly-brace'], $string)) {
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...
263
                $string = str_replace($this->specialCharacters['left-curly-brace'], null, str_replace($this->specialCharacters['right-curly-brace'], null, $string));
264
                $toSetValue = true;
265
            } else {
266
                $isConstant = true;
267
            }
268
269
            $_string = str_replace(' ', '-', $string);
270
            if (isset($jsonStructure[$topLevelObject][$_string])) {
271
                if ($toSetValue && !is_bool($jsonStructure[$topLevelObject][$_string])) {
272
                    if (in_array($_string, $this->objectDefiners)) {
273
                        $_str = [];
274
                        foreach ($jsonStructure[$topLevelObject][$_string] as $jsonStructures) {
275
                            $_str[] = self::generateSqlFromObjectDefiner([$_string => $jsonStructures], $_string);
276
                        }
277
                        $jsonStructure[$topLevelObject][$_string] = '('.implode(', ', $_str).')';
278
                    }
279
                    $jsyn[$i] = $jsonStructure[$topLevelObject][$_string];
280
                } else {
281
                    $jsyn[$i] = (isset($jsonStructure[$topLevelObject][$_string]) && $jsonStructure[$topLevelObject][$_string] == true) ? strtoupper($string) : null;
282
                }
283 View Code Duplication
            } else {
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...
284
                if (!$isConstant) {
285
                    if (isset($jsyn[$i - 1]) && $jsyn[$i - 1] == '=') {
286
                        unset($jsyn[$i - 1]);
287
                    }
288
                    unset($jsyn[$i]);
289
                }
290
            }
291
        }
292
293
        return implode(' ', $jsyn);
294
    }
295
296
    /**
297
     * @param $jsonStructures array
298
     * @param $objectDefiner string
299
     *
300
     * While the {@link generateSqlFromStructure()} method above generates sql string
301
     * from only valid top level objects, this method generates sql statements from valid
302
     * object definers. Accepts an $objectDefiner and a $jsonStructure array as parameters.
303
     *
304
     * @return string
305
     */
306
    public function generateSqlFromObjectDefiner($jsonStructures, $objectDefiner)
307
    {
308
        $topLevelObject = $objectDefiner;
309
        $jsynFileName = $objectDefiner.'.jsyn';
310
311
        $jsynExtractor = new JsynExtractor($this->jsynDirectory.$jsynFileName, $this->sqlVendor);
312
        $jsynExtractor->formatJsyn();
313
        $jsyn = $jsynExtractor->getJsyn();
314
315
        $count = count($jsyn);
316
        foreach ($jsonStructures as $jsonStructure) {
317
            $jsonStructure = [$topLevelObject => $jsonStructure];
318
            for ($i = 0; $i < $count; ++$i) {
319
                $string = $jsyn[$i];
320
                $toSetValue = false;
321
                $isConstant = false;
322
                $replaceWithComma = false;
323
324
                if (self::enclosed($this->specialCharacters['left-square-bracket'], $this->specialCharacters['right-square-bracket'], $string)) {
325
                    $string = str_replace($this->specialCharacters['left-square-bracket'], null, str_replace($this->specialCharacters['right-square-bracket'], null, $string));
326
                    if (self::enclosed($this->specialCharacters['left-curly-brace'], $this->specialCharacters['right-curly-brace'], $string)) {
327
                        $string = str_replace($this->specialCharacters['left-curly-brace'], null, str_replace($this->specialCharacters['right-curly-brace'], null, $string));
328
                        $toSetValue = true;
329 View Code Duplication
                    } elseif (self::enclosed($this->specialCharacters['left-bracket'], $this->specialCharacters['right-bracket'], $string)) {
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...
330
                        $string = str_replace($this->specialCharacters['left-bracket'], null, str_replace($this->specialCharacters['right-bracket'], null, $string));
331
                        $toSetValue = false;
332
                        $replaceWithComma = true;
333
                    }
334
                } elseif (self::enclosed($this->specialCharacters['left-curly-brace'], $this->specialCharacters['right-curly-brace'], $string)) {
335
                    $string = str_replace($this->specialCharacters['left-curly-brace'], null, str_replace($this->specialCharacters['right-curly-brace'], null, $string));
336
                    $toSetValue = true;
337
                } else {
338
                    $isConstant = true;
339
                }
340
341
                $_string = str_replace(' ', '-', $string);
342
                if (isset($jsonStructure[$topLevelObject][$_string])) {
343
                    if ($toSetValue && !is_bool($jsonStructure[$topLevelObject][$_string])) {
344
                        $jsyn[$i] = $jsonStructure[$topLevelObject][$_string];
345
                    } else {
346
                        if ($replaceWithComma) {
347
                            $string = ", $string";
348
                        }
349
                        $jsyn[$i] = (isset($jsonStructure[$topLevelObject][$_string]) && $jsonStructure[$topLevelObject][$_string] == true) ? strtoupper($string) : null;
350
                    }
351 View Code Duplication
                } else {
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...
352
                    if (!$isConstant) {
353
                        if (isset($jsyn[$i - 1]) && $jsyn[$i - 1] == '=') {
354
                            unset($jsyn[$i - 1]);
355
                        }
356
                        unset($jsyn[$i]);
357
                    }
358
                }
359
            }
360
        }
361
362
        return implode(' ', $jsyn);
363
    }
364
365
    /**
366
     * @param $encloserPre string
367
     * @param $encloserPost string
368
     * @param $enclosee string
369
     *
370
     * Checks to see if a string ($enclosee) is enclosed by special characters
371
     * such as '{' and '}' and '[' and ']'.
372
     *
373
     * @return bool
374
     */
375
    private function enclosed($encloserPre, $encloserPost, $enclosee)
376
    {
377
        if (substr($enclosee, 0, 1) == $encloserPre && substr($enclosee, strlen($enclosee) - 1) == $encloserPost) {
378
            return true;
379
        } else {
380
            return false;
381
        }
382
    }
383
384
    /**
385
     * Parses a jsonStructure in global scope and assigns
386
     * a generated array to either of the sql string generator methods
387
     * depending on the top level objects or object definers.
388
     *
389
     * @return bool
390
     */
391
    public function parseStructure()
392
    {
393
        foreach ($this->jsonStructure as $object => $jsonStructure) {
0 ignored issues
show
Bug introduced by
The expression $this->jsonStructure of type array<string,?>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
394
            if (!strpos($object, $this->objectGroupKeyword)) {
395
                $jsonStructure = [$object => $jsonStructure];
396
                if (self::isValidTopLevelObject($jsonStructure)) {
397
                    $this->generatedSql[] = self::generateSqlFromStructure($jsonStructure);
398
                }
399
400
                $topLevelObject = self::isAnotherObjectPresent($jsonStructure[$object]);
401
                while ($topLevelObject) {
402
                    if (strtolower($object) == ':database') {
403
                        $dbname = ($jsonStructure[$object]['name']);
404
                        $this->generatedSql[] = "USE $dbname";
405
                    }
406
                    $this->jsonStructure = [$topLevelObject => $jsonStructure[$object][$topLevelObject]];
0 ignored issues
show
Documentation Bug introduced by
It seems like array($topLevelObject =>...ject][$topLevelObject]) of type array<string,?> is incompatible with the declared type null of property $jsonStructure.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
407
                    $topLevelObject = self::isAnotherObjectPresent($jsonStructure[$object][$topLevelObject]);
408
                    self::parseStructure();
409
                }
410
            } else {
411
                foreach ($jsonStructure as $_jsonStructure) {
412
                    $object = substr($object, 0, strlen($object) - strpos($object, $this->objectGroupKeyword));
413
                    $_jsonStructure = [$object => $_jsonStructure];
414
                    if (self::isValidTopLevelObject($_jsonStructure)) {
415
                        $this->generatedSql[] = self::generateSqlFromStructure($_jsonStructure);
416
                    }
417
418
                    $topLevelObject = self::isAnotherObjectPresent($_jsonStructure[$object]);
419
                    while ($topLevelObject) {
420
                        $this->jsonStructure = [$topLevelObject => $_jsonStructure[$object][$topLevelObject]];
0 ignored issues
show
Documentation Bug introduced by
It seems like array($topLevelObject =>...ject][$topLevelObject]) of type array<string,?> is incompatible with the declared type null of property $jsonStructure.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
421
                        $topLevelObject = self::isAnotherObjectPresent($_jsonStructure[$object][$topLevelObject]);
422
                        self::parseStructure();
423
                    }
424
                }
425
            }
426
        }
427
428
        return true;
429
    }
430
431
    /**
432
     * @param $jsonStructure array
433
     *
434
     * Determines if another top level object or object definer is
435
     * present within the supplied json structure.
436
     * Returns the name of the object if found and false if not found.
437
     *
438
     * @return string
439
     */
440
    public function isAnotherObjectPresent($jsonStructure)
441
    {
442
        foreach ($this->topLevelObjects as $topLevelObject) {
443
            if (isset($jsonStructure[$topLevelObject])) {
444
                return $topLevelObject;
445
            }
446
        }
447
    }
448
449
    /**
450
     * @param $delimiter string
451
     *
452
     * Returns the parsed and generated string containing the sql
453
     * statement delimited by a value supplied in the $delimiter
454
     * parameter.
455
     *
456
     * @return string
457
     */
458
    public function getGeneratedSql($delimiter = ";\n")
459
    {
460
        return implode($delimiter, $this->generatedSql);
461
    }
462
}
463