Completed
Push — master ( 863dbf...157f19 )
by P.R.
09:00
created

MySqlConstantWorker::executeCreateConstants()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 7
ccs 4
cts 4
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
4
namespace SetBased\Stratum\MySql\Backend;
5
6
use SetBased\Exception\RuntimeException;
7
use SetBased\Stratum\Backend\ConstantWorker;
8
use SetBased\Stratum\Common\Helper\ClassReflectionHelper;
9
use SetBased\Stratum\Common\Helper\Util;
10
use SetBased\Stratum\MySql\Exception\MySqlConnectFailedException;
11
use SetBased\Stratum\MySql\Exception\MySqlDataLayerException;
12
use SetBased\Stratum\MySql\Exception\MySqlQueryErrorException;
13
use SetBased\Stratum\MySql\Helper\DataTypeHelper;
14
15
/**
16
 * Command for creating PHP constants based on column widths, auto increment columns and labels.
17
 */
18
class MySqlConstantWorker extends MySqlWorker implements ConstantWorker
19
{
20
  //--------------------------------------------------------------------------------------------------------------------
21
  /**
22
   * Name of the class that contains all constants.
23
   *
24
   * @var string|null
25
   */
26
  private ?string $className;
27
28
  /**
29
   * All columns in the MySQL schema.
30
   *
31
   * @var array
32
   */
33
  private array $columns = [];
34
35
  /**
36
   * @var array All constants.
37
   */
38
  private array $constants = [];
39
40
  /**
41
   * Filename with column names, their widths, and constant names.
42
   *
43
   * @var string|null
44
   */
45
  private ?string $constantsFilename;
46
47
  /**
48
   * All primary key labels, their widths and constant names.
49
   *
50
   * @var array
51
   */
52
  private array $labels = [];
53
54
  /**
55
   * The previous column names, widths, and constant names (i.e. the content of $constantsFilename upon starting
56
   * this program).
57
   *
58
   * @var array
59
   */
60
  private array $oldColumns = [];
61
62
  //--------------------------------------------------------------------------------------------------------------------
63
  /**
64
   * @inheritdoc
65
   *
66
   * @throws MySqlConnectFailedException
67
   * @throws MySqlDataLayerException
68
   * @throws RuntimeException
69
   */
70 1
  public function execute(): int
71
  {
72 1
    $this->constantsFilename = $this->settings->optString('constants.columns');
73 1
    $this->className         = $this->settings->optString('constants.class');
74
75 1
    if ($this->constantsFilename!==null || $this->className!==null)
76
    {
77 1
      $this->io->title('PhpStratum: MySql Constants');
78
79 1
      $this->connect();
80
81 1
      $this->executeEnabled();
82
83 1
      $this->disconnect();
84
    }
85
    else
86
    {
87
      $this->io->logVerbose('Constants not enabled');
88
    }
89
90 1
    return 0;
91
  }
92
93
  //--------------------------------------------------------------------------------------------------------------------
94
  /**
95
   * Enhances $oldColumns as follows:
96
   * If the constant name is *, is is replaced with the column name prefixed by $this->myPrefix in uppercase.
97
   * Otherwise the constant name is set to uppercase.
98
   */
99 1
  private function enhanceColumns(): void
100
  {
101 1
    foreach ($this->oldColumns as $table)
102
    {
103
      foreach ($table as $column)
104
      {
105
        $tableName  = $column['table_name'];
106
        $columnName = $column['column_name'];
107
108
        if ($column['constant_name']=='*')
109
        {
110
          $constantName                                               = strtoupper($column['column_name']);
111
          $this->oldColumns[$tableName][$columnName]['constant_name'] = $constantName;
112
        }
113
        else
114
        {
115
          $constantName                                               = strtoupper($this->oldColumns[$tableName][$columnName]['constant_name']);
116
          $this->oldColumns[$tableName][$columnName]['constant_name'] = $constantName;
117
        }
118
      }
119
    }
120 1
  }
121
122
  //--------------------------------------------------------------------------------------------------------------------
123
  /**
124
   * Gathers constants based on column widths.
125
   *
126
   * @throws RuntimeException
127
   * @throws MySqlQueryErrorException
128
   */
129 1
  private function executeColumnWidths(): void
130
  {
131 1
    $this->loadOldColumns();
132
133 1
    $this->loadColumns();
134
135 1
    $this->enhanceColumns();
136
137 1
    $this->mergeColumns();
138
139 1
    $this->writeColumns();
140 1
  }
141
142
  //--------------------------------------------------------------------------------------------------------------------
143
  /**
144
   * Creates constants declarations in a class.
145
   *
146
   * @throws MySqlQueryErrorException
147
   * @throws RuntimeException
148
   */
149 1
  private function executeCreateConstants(): void
150
  {
151 1
    $this->loadLabels();
152
153 1
    $this->fillConstants();
154
155 1
    $this->writeConstantClass();
156 1
  }
157
158
  //--------------------------------------------------------------------------------------------------------------------
159
  /**
160
   * Executes the enabled functionalities.
161
   *
162
   * @throws RuntimeException
163
   * @throws MySqlQueryErrorException
164
   */
165 1
  private function executeEnabled(): void
166
  {
167 1
    if ($this->constantsFilename!==null)
168
    {
169 1
      $this->executeColumnWidths();
170
    }
171
172 1
    if ($this->className!==null)
173
    {
174 1
      $this->executeCreateConstants();
175
    }
176
177 1
    $this->logNumberOfConstants();
178 1
  }
179
180
  //--------------------------------------------------------------------------------------------------------------------
181
  /**
182
   * Searches for 3 lines in the source code of the class for constants. The lines are:
183
   * * The first line of the doc block with the annotation '@setbased.stratum.constants'.
184
   * * The last line of this doc block.
185
   * * The last line of continuous constant declarations directly after the doc block.
186
   * If one of these line can not be found the line number will be set to null.
187
   *
188
   * @param string $source The source code of the constant class.
189
   *
190
   * @return array With the 3 line number as described
191
   */
192 1
  private function extractLines(string $source): array
193
  {
194 1
    $tokens = token_get_all($source);
195
196 1
    $line1 = null;
197 1
    $line2 = null;
198 1
    $line3 = null;
199
200
    // Find annotation @constants
201 1
    $step = 1;
202 1
    foreach ($tokens as $token)
203
    {
204
      switch ($step)
205
      {
206 1
        case 1:
207
          // Step 1: Find doc comment with annotation.
208 1
          if (is_array($token) && $token[0]==T_DOC_COMMENT)
209
          {
210 1
            if (strpos($token[1], '@setbased.stratum.constants')!==false)
211
            {
212 1
              $line1 = $token[2];
213 1
              $step  = 2;
214
            }
215
          }
216 1
          break;
217
218 1
        case 2:
219
          // Step 2: Find end of doc block.
220 1
          if (is_array($token))
221
          {
222 1
            if ($token[0]==T_WHITESPACE)
223
            {
224 1
              $line2 = $token[2];
225 1
              if (substr_count($token[1], "\n")>1)
226
              {
227
                // Whitespace contains new line: end doc block without constants.
228 1
                $step = 4;
229
              }
230
            }
231
            else
232
            {
233 1
              if ($token[0]==T_CONST)
234
              {
235 1
                $line3 = $token[2];
236 1
                $step  = 3;
237
              }
238
              else
239
              {
240
                $step = 4;
241
              }
242
            }
243
          }
244 1
          break;
245
246 1
        case 3:
247
          // Step 4: Find en of constants declarations.
248 1
          if (is_array($token))
249
          {
250 1
            if ($token[0]==T_WHITESPACE)
251
            {
252 1
              if (substr_count($token[1], "\n")<=1)
253
              {
254
                // Ignore whitespace.
255 1
                $line3 = $token[2];
256
              }
257
              else
258
              {
259
                // Whitespace contains new line: end of const declarations.
260 1
                $step = 4;
261
              }
262
            }
263 1
            elseif ($token[0]==T_CONST || $token[2]==$line3)
264
            {
265 1
              $line3 = $token[2];
266
            }
267
            else
268
            {
269
              $step = 4;
270
            }
271
          }
272 1
          break;
273
274
        case 4:
275
          // Leave loop.
276
          break;
277
      }
278
    }
279
280
    // @todo get indent based on indent of the doc block.
281
282 1
    return [$line1, $line2, $line3];
283
  }
284
285
  //--------------------------------------------------------------------------------------------------------------------
286
  /**
287
   * Merges $columns and $labels (i.e. all known constants) into $constants.
288
   */
289 1
  private function fillConstants(): void
290
  {
291 1
    foreach ($this->columns as $tableName => $table)
292
    {
293 1
      foreach ($table as $columnName => $column)
294
      {
295 1
        if (isset($this->columns[$tableName][$columnName]['constant_name']))
296
        {
297
          $this->constants[$column['constant_name']] = $column['length'];
298
        }
299
      }
300
    }
301
302 1
    foreach ($this->labels as $label => $id)
303
    {
304 1
      $this->constants[$label] = $id;
305
    }
306
307 1
    ksort($this->constants);
308 1
  }
309
310
  //--------------------------------------------------------------------------------------------------------------------
311
  /**
312
   * Loads the width of all columns in the MySQL schema into $columns.
313
   *
314
   * @throws MySqlQueryErrorException
315
   */
316 1
  private function loadColumns(): void
317
  {
318 1
    $rows = $this->dl->allTableColumns();
1 ignored issue
show
Bug introduced by
The method allTableColumns() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

318
    /** @scrutinizer ignore-call */ 
319
    $rows = $this->dl->allTableColumns();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
319 1
    foreach ($rows as $row)
320
    {
321 1
      $row['length']                                          = DataTypeHelper::deriveFieldLength($row);
322 1
      $this->columns[$row['table_name']][$row['column_name']] = $row;
323
    }
324 1
  }
325
326
  //--------------------------------------------------------------------------------------------------------------------
327
  /**
328
   * Loads all primary key labels from the MySQL database.
329
   *
330
   * @throws MySqlQueryErrorException
331
   */
332 1
  private function loadLabels(): void
333
  {
334 1
    $tables = $this->dl->allLabelTables();
335 1
    foreach ($tables as $table)
336
    {
337 1
      $rows = $this->dl->labelsFromTable($table['table_name'], $table['id'], $table['label']);
338 1
      foreach ($rows as $row)
339
      {
340 1
        $this->labels[$row['label']] = $row['id'];
341
      }
342
    }
343 1
  }
344
345
  //--------------------------------------------------------------------------------------------------------------------
346
  /**
347
   * Loads from file $constantsFilename the previous table and column names, the width of the column,
348
   * and the constant name (if assigned) and stores this data in $oldColumns.
349
   *
350
   * @throws RuntimeException
351
   */
352 1
  private function loadOldColumns(): void
353
  {
354 1
    if (file_exists($this->constantsFilename))
355
    {
356 1
      $handle = fopen($this->constantsFilename, 'r');
357
358 1
      $lineNumber = 0;
359 1
      while (($line = fgets($handle)))
1 ignored issue
show
Bug introduced by
It seems like $handle can also be of type false; however, parameter $handle of fgets() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

359
      while (($line = fgets(/** @scrutinizer ignore-type */ $handle)))
Loading history...
360
      {
361 1
        $lineNumber++;
362 1
        if ($line!="\n")
363
        {
364 1
          $n = preg_match('/^\s*(([a-zA-Z0-9_]+)\.)?([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\s+(\d+)\s*(\*|[a-zA-Z0-9_]+)?\s*$/', $line, $matches);
365 1
          if ($n==0)
366
          {
367
            throw new RuntimeException("Illegal format at line %d in file '%s'.",
368
                                       $lineNumber,
369
                                       $this->constantsFilename);
370
          }
371
372 1
          if (isset($matches[6]))
373
          {
374
            $schemaName   = $matches[2];
375
            $tableName    = $matches[3];
376
            $columnName   = $matches[4];
377
            $length       = $matches[5];
378
            $constantName = $matches[6];
379
380
            if ($schemaName)
381
            {
382
              $tableName = $schemaName.'.'.$tableName;
383
            }
384
385
            $this->oldColumns[$tableName][$columnName] = ['table_name'    => $tableName,
386
                                                          'column_name'   => $columnName,
387
                                                          'length'        => $length,
388
                                                          'constant_name' => $constantName];
389
          }
390
        }
391
      }
392 1
      if (!feof($handle))
1 ignored issue
show
Bug introduced by
It seems like $handle can also be of type false; however, parameter $handle of feof() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

392
      if (!feof(/** @scrutinizer ignore-type */ $handle))
Loading history...
393
      {
394
        throw new RuntimeException("Error reading from file '%s'.", $this->constantsFilename);
395
      }
396
397 1
      $ok = fclose($handle);
1 ignored issue
show
Bug introduced by
It seems like $handle can also be of type false; however, parameter $handle of fclose() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

397
      $ok = fclose(/** @scrutinizer ignore-type */ $handle);
Loading history...
398 1
      if ($ok===false)
399
      {
400
        throw new RuntimeException("Error closing file '%s'.", $this->constantsFilename);
401
      }
402
    }
403 1
  }
404
405
  //--------------------------------------------------------------------------------------------------------------------
406
  /**
407
   * Logs the number of constants generated.
408
   */
409 1
  private function logNumberOfConstants(): void
410
  {
411 1
    $countIds    = count($this->labels);
412 1
    $countWidths = count($this->constants) - $countIds;
413
414 1
    $this->io->writeln('');
415 1
    $this->io->text(sprintf('Number of constants based on column widths: %d', $countWidths));
416 1
    $this->io->text(sprintf('Number of constants based on database IDs : %d', $countIds));
417 1
  }
418
419
  //--------------------------------------------------------------------------------------------------------------------
420
  /**
421
   * Generates PHP code with constant declarations.
422
   *
423
   * @return array The generated PHP code, lines are stored as rows in the array.
424
   */
425 1
  private function makeConstantStatements(): array
426
  {
427 1
    $width1    = 0;
428 1
    $width2    = 0;
429 1
    $constants = [];
430
431 1
    foreach ($this->constants as $constant => $value)
432
    {
433 1
      $width1 = max(mb_strlen($constant), $width1);
434 1
      $width2 = max(mb_strlen((string)$value), $width2);
435
    }
436
437 1
    $format = sprintf('  const %%-%ds = %%%dd;', $width1, $width2);
438 1
    foreach ($this->constants as $constant => $value)
439
    {
440 1
      $constants[] = sprintf($format, $constant, $value);
441
    }
442
443 1
    return $constants;
444
  }
445
446
  //--------------------------------------------------------------------------------------------------------------------
447
  /**
448
   * Preserves relevant data in $oldColumns into $columns.
449
   */
450 1
  private function mergeColumns(): void
451
  {
452 1
    foreach ($this->oldColumns as $tableName => $table)
453
    {
454
      foreach ($table as $columnName => $column)
455
      {
456
        if (isset($this->columns[$tableName][$columnName]))
457
        {
458
          $this->columns[$tableName][$columnName]['constant_name'] = $column['constant_name'];
459
        }
460
      }
461
    }
462 1
  }
463
464
  //--------------------------------------------------------------------------------------------------------------------
465
  /**
466
   * Writes table and column names, the width of the column, and the constant name (if assigned) to
467
   * $constantsFilename.
468
   */
469 1
  private function writeColumns(): void
470
  {
471 1
    ksort($this->columns);
472
473 1
    $content = '';
474 1
    foreach ($this->columns as $table)
475
    {
476 1
      $width1 = 0;
477 1
      $width2 = 0;
478 1
      foreach ($table as $column)
479
      {
480 1
        $width1 = max(mb_strlen($column['column_name']), $width1);
481 1
        $width2 = max(mb_strlen((string)$column['length']), $width2);
482
      }
483
484 1
      foreach ($table as $column)
485
      {
486 1
        if (isset($column['length']))
487
        {
488 1
          if (isset($column['constant_name']))
489
          {
490
            $format  = sprintf("%%s.%%-%ds %%%dd %%s\n", $width1, $width2);
491
            $content .= sprintf($format,
492
                                $column['table_name'],
493
                                $column['column_name'],
494
                                $column['length'],
495
                                $column['constant_name']);
496
          }
497
          else
498
          {
499 1
            $format  = sprintf("%%s.%%-%ds %%%dd\n", $width1, $width2);
500 1
            $content .= sprintf($format,
501 1
                                $column['table_name'],
502 1
                                $column['column_name'],
503 1
                                $column['length']);
504
          }
505
        }
506
      }
507
508 1
      $content .= "\n";
509
    }
510
511
    // Save the columns, width and constants to the filesystem.
512 1
    Util::writeTwoPhases($this->constantsFilename, $content, $this->io);
1 ignored issue
show
Bug introduced by
It seems like $this->constantsFilename can also be of type null; however, parameter $filename of SetBased\Stratum\Common\...\Util::writeTwoPhases() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

512
    Util::writeTwoPhases(/** @scrutinizer ignore-type */ $this->constantsFilename, $content, $this->io);
Loading history...
513 1
  }
514
515
  //--------------------------------------------------------------------------------------------------------------------
516
  /**
517
   * Inserts new and replace old (if any) constant declaration statements in a PHP source file.
518
   *
519
   * @throws RuntimeException
520
   */
521 1
  private function writeConstantClass(): void
522
  {
523
    // Read the source of the class without actually loading the class. Otherwise, we can not (re)load the class in
524
    // MySqlRoutineLoaderWorker::replacePairsConstants.
525 1
    $fileName    = ClassReflectionHelper::getFileName($this->className);
1 ignored issue
show
Bug introduced by
It seems like $this->className can also be of type null; however, parameter $className of SetBased\Stratum\Common\...onHelper::getFileName() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

525
    $fileName    = ClassReflectionHelper::getFileName(/** @scrutinizer ignore-type */ $this->className);
Loading history...
526 1
    $source      = file_get_contents($fileName);
527 1
    $sourceLines = explode("\n", $source);
528
529
    // Search for the lines where to insert and replace constant declaration statements.
530 1
    $lineNumbers = $this->extractLines($source);
531 1
    if (!isset($lineNumbers[0]))
532
    {
533
      throw new RuntimeException("Annotation not found in '%s'.", $fileName);
534
    }
535
536
    // Generate the constant declaration statements.
537 1
    $constants = $this->makeConstantStatements();
538
539
    // Insert new and replace old (if any) constant declaration statements.
540 1
    $tmp1        = array_splice($sourceLines, 0, $lineNumbers[1]);
541 1
    $tmp2        = array_splice($sourceLines, (isset($lineNumbers[2])) ? $lineNumbers[2] - $lineNumbers[1] : 0);
542 1
    $sourceLines = array_merge($tmp1, $constants, $tmp2);
543
544
    // Save the configuration file.
545 1
    Util::writeTwoPhases($fileName, implode("\n", $sourceLines), $this->io);
546 1
  }
547
548
  //--------------------------------------------------------------------------------------------------------------------
549
}
550
551
//----------------------------------------------------------------------------------------------------------------------
552