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; |
||
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 { |
|
0 ignored issues
–
show
|
|||
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 { |
|
0 ignored issues
–
show
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. ![]() |
|||
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 |
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.