Passed
Push — master ( e0c5e8...7926db )
by P.R.
04:00
created

MysqlConstantWorker::loadOldColumns()   B

Complexity

Conditions 9
Paths 17

Size

Total Lines 49
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 21.4523

Importance

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

358
      while (($line = fgets(/** @scrutinizer ignore-type */ $handle)))
Loading history...
359
      {
360 1
        $line_number++;
361 1
        if ($line!="\n")
362
        {
363 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);
364 1
          if ($n==0)
365
          {
366
            throw new RuntimeException("Illegal format at line %d in file '%s'.",
367
                                       $line_number,
368
                                       $this->constantsFilename);
369
          }
370
371 1
          if (isset($matches[6]))
372
          {
373
            $schema_name   = $matches[2];
374
            $table_name    = $matches[3];
375
            $column_name   = $matches[4];
376
            $length        = $matches[5];
377
            $constant_name = $matches[6];
378
379
            if ($schema_name)
380
            {
381
              $table_name = $schema_name.'.'.$table_name;
382
            }
383
384
            $this->oldColumns[$table_name][$column_name] = ['table_name'    => $table_name,
385
                                                            'column_name'   => $column_name,
386
                                                            'length'        => $length,
387
                                                            'constant_name' => $constant_name];
388
          }
389
        }
390
      }
391 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

391
      if (!feof(/** @scrutinizer ignore-type */ $handle))
Loading history...
392
      {
393
        throw new RuntimeException("Error reading from file '%s'.", $this->constantsFilename);
394
      }
395
396 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

396
      $ok = fclose(/** @scrutinizer ignore-type */ $handle);
Loading history...
397 1
      if ($ok===false)
398
      {
399
        throw new RuntimeException("Error closing file '%s'.", $this->constantsFilename);
400
      }
401
    }
402 1
  }
403
404
  //--------------------------------------------------------------------------------------------------------------------
405
  /**
406
   * Logs the number of constants generated.
407
   */
408 1
  private function logNumberOfConstants(): void
409
  {
410 1
    $n_id  = sizeof($this->labels);
411 1
    $n_len = sizeof($this->constants) - $n_id;
412
413 1
    $this->io->writeln('');
414 1
    $this->io->text(sprintf('Number of constants based on column widths: %d', $n_len));
415 1
    $this->io->text(sprintf('Number of constants based on database IDs : %d', $n_id));
416 1
  }
417
418
  //--------------------------------------------------------------------------------------------------------------------
419
  /**
420
   * Generates PHP code with constant declarations.
421
   *
422
   * @return array The generated PHP code, lines are stored as rows in the array.
423
   */
424 1
  private function makeConstantStatements(): array
425
  {
426 1
    $width1    = 0;
427 1
    $width2    = 0;
428 1
    $constants = [];
429
430 1
    foreach ($this->constants as $constant => $value)
431
    {
432 1
      $width1 = max(mb_strlen($constant), $width1);
433 1
      $width2 = max(mb_strlen((string)$value), $width2);
434
    }
435
436 1
    $line_format = sprintf('  const %%-%ds = %%%dd;', $width1, $width2);
437 1
    foreach ($this->constants as $constant => $value)
438
    {
439 1
      $constants[] = sprintf($line_format, $constant, $value);
440
    }
441
442 1
    return $constants;
443
  }
444
445
  //--------------------------------------------------------------------------------------------------------------------
446
  /**
447
   * Preserves relevant data in $oldColumns into $columns.
448
   */
449 1
  private function mergeColumns(): void
450
  {
451 1
    foreach ($this->oldColumns as $table_name => $table)
452
    {
453
      foreach ($table as $column_name => $column)
454
      {
455
        if (isset($this->columns[$table_name][$column_name]))
456
        {
457
          $this->columns[$table_name][$column_name]['constant_name'] = $column['constant_name'];
458
        }
459
      }
460
    }
461 1
  }
462
463
  //--------------------------------------------------------------------------------------------------------------------
464
  /**
465
   * Writes table and column names, the width of the column, and the constant name (if assigned) to
466
   * $constantsFilename.
467
   */
468 1
  private function writeColumns(): void
469
  {
470 1
    ksort($this->columns);
471
472 1
    $content = '';
473 1
    foreach ($this->columns as $table)
474
    {
475 1
      $width1 = 0;
476 1
      $width2 = 0;
477 1
      foreach ($table as $column)
478
      {
479 1
        $width1 = max(mb_strlen($column['column_name']), $width1);
480 1
        $width2 = max(mb_strlen((string)$column['length']), $width2);
481
      }
482
483 1
      foreach ($table as $column)
484
      {
485 1
        if (isset($column['length']))
486
        {
487 1
          if (isset($column['constant_name']))
488
          {
489
            $line_format = sprintf("%%s.%%-%ds %%%dd %%s\n", $width1, $width2);
490
            $content     .= sprintf($line_format,
491
                                    $column['table_name'],
492
                                    $column['column_name'],
493
                                    $column['length'],
494
                                    $column['constant_name']);
495
          }
496
          else
497
          {
498 1
            $line_format = sprintf("%%s.%%-%ds %%%dd\n", $width1, $width2);
499 1
            $content     .= sprintf($line_format,
500 1
                                    $column['table_name'],
501 1
                                    $column['column_name'],
502 1
                                    $column['length']);
503
          }
504
        }
505
      }
506
507 1
      $content .= "\n";
508
    }
509
510
    // Save the columns, width and constants to the filesystem.
511 1
    $this->writeTwoPhases($this->constantsFilename, $content);
512 1
  }
513
514
  //--------------------------------------------------------------------------------------------------------------------
515
  /**
516
   * Inserts new and replace old (if any) constant declaration statements in a PHP source file.
517
   *
518
   * @throws RuntimeException
519
   */
520 1
  private function writeConstantClass(): void
521
  {
522
    // Get the class loader.
523
    /** @var ClassLoader $loader */
524 1
    $loader = spl_autoload_functions()[0][0];
525
526
    // Find the source file of the constant class.
527 1
    $file_name = $loader->findFile($this->className);
528 1
    if ($file_name===false)
529
    {
530
      throw new RuntimeException("ClassLoader can not find class '%s'.", $this->className);
531
    }
532
533
    // Read the source of the class without actually loading the class. Otherwise, we can not (re)load the class in
534
    // \SetBased\Stratum\MySqlRoutineLoaderWorker::replacePairsConstants.
535 1
    $source = file_get_contents($file_name);
536 1
    if ($source===false)
537
    {
538
      throw new RuntimeException("Unable the open source file '%s'.", $file_name);
539
    }
540 1
    $source_lines = explode("\n", $source);
541
542
    // Search for the lines where to insert and replace constant declaration statements.
543 1
    $line_numbers = $this->extractLines($source);
544 1
    if (!isset($line_numbers[0]))
545
    {
546
      throw new RuntimeException("Annotation not found in '%s'.", $file_name);
547
    }
548
549
    // Generate the constant declaration statements.
550 1
    $constants = $this->makeConstantStatements();
551
552
    // Insert new and replace old (if any) constant declaration statements.
553 1
    $tmp1         = array_splice($source_lines, 0, $line_numbers[1]);
554 1
    $tmp2         = array_splice($source_lines, (isset($line_numbers[2])) ? $line_numbers[2] - $line_numbers[1] : 0);
555 1
    $source_lines = array_merge($tmp1, $constants, $tmp2);
556
557
    // Save the configuration file.
558 1
    $this->writeTwoPhases($file_name, implode("\n", $source_lines));
559 1
  }
560
561
  //--------------------------------------------------------------------------------------------------------------------
562
}
563
564
//----------------------------------------------------------------------------------------------------------------------
565