Completed
Push — master ( f33138...47ba09 )
by Samuel
02:41
created

src/JsonDbStructure.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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)
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
        }
117
        else {
118
            $this->jsonStructure = self::getObjectFromJsonFile($jsonStructureFile);
119
        }
120
        $this->sqlVendor = $sqlVendor;
121
    }
122
123
    public function setJsynDirectory($jsynDirectory)
124
    {
125
        $this->jsynDirectory = $jsynDirectory;
126
    }
127
128
    public function setSqlVendor($sqlVendor)
129
    {
130
        $this->sqlVendor = $sqlVendor;
131
    }
132
133
    /**
134
     * @param $topLevelObject string
135
     * @param $crudAction string
136
     *
137
     * Based on the values provided in the $topLevelObject and $crudAction
138
     * variables, this method tries to derive the name of the jsyn file to use
139
     * for parsing.
140
     *
141
     * @return string | bool
142
     */
143
    private function guessJsynFileName($topLevelObject, $crudAction)
144
    {
145
        if (in_array($topLevelObject, $this->topLevelObjects)) {
146
            $this->crudAction = strtolower($crudAction);
147
148
            return $this->crudAction.'-'.self::objectIdentifierToString($topLevelObject).$this->jsynExtension;
149
        }
150
151
        return false;
152
    }
153
154
    /**
155
     * @param $jsonFile PathUtil | string
156
     *
157
     * Gets the content of a json file, decodes it and 
158
     * returns an array of the decoded json.
159
     *
160
     * @return array
161
     */
162
    private function getObjectFromJsonFile($jsonFile)
163
    {
164
        $jsonStructure = file_get_contents($jsonFile);
165
166
        return json_decode($jsonStructure, JSON_FORCE_OBJECT);
167
    }
168
169
    /**
170
     * @param $jsonStructure array
171
     *
172
     * Tries to get the top level object from an array of
173
     * a json structure, returns false if no top level object
174
     * is found.
175
     *
176
     * @return string | bool
177
     */
178
    private function getProvidedTopLevelObject($jsonStructure)
179
    {
180
        foreach ($this->topLevelObjects as $topLevelObject) {
181
            if (isset($jsonStructure[$topLevelObject])) {
182
                return $topLevelObject;
183
            }
184
        }
185
186
        return false;
187
    }
188
189
    /**
190
     * @param $jsonStructure array
191
     *
192
     * Determines if a top level object is a valid one by checking
193
     * the $topLevelObjects array to see if its present.
194
     *
195
     * @return bool
196
     */
197
    private function isValidTopLevelObject($jsonStructure)
198
    {
199
        foreach ($this->topLevelObjects as $topLevelObject) {
200
            if (isset($jsonStructure[$topLevelObject])) {
201
                return true;
202
            }
203
        }
204
205
        return false;
206
    }
207
208
    /**
209
     * @param $objectIdentifier string
210
     *
211
     * Strips a supplied $objectIdentifier string variable of
212
     * special characters and returns a new string with only alphanumeric
213
     * characters.
214
     *
215
     * @return string
216
     */
217
    private function objectIdentifierToString($objectIdentifier)
218
    {
219
        return substr($objectIdentifier, 1, strlen($objectIdentifier) - 1);
220
    }
221
222
    /**
223
     * @param $jsonStructure array
224
     *
225
     * Converts a $jsonStructure array into a string containing valid
226
     * sql statements.
227
     *
228
     * @return string
229
     */
230
    public function generateSqlFromStructure($jsonStructure)
231
    {
232
        $topLevelObject = self::getProvidedTopLevelObject($jsonStructure);
233
        $crudAction = $jsonStructure[$topLevelObject][$this->crudActionKeyword];
234
235
        $jsynFileName = self::guessJsynFileName($topLevelObject, $crudAction);
236
237
        $jsynExtractor = new JsynExtractor($this->jsynDirectory.$jsynFileName, $this->sqlVendor);
238
        $jsynExtractor->formatJsyn();
239
        $jsyn = $jsynExtractor->getJsyn();
240
241
        $count = count($jsyn);
242
        for ($i = 0; $i < $count; ++$i) {
243
            $string = $jsyn[$i];
244
            $toSetValue = false;
245
            $isConstant = false;
246
247
            if (self::enclosed($this->specialCharacters['left-square-bracket'], $this->specialCharacters['right-square-bracket'], $string)) {
248
                $string = str_replace($this->specialCharacters['left-square-bracket'], null, str_replace($this->specialCharacters['right-square-bracket'], null, $string));
249 View Code Duplication
                if (self::enclosed($this->specialCharacters['left-curly-brace'], $this->specialCharacters['right-curly-brace'], $string)) {
250
                    $string = str_replace($this->specialCharacters['left-curly-brace'], null, str_replace($this->specialCharacters['right-curly-brace'], null, $string));
251
                    $toSetValue = true;
252
                }
253 View Code Duplication
            } elseif (self::enclosed($this->specialCharacters['left-curly-brace'], $this->specialCharacters['right-curly-brace'], $string)) {
254
                $string = str_replace($this->specialCharacters['left-curly-brace'], null, str_replace($this->specialCharacters['right-curly-brace'], null, $string));
255
                $toSetValue = true;
256
            } else {
257
                $isConstant = true;
258
            }
259
260
            $_string = str_replace(' ', '-', $string);
261
            if (isset($jsonStructure[$topLevelObject][$_string])) {
262
                if ($toSetValue && !is_bool($jsonStructure[$topLevelObject][$_string])) {
263
                    if (in_array($_string, $this->objectDefiners)) {
264
                        $_str = [];
265
                        foreach ($jsonStructure[$topLevelObject][$_string] as $jsonStructures) {
266
                            $_str[] = self::generateSqlFromObjectDefiner([$_string => $jsonStructures], $_string);
267
                        }
268
                        $jsonStructure[$topLevelObject][$_string] = '('.implode(', ', $_str).')';
269
                    }
270
                    $jsyn[$i] = $jsonStructure[$topLevelObject][$_string];
271
                } else {
272
                    $jsyn[$i] = (isset($jsonStructure[$topLevelObject][$_string]) && $jsonStructure[$topLevelObject][$_string] == true) ? strtoupper($string) : null;
273
                }
274 View Code Duplication
            } else {
275
                if (!$isConstant) {
276
                    if (isset($jsyn[$i - 1]) && $jsyn[$i - 1] == '=') {
277
                        unset($jsyn[$i - 1]);
278
                    }
279
                    unset($jsyn[$i]);
280
                }
281
            }
282
        }
283
284
        return implode(' ', $jsyn);
285
    }
286
287
    /**
288
     * @param $jsonStructures array
289
     * @param $objectDefiner string
290
     *
291
     * While the {@link generateSqlFromStructure()} method above generates sql string
292
     * from only valid top level objects, this method generates sql statements from valid
293
     * object definers. Accepts an $objectDefiner and a $jsonStructure array as parameters.
294
     *
295
     * @return string
296
     */
297
    public function generateSqlFromObjectDefiner($jsonStructures, $objectDefiner)
298
    {
299
        $topLevelObject = $objectDefiner;
300
        $jsynFileName = $objectDefiner.'.jsyn';
301
302
        $jsynExtractor = new JsynExtractor($this->jsynDirectory.$jsynFileName, $this->sqlVendor);
303
        $jsynExtractor->formatJsyn();
304
        $jsyn = $jsynExtractor->getJsyn();
305
306
        $count = count($jsyn);
307
        foreach ($jsonStructures as $jsonStructure) {
308
            $jsonStructure = [$topLevelObject => $jsonStructure];
309
            for ($i = 0; $i < $count; ++$i) {
310
                $string = $jsyn[$i];
311
                $toSetValue = false;
312
                $isConstant = false;
313
                $replaceWithComma = false;
314
315
                if (self::enclosed($this->specialCharacters['left-square-bracket'], $this->specialCharacters['right-square-bracket'], $string)) {
316
                    $string = str_replace($this->specialCharacters['left-square-bracket'], null, str_replace($this->specialCharacters['right-square-bracket'], null, $string));
317
                    if (self::enclosed($this->specialCharacters['left-curly-brace'], $this->specialCharacters['right-curly-brace'], $string)) {
318
                        $string = str_replace($this->specialCharacters['left-curly-brace'], null, str_replace($this->specialCharacters['right-curly-brace'], null, $string));
319
                        $toSetValue = true;
320 View Code Duplication
                    } else if (self::enclosed($this->specialCharacters['left-bracket'], $this->specialCharacters['right-bracket'], $string)) {
321
                        $string = str_replace($this->specialCharacters['left-bracket'], null, str_replace($this->specialCharacters['right-bracket'], null, $string));
322
                        $toSetValue = false;
323
                        $replaceWithComma = true;
324
                    }
325 View Code Duplication
                } else if (self::enclosed($this->specialCharacters['left-curly-brace'], $this->specialCharacters['right-curly-brace'], $string)) {
326
                    $string = str_replace($this->specialCharacters['left-curly-brace'], null, str_replace($this->specialCharacters['right-curly-brace'], null, $string));
327
                    $toSetValue = true;
328
                } else {
329
                    $isConstant = true;
330
                }
331
332
                $_string = str_replace(' ', '-', $string);
333
                if (isset($jsonStructure[$topLevelObject][$_string])) {
334
                    if ($toSetValue && !is_bool($jsonStructure[$topLevelObject][$_string])) {
335
                        $jsyn[$i] = $jsonStructure[$topLevelObject][$_string];
336
                    } else {
337
                        if ($replaceWithComma){
338
                            $string = ", $string";
339
                        }
340
                        $jsyn[$i] = (isset($jsonStructure[$topLevelObject][$_string]) && $jsonStructure[$topLevelObject][$_string] == true) ? strtoupper($string) : null;
341
                       
342
                    }
343 View Code Duplication
                } else {
344
                    if (!$isConstant) {
345
                        if (isset($jsyn[$i - 1]) && $jsyn[$i - 1] == '=') {
346
                            unset($jsyn[$i - 1]);
347
                        }
348
                        unset($jsyn[$i]);
349
                    }
350
                }
351
            }
352
        }
353
        
354
        return implode(' ', $jsyn);
355
    }
356
357
    /**
358
     * @param $encloserPre string
359
     * @param $encloserPost string
360
     * @param $enclosee string
361
     *
362
     * Checks to see if a string ($enclosee) is enclosed by special characters
363
     * such as '{' and '}' and '[' and ']'.
364
     * 
365
     * @return bool
366
     */
367
    private function enclosed($encloserPre, $encloserPost, $enclosee)
368
    {
369
        if (substr($enclosee, 0, 1) == $encloserPre && substr($enclosee, strlen($enclosee) - 1) == $encloserPost) {
370
            return true;
371
        } else {
372
            return false;
373
        }
374
    }
375
376
    /**
377
     * Parses a jsonStructure in global scope and assigns
378
     * a generated array to either of the sql string generator methods
379
     * depending on the top level objects or object definers.
380
     *
381
     * @return bool
382
     */
383
    public function parseStructure()
384
    {
385
        foreach ($this->jsonStructure as $object => $jsonStructure) {
386
            if (!strpos($object, $this->objectGroupKeyword)) {
387
                $jsonStructure = [$object => $jsonStructure];
388
                if (self::isValidTopLevelObject($jsonStructure)) {
389
                    $this->generatedSql[] = self::generateSqlFromStructure($jsonStructure);
390
                }
391
392
                $topLevelObject = self::isAnotherObjectPresent($jsonStructure[$object]);
393
                while ($topLevelObject) {
394
                    if (strtolower($object) == ':database') {
395
                        $dbname = ($jsonStructure[$object]['name']);
396
                        $this->generatedSql[] = "USE $dbname";
397
                    }
398
                    $this->jsonStructure = [$topLevelObject => $jsonStructure[$object][$topLevelObject]];
399
                    $topLevelObject = self::isAnotherObjectPresent($jsonStructure[$object][$topLevelObject]);
400
                    self::parseStructure();
401
                }
402
            } else {
403
                foreach ($jsonStructure as $_jsonStructure) {
404
                    $object = substr($object, 0, strlen($object) - strpos($object, $this->objectGroupKeyword));
405
                    $_jsonStructure = [$object => $_jsonStructure];
406
                    if (self::isValidTopLevelObject($_jsonStructure)) {
407
                        $this->generatedSql[] = self::generateSqlFromStructure($_jsonStructure);
408
                    }
409
410
                    $topLevelObject = self::isAnotherObjectPresent($_jsonStructure[$object]);
411
                    while ($topLevelObject) {
412
                        $this->jsonStructure = [$topLevelObject => $_jsonStructure[$object][$topLevelObject]];
413
                        $topLevelObject = self::isAnotherObjectPresent($_jsonStructure[$object][$topLevelObject]);
414
                        self::parseStructure();
415
                    }
416
                }
417
            }
418
        }
419
420
        return true;
421
    }
422
423
    /**
424
     * @param $jsonStructure array
425
     *
426
     * Determines if another top level object or object definer is
427
     * present within the supplied json structure.
428
     * Returns the name of the object if found and false if not found.
429
     *
430
     * @return string| bool
431
     */
432
    public function isAnotherObjectPresent($jsonStructure)
433
    {
434
        foreach ($this->topLevelObjects as $topLevelObject) {
435
            if (isset($jsonStructure[$topLevelObject])) {
436
                return $topLevelObject;
437
            }
438
        }
439
440
        return false;
441
    }
442
443
    /**
444
     * @param $delimiter string
445
     *
446
     * Returns the parsed and generated string containing the sql
447
     * statement delimited by a value supplied in the $delimiter
448
     * parameter.
449
     *
450
     * @return string
451
     */
452
    public function getGeneratedSql($delimiter = ";\n")
453
    {
454
        return implode($delimiter, $this->generatedSql);
455
    }
456
}
457