Passed
Push — master ( 2e64cc...304ce4 )
by P.R.
03:51
created

MySqlRoutineLoaderWorker   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 620
Duplicated Lines 0 %

Test Coverage

Coverage 74.76%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 192
c 3
b 0
f 0
dl 0
loc 620
ccs 154
cts 206
cp 0.7476
rs 4.08
wmc 59

20 Methods

Rating   Name   Duplication   Size   Complexity  
A execute() 0 31 3
B replacePairsColumnTypesMaxLength() 0 34 8
A getDuplicates() 0 30 6
A detectNameConflicts() 0 31 6
A loadAll() 0 15 1
A removeObsoleteMetadata() 0 9 2
A logOverviewErrors() 0 6 2
A replacePairsConstants() 0 17 4
A loadList() 0 10 1
A loadStoredRoutines() 0 52 5
A findSourceFiles() 0 11 2
A getOldStoredRoutinesInfo() 0 8 2
A replacePairsColumnTypes() 0 8 1
A replacePairsColumnTypesExact() 0 15 3
A findSourceFilesFromList() 0 15 3
A methodName() 0 11 2
A dropObsoleteRoutines() 0 19 4
A maxCharacters() 0 8 2
A writeStoredRoutineMetadata() 0 3 1
A replacePairs() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like MySqlRoutineLoaderWorker often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MySqlRoutineLoaderWorker, and based on these observations, apply Extract Interface, too.

1
<?php
2
declare(strict_types=1);
3
4
namespace SetBased\Stratum\MySql\Backend;
5
6
use SetBased\Exception\RuntimeException;
7
use SetBased\Helper\InvalidCastException;
8
use SetBased\Stratum\Backend\RoutineLoaderWorker;
9
use SetBased\Stratum\Common\Exception\RoutineLoaderException;
10
use SetBased\Stratum\Common\Helper\SourceFinderHelper;
11
use SetBased\Stratum\Middle\Exception\ResultException;
12
use SetBased\Stratum\Middle\Helper\RowSetHelper;
13
use SetBased\Stratum\Middle\NameMangler\NameMangler;
14
use SetBased\Stratum\MySql\Exception\MySqlConnectFailedException;
15
use SetBased\Stratum\MySql\Exception\MySqlDataLayerException;
16
use SetBased\Stratum\MySql\Exception\MySqlQueryErrorException;
17
use SetBased\Stratum\MySql\Helper\RoutineLoaderHelper;
18
use SetBased\Stratum\MySql\Helper\SqlModeHelper;
19
use SetBased\Stratum\MySql\Helper\StratumMetadata;
20
use Symfony\Component\Console\Formatter\OutputFormatter;
21
22
/**
23
 * Command for loading stored routines into a MySQL instance from pseudo SQL files.
24
 */
25
class MySqlRoutineLoaderWorker extends MySqlWorker implements RoutineLoaderWorker
26
{
27
  //--------------------------------------------------------------------------------------------------------------------
28
  /**
29
   * The maximum column size in bytes.
30
   */
31
  const MAX_COLUMN_SIZE = 65532;
32
33
  /**
34
   * Details of all character sets.
35
   *
36
   * @var array[]
37
   */
38
  private $characterSets;
39
40
  /**
41
   * Name of the class that contains all constants.
42
   *
43
   * @var string
44
   */
45
  private $constantClassName;
46
47
  /**
48
   * The default character set under which the stored routine will be loaded and run.
49
   *
50
   * @var string
51
   */
52
  private $defaultCharacterSet;
53
54
  /**
55
   * The default collate under which the stored routine will be loaded and run.
56
   *
57
   * @var string
58
   */
59
  private $defaultCollate;
60
61
  /**
62
   * An array with source filenames that are not loaded into MySQL.
63
   *
64
   * @var string[]
65
   */
66
  private $errorFilenames = [];
67
68
  /**
69
   * Class name for mangling routine and parameter names.
70
   *
71
   * @var string
72
   */
73
  private $nameMangler;
74
75
  /**
76
   * Old metadata of all stored routines. Note: this data comes from information_schema.ROUTINES.
77
   *
78
   * @var array
79
   */
80
  private $rdbmsOldMetadata;
81
82
  /**
83
   * A map from placeholders to their actual values.
84
   *
85
   * @var array
86
   */
87
  private $replacePairs = [];
88
89
  /**
90
   * Pattern where of the sources files.
91
   *
92
   * @var string
93
   */
94
  private $sourcePattern;
95
96
  /**
97
   * All sources with stored routines. Each element is an array with the following keys:
98
   * <ul>
99
   * <li> path_name    The path the source file.
100
   * <li> routine_name The name of the routine (equals the basename of the path).
101
   * <li> method_name  The name of the method in the data layer for the wrapper method of the stored routine.
102
   * </ul>
103
   *
104
   * @var array[]
105
   */
106
  private $sources = [];
107
108
  /**
109
   * The SQL mode under which the stored routine will be loaded and run.
110
   *
111
   * @var string
112
   */
113
  private $sqlMode;
114
115
  /**
116
   * The metadata of all stored routines. Note: this data is stored in the metadata file and is generated by PhpStratum.
117
   *
118
   * @var StratumMetadata
119
   */
120
  private $stratumMetaData;
121
122
  //--------------------------------------------------------------------------------------------------------------------
123
  /**
124
   * @inheritdoc
125
   *
126
   * @throws InvalidCastException
127
   * @throws MySqlConnectFailedException
128
   * @throws MySqlDataLayerException
129
   * @throws MySqlQueryErrorException
130
   * @throws RuntimeException
131
   * @throws \ReflectionException
132
   * @throws \RuntimeException
133
   */
134 1
  public function execute(?array $sources = null): int
135
  {
136 1
    $this->io->title('PhpStratum: MySql Loader');
137
138 1
    $metadataFilename          = $this->settings->manString('loader.metadata');
139 1
    $this->sourcePattern       = $this->settings->manString('loader.sources');
140 1
    $this->sqlMode             = $this->settings->manString('loader.sql_mode');
141 1
    $this->defaultCharacterSet = $this->settings->manString('loader.character_set');
142 1
    $this->defaultCollate      = $this->settings->manString('loader.collate');
143 1
    $this->constantClassName   = $this->settings->optString('constants.class');
144 1
    $this->nameMangler         = $this->settings->optString('wrapper.mangler_class');
145
146 1
    $this->connect();
147
148 1
    $this->stratumMetaData = new StratumMetadata($metadataFilename, RoutineLoaderHelper::METADATA_REVISION);
149 1
    $this->characterSets   = $this->dl->allCharacterSets();
150
151 1
    if (empty($sources))
152
    {
153 1
      $this->loadAll();
154
    }
155
    else
156
    {
157
      $this->loadList($sources);
158
    }
159
160 1
    $this->logOverviewErrors();
161
162 1
    $this->disconnect();
163
164 1
    return ($this->errorFilenames) ? 1 : 0;
165
  }
166
167
  //--------------------------------------------------------------------------------------------------------------------
168
  /**
169
   * Detects stored routines that would result in duplicate wrapper method name.
170
   */
171 1
  private function detectNameConflicts(): void
172
  {
173
    // Get same method names from array
174 1
    [$sourcesByPath, $sourcesByMethod] = $this->getDuplicates();
175
176
    // Add every not unique method name to myErrorFileNames
177 1
    foreach ($sourcesByPath as $source)
178
    {
179
      $this->errorFilenames[] = $source['path_name'];
180
    }
181
182
    // Log the sources files with duplicate method names.
183 1
    foreach ($sourcesByMethod as $method => $sources)
184
    {
185
      $tmp = [];
186
      foreach ($sources as $source)
187
      {
188
        $tmp[] = $source['path_name'];
189
      }
190
191
      $this->io->error(sprintf("The following source files would result wrapper methods with equal name '%s'",
192
                               $method));
193
      $this->io->listing($tmp);
194
    }
195
196
    // Remove duplicates from sources.
197 1
    foreach ($this->sources as $key => $source)
198
    {
199 1
      if (isset($sourcesByPath[$source['path_name']]))
200
      {
201
        unset($this->sources[$key]);
202
      }
203
    }
204 1
  }
205
206
  //--------------------------------------------------------------------------------------------------------------------
207
  /**
208
   * Drops obsolete stored routines (i.e. stored routines that exits in the current schema but for which we don't have
209
   * a source file).
210
   *
211
   * @throws MySqlQueryErrorException
212
   */
213 1
  private function dropObsoleteRoutines(): void
214
  {
215
    // Make a lookup table from routine name to source.
216 1
    $lookup = [];
217 1
    foreach ($this->sources as $source)
218
    {
219 1
      $lookup[$source['routine_name']] = $source;
220
    }
221
222
    // Drop all routines not longer in sources.
223 1
    foreach ($this->rdbmsOldMetadata as $oldRoutine)
224
    {
225 1
      if (!isset($lookup[$oldRoutine['routine_name']]))
226
      {
227
        $this->io->logInfo('Dropping %s <dbo>%s</dbo>',
228
                           strtolower($oldRoutine['routine_type']),
229
                           $oldRoutine['routine_name']);
230
231
        $this->dl->dropRoutine($oldRoutine['routine_type'], $oldRoutine['routine_name']);
232
      }
233
    }
234 1
  }
235
236
  //--------------------------------------------------------------------------------------------------------------------
237
  /**
238
   * Searches recursively for all source files.
239
   */
240 1
  private function findSourceFiles(): void
241
  {
242 1
    $helper    = new SourceFinderHelper(dirname($this->settings->manString('stratum.config_path')));
243 1
    $filenames = $helper->findSources($this->sourcePattern);
244
245 1
    foreach ($filenames as $filename)
246
    {
247 1
      $routineName     = pathinfo($filename, PATHINFO_FILENAME);
248 1
      $this->sources[] = ['path_name'    => $filename,
249 1
                          'routine_name' => $routineName,
250 1
                          'method_name'  => $this->methodName($routineName)];
251
    }
252 1
  }
253
254
  //--------------------------------------------------------------------------------------------------------------------
255
  /**
256
   * Finds all source files that actually exists from a list of file names.
257
   *
258
   * @param string[] $sources The list of file names.
259
   */
260
  private function findSourceFilesFromList(array $sources): void
261
  {
262
    foreach ($sources as $path)
263
    {
264
      if (!file_exists($path))
265
      {
266
        $this->io->error(sprintf("File not exists: '%s'", $path));
267
        $this->errorFilenames[] = $path;
268
      }
269
      else
270
      {
271
        $routineName     = pathinfo($path, PATHINFO_FILENAME);
272
        $this->sources[] = ['path_name'    => $path,
273
                            'routine_name' => $routineName,
274
                            'method_name'  => $this->methodName($routineName)];
275
      }
276
    }
277
  }
278
279
  //--------------------------------------------------------------------------------------------------------------------
280
  /**
281
   * Returns all elements in {@link $sources} with duplicate method names.
282
   *
283
   * @return array[]
284
   */
285 1
  private function getDuplicates(): array
286
  {
287
    // First pass make lookup table by method_name.
288 1
    $lookup = [];
289 1
    foreach ($this->sources as $source)
290
    {
291 1
      if (isset($source['method_name']))
292
      {
293 1
        if (!isset($lookup[$source['method_name']]))
294
        {
295 1
          $lookup[$source['method_name']] = [];
296
        }
297
298 1
        $lookup[$source['method_name']][] = $source;
299
      }
300
    }
301
302
    // Second pass find duplicate sources.
303 1
    $duplicatesSources = [];
304 1
    $duplicatesMethods = [];
305 1
    foreach ($this->sources as $source)
306
    {
307 1
      if (sizeof($lookup[$source['method_name']])>1)
308
      {
309
        $duplicatesSources[$source['path_name']]   = $source;
310
        $duplicatesMethods[$source['method_name']] = $lookup[$source['method_name']];
311
      }
312
    }
313
314 1
    return [$duplicatesSources, $duplicatesMethods];
315
  }
316
317
  //--------------------------------------------------------------------------------------------------------------------
318
  /**
319
   * Retrieves information about all stored routines in the current schema.
320
   *
321
   * @throws MySqlQueryErrorException
322
   */
323 1
  private function getOldStoredRoutinesInfo(): void
324
  {
325 1
    $this->rdbmsOldMetadata = [];
326
327 1
    $routines = $this->dl->allRoutines();
328 1
    foreach ($routines as $routine)
329
    {
330 1
      $this->rdbmsOldMetadata[$routine['routine_name']] = $routine;
331
    }
332 1
  }
333
334
  //--------------------------------------------------------------------------------------------------------------------
335
  /**
336
   * Loads all stored routines into MySQL.
337
   *
338
   * @throws InvalidCastException
339
   * @throws MySqlQueryErrorException
340
   * @throws RuntimeException
341
   * @throws \ReflectionException
342
   */
343 1
  private function loadAll(): void
344
  {
345 1
    $this->findSourceFiles();
346 1
    $this->detectNameConflicts();
347 1
    $this->replacePairs();
348 1
    $this->getOldStoredRoutinesInfo();
349
350 1
    $this->loadStoredRoutines();
351
352 1
    $this->dropObsoleteRoutines();
353 1
    $this->removeObsoleteMetadata();
354
355 1
    $this->io->writeln('');
356
357 1
    $this->writeStoredRoutineMetadata();
358 1
  }
359
360
  //--------------------------------------------------------------------------------------------------------------------
361
  /**
362
   * Loads all stored routines in a list into MySQL.
363
   *
364
   * @param string[] $sources The list of files to be loaded.
365
   *
366
   * @throws InvalidCastException
367
   * @throws MySqlQueryErrorException
368
   * @throws \LogicException
369
   * @throws \ReflectionException
370
   * @throws \RuntimeException
371
   */
372
  private function loadList(array $sources): void
373
  {
374
    $this->findSourceFilesFromList($sources);
375
    $this->detectNameConflicts();
376
    $this->replacePairs();
377
    $this->getOldStoredRoutinesInfo();
378
379
    $this->loadStoredRoutines();
380
381
    $this->writeStoredRoutineMetadata();
382
  }
383
384
  //--------------------------------------------------------------------------------------------------------------------
385
  /**
386
   * Loads all stored routines.
387
   *
388
   * @throws InvalidCastException
389
   * @throws MySqlQueryErrorException
390
   * @throws ResultException
391
   */
392 1
  private function loadStoredRoutines(): void
393
  {
394 1
    $this->io->writeln('');
395
396 1
    usort($this->sources, function ($a, $b) {
397 1
      return strcmp($a['routine_name'], $b['routine_name']);
398 1
    });
399
400 1
    $sqlModeHelper = new SqlModeHelper($this->dl, $this->sqlMode);
401
402 1
    foreach ($this->sources as $filename)
403
    {
404 1
      $routineName = $filename['routine_name'];
405
406 1
      $helper = new RoutineLoaderHelper($this->dl,
407 1
                                        $this->io,
408
                                        $sqlModeHelper,
409 1
                                        $filename['path_name'],
410 1
                                        $this->stratumMetaData->getMetadata($routineName),
411 1
                                        $this->replacePairs,
412 1
                                        $this->rdbmsOldMetadata[$routineName] ?? [],
413 1
                                        $this->defaultCharacterSet,
414 1
                                        $this->defaultCollate);
415
416
      try
417
      {
418 1
        $metadata = $helper->loadStoredRoutine();
419 1
        $this->stratumMetaData->putMetadata($routineName, $metadata);
420
      }
421
      catch (RoutineLoaderException $e)
422
      {
423
        $messages = [$e->getMessage(), sprintf("Failed to load file '%s'", $filename['path_name'])];
424
        $this->io->error($messages);
425
426
        $this->errorFilenames[] = $filename['path_name'];
427
        $this->stratumMetaData->delMetadata($routineName);
428
      }
429
      catch (MySqlQueryErrorException $e)
430
      {
431
        // Exception is caused by a SQL error. Log the message and the SQL statement with highlighting the error.
432
        $this->io->error($e->getMessage());
433
        $this->io->text($e->styledQuery());
434
435
        $this->errorFilenames[] = $filename['path_name'];
436
        $this->stratumMetaData->delMetadata($routineName);
437
      }
438
      catch (MySqlDataLayerException $e)
439
      {
440
        $this->io->error($e->getMessage());
441
442
        $this->errorFilenames[] = $filename['path_name'];
443
        $this->stratumMetaData->delMetadata($routineName);
444
      }
445
    }
446 1
  }
447
448
  //--------------------------------------------------------------------------------------------------------------------
449
  /**
450
   * Logs the source files that were not successfully loaded into MySQL.
451
   */
452 1
  private function logOverviewErrors(): void
453
  {
454 1
    if (!empty($this->errorFilenames))
455
    {
456
      $this->io->warning('Routines in the files below are not loaded:');
457
      $this->io->listing($this->errorFilenames);
458
    }
459 1
  }
460
461
  //--------------------------------------------------------------------------------------------------------------------
462
  /**
463
   * Returns the maximum number of characters in a VARCHAR or CHAR.
464
   *
465
   * @param string $characterSetName The name of the character set of the column.
466
   *
467
   * @return int
468
   */
469 1
  private function maxCharacters(string $characterSetName): ?int
470
  {
471 1
    $key = RowSetHelper::searchInRowSet($this->characterSets, 'character_set_name', $characterSetName);
472 1
    if ($key===null) return null;
473
474 1
    $size = $this->characterSets[$key]['maxlen'];
475
476 1
    return (int)floor(self::MAX_COLUMN_SIZE / $size);
477
  }
478
479
  //--------------------------------------------------------------------------------------------------------------------
480
  /**
481
   * Returns the method name in the wrapper for a stored routine. Returns null when name mangler is not set.
482
   *
483
   * @param string $routineName The name of the routine.
484
   *
485
   * @return null|string
486
   */
487 1
  private function methodName(string $routineName): ?string
488
  {
489 1
    if ($this->nameMangler!==null)
490
    {
491
      /** @var NameMangler $mangler */
492 1
      $mangler = $this->nameMangler;
493
494 1
      return $mangler::getMethodName($routineName);
495
    }
496
497
    return null;
498
  }
499
500
  //--------------------------------------------------------------------------------------------------------------------
501
  /**
502
   * Removes obsolete entries from the metadata of all stored routines.
503
   */
504 1
  private function removeObsoleteMetadata(): void
505
  {
506 1
    $routines = [];
507 1
    foreach ($this->sources as $source)
508
    {
509 1
      $routines[] = $source['routine_name'];
510
    }
511
512 1
    $this->stratumMetaData->purge($routines);
513 1
  }
514
515
  //--------------------------------------------------------------------------------------------------------------------
516
  /**
517
   * Gathers all replace pairs.
518
   *
519
   * @throws MySqlQueryErrorException
520
   * @throws \ReflectionException
521
   */
522 1
  private function replacePairs(): void
523
  {
524 1
    $this->replacePairsColumnTypes();
525 1
    $this->replacePairsConstants();
526 1
  }
527
528
  //--------------------------------------------------------------------------------------------------------------------
529
  /**
530
   * Selects schema, table, column names and the column type from MySQL and saves them as replace pairs.
531
   *
532
   * @throws MySqlQueryErrorException
533
   */
534 1
  private function replacePairsColumnTypes(): void
535
  {
536 1
    $columns = $this->dl->allTableColumns();
537
538 1
    $this->replacePairsColumnTypesExact($columns);
539 1
    $this->replacePairsColumnTypesMaxLength($columns);
540
541 1
    $this->io->text(sprintf('Selected %d column types for substitution', sizeof($columns)));
542 1
  }
543
544
  //--------------------------------------------------------------------------------------------------------------------
545
  /**
546
   * Gathers replace pairs based on exact column types.
547
   *
548
   * @param array[] $columns The details of all table columns.
549
   */
550 1
  private function replacePairsColumnTypesExact(array $columns): void
551
  {
552 1
    foreach ($columns as $column)
553
    {
554 1
      $key = mb_strtoupper('@'.$column['table_name'].'.'.$column['column_name'].'%type@');
555
556 1
      $value = $column['column_type'];
557
558
      // For VARCHAR and TEXT columns add character set.
559 1
      if ($column['character_set_name']!==null)
560
      {
561 1
        $value .= ' character set '.$column['character_set_name'];
562
      }
563
564 1
      $this->replacePairs[$key] = $value;
565
    }
566 1
  }
567
568
  //--------------------------------------------------------------------------------------------------------------------
569
  /**
570
   * Gathers replace pairs based on column types with maximum length.
571
   *
572
   * @param array[] $columns The details of all table columns.
573
   */
574 1
  private function replacePairsColumnTypesMaxLength(array $columns): void
575
  {
576 1
    foreach ($columns as $column)
577
    {
578 1
      $key = mb_strtoupper('@'.$column['table_name'].'.'.$column['column_name'].'%sort@');
579
580 1
      switch ($column['data_type'])
581
      {
582 1
        case 'char':
583 1
        case 'varchar':
584 1
          $max = $this->maxCharacters($column['character_set_name']);
585 1
          if ($max!==null)
586
          {
587 1
            $value = sprintf('%s(%d) character set %s',
588 1
                             $column['data_type'],
589
                             $max,
590 1
                             $column['character_set_name']);
591
          }
592
          else
593
          {
594
            $value = null;
595
          }
596 1
          break;
597
598 1
        case 'binary':
599 1
        case 'varbinary':
600 1
          $value = sprintf('%s(%d)', $column['data_type'], self::MAX_COLUMN_SIZE);
601 1
          break;
602
603
        default:
604 1
          $value = null;
605
      }
606
607 1
      if ($value!==null) $this->replacePairs[$key] = $value;
608
    }
609 1
  }
610
611
  //--------------------------------------------------------------------------------------------------------------------
612
  /**
613
   * Reads constants set the PHP configuration file and  adds them to the replace pairs.
614
   *
615
   * @throws \ReflectionException
616
   */
617 1
  private function replacePairsConstants(): void
618
  {
619 1
    if (!isset($this->constantClassName)) return;
620
621 1
    $reflection = new \ReflectionClass($this->constantClassName);
622
623 1
    $constants = $reflection->getConstants();
624 1
    foreach ($constants as $name => $value)
625
    {
626
      if (!is_numeric($value)) $value = "'".$value."'";
627
628
      $this->replacePairs['@'.$name.'@'] = $value;
629
    }
630
631 1
    $this->io->text(sprintf('Read %d constants for substitution from <fso>%s</fso>',
632 1
                            sizeof($constants),
633 1
                            OutputFormatter::escape($reflection->getFileName())));
634 1
  }
635
636
  //--------------------------------------------------------------------------------------------------------------------
637
  /**
638
   * Writes the metadata of all stored routines to the metadata file.
639
   *
640
   * @throws RuntimeException
641
   */
642 1
  private function writeStoredRoutineMetadata(): void
643
  {
644 1
    $this->stratumMetaData->writeMetadata($this->io);
645 1
  }
646
647
  //--------------------------------------------------------------------------------------------------------------------
648
}
649
650
//----------------------------------------------------------------------------------------------------------------------
651