Issues (16)

src/Backend/MySqlConstantWorker.php (3 issues)

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

320
    if (file_exists(/** @scrutinizer ignore-type */ $this->constantsFilename))
Loading history...
321 1
    {
322 1
      $lines = explode(PHP_EOL, file_get_contents($this->constantsFilename));
0 ignored issues
show
It seems like $this->constantsFilename can also be of type null; however, parameter $filename of file_get_contents() 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

322
      $lines = explode(PHP_EOL, file_get_contents(/** @scrutinizer ignore-type */ $this->constantsFilename));
Loading history...
323
      foreach ($lines as $index => $line)
324
      {
325
        if ($line!=='')
326
        {
327
          $n = preg_match('/^\s*(([a-zA-Z0-9_]+)\.)?([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)\s+(\d+)\s*(\*|[a-zA-Z0-9_]+)?\s*$/',
328
                          $line,
329
                          $matches);
330
          if ($n===0)
331
          {
332 1
            throw new RuntimeException("Illegal format at line %d in file '%s'.", $index + 1, $this->constantsFilename);
333
          }
334 1
335 1
          if (isset($matches[6]))
336
          {
337 1
            $schemaName   = $matches[2];
338 1
            $tableName    = $matches[3];
339
            $columnName   = $matches[4];
340 1
            $length       = $matches[5];
341
            $constantName = $matches[6];
342
343
            if ($schemaName)
344
            {
345
              $tableName = $schemaName.'.'.$tableName;
346
            }
347
348
            $this->oldColumns[$tableName][$columnName] = ['table_name'    => $tableName,
349
                                                          'column_name'   => $columnName,
350
                                                          'length'        => $length,
351
                                                          'constant_name' => $constantName];
352 1
          }
353
        }
354 1
      }
355
    }
356 1
  }
357
358 1
  //--------------------------------------------------------------------------------------------------------------------
359 1
  /**
360
   * Logs the number of constants generated.
361 1
   */
362 1
  private function logNumberOfConstants(): void
363
  {
364 1
    $countIds    = count($this->labels);
365 1
    $countWidths = count($this->constants) - $countIds;
366
367
    $this->io->writeln('');
368
    $this->io->text(sprintf('Number of constants based on column widths: %d', $countWidths));
369
    $this->io->text(sprintf('Number of constants based on database IDs : %d', $countIds));
370
  }
371
372 1
  //--------------------------------------------------------------------------------------------------------------------
373
  /**
374
   * Generates PHP code with constant declarations.
375
   */
376
  private function makeConstantStatements(): array
377
  {
378
    $width1    = 0;
379
    $width2    = 0;
380
    $constants = [];
381
382
    foreach ($this->constants as $constant => $value)
383
    {
384
      $width1 = max(mb_strlen($constant), $width1);
385
      $width2 = max(mb_strlen((string)$value), $width2);
386
    }
387
388
    $format = sprintf('  const %%-%ds = %%%dd;', $width1, $width2);
389
    foreach ($this->constants as $constant => $value)
390
    {
391
      $constants[] = sprintf($format, $constant, $value);
392 1
    }
393
394
    return $constants;
395
  }
396
397 1
  //--------------------------------------------------------------------------------------------------------------------
398 1
  /**
399
   * Preserves relevant data in $oldColumns into $columns.
400
   */
401
  private function mergeColumns(): void
402
  {
403
    foreach ($this->oldColumns as $tableName => $table)
404
    {
405
      foreach ($table as $columnName => $column)
406
      {
407
        if (isset($this->columns[$tableName][$columnName]))
408
        {
409 1
          $this->columns[$tableName][$columnName]['constant_name'] = $column['constant_name'];
410
        }
411 1
      }
412 1
    }
413
  }
414 1
415 1
  //--------------------------------------------------------------------------------------------------------------------
416 1
  /**
417
   * Writes table and column names, the width of the column, and the constant name (if assigned) to
418
   * $constantsFilename.
419
   */
420
  private function writeColumns(): void
421
  {
422
    ksort($this->columns);
423
424
    $content = '';
425 1
    foreach ($this->columns as $table)
426
    {
427 1
      $width1 = 0;
428 1
      $width2 = 0;
429 1
      foreach ($table as $column)
430
      {
431 1
        $width1 = max(mb_strlen($column['column_name']), $width1);
432
        $width2 = max(mb_strlen((string)$column['length']), $width2);
433 1
      }
434 1
435
      foreach ($table as $column)
436
      {
437 1
        if (isset($column['length']))
438 1
        {
439
          if (isset($column['constant_name']))
440 1
          {
441
            $format  = sprintf("%%s.%%-%ds %%%dd %%s\n", $width1, $width2);
442
            $content .= sprintf($format,
443 1
                                $column['table_name'],
444
                                $column['column_name'],
445
                                $column['length'],
446
                                $column['constant_name']);
447
          }
448
          else
449
          {
450 1
            $format  = sprintf("%%s.%%-%ds %%%dd\n", $width1, $width2);
451
            $content .= sprintf($format,
452 1
                                $column['table_name'],
453
                                $column['column_name'],
454
                                $column['length']);
455
          }
456
        }
457
      }
458
459
      $content .= "\n";
460
    }
461
462
    // Save the columns, width and constants to the filesystem.
463
    Util::writeTwoPhases($this->constantsFilename, $content, $this->io);
464
  }
465
466
  //--------------------------------------------------------------------------------------------------------------------
467
  /**
468
   * Inserts new and replace old (if any) constant declaration statements in a PHP source file.
469 1
   */
470
  private function writeConstantClass(): void
471 1
  {
472
    // Read the source of the class without actually loading the class. Otherwise, we can not (re)load the class in
473 1
    // MySqlRoutineLoaderWorker::replacePairsConstants.
474 1
    $fileName    = ClassReflectionHelper::getFileName($this->className);
475
    $source      = file_get_contents($fileName);
476 1
    $sourceLines = explode(PHP_EOL, $source);
477 1
478 1
    // Search for the lines where to insert and replace constant declaration statements.
479
    $lineNumbers = $this->extractLines($source);
480 1
    if (!isset($lineNumbers[0]))
481 1
    {
482
      throw new RuntimeException("Annotation not found in '%s'.", $fileName);
483
    }
484 1
485
    // Generate the constant declaration statements.
486 1
    $constants = $this->makeConstantStatements();
487
488 1
    // Insert new and replace old (if any) constant declaration statements.
489
    if ($lineNumbers[2]===null)
0 ignored issues
show
The condition $lineNumbers[2] === null is always true.
Loading history...
490
    {
491
      $tmp1 = array_slice($sourceLines, 0, $lineNumbers[1]);
492
      $tmp2 = array_slice($sourceLines, $lineNumbers[1] + 0);
493
    }
494
    else
495
    {
496
      $tmp1 = array_slice($sourceLines, 0, $lineNumbers[1]);
497
      $tmp2 = array_slice($sourceLines, $lineNumbers[2] + 0);
498
    }
499 1
    $sourceLines = array_merge($tmp1, $constants, $tmp2);
500 1
501 1
    // Save the configuration file.
502 1
    Util::writeTwoPhases($fileName, implode(PHP_EOL, $sourceLines), $this->io);
503 1
  }
504
505
  //--------------------------------------------------------------------------------------------------------------------
506
}
507
508
//----------------------------------------------------------------------------------------------------------------------
509