Passed
Push — master ( 304ce4...56185b )
by P.R.
04:05
created

MySqlRoutineLoaderWorker::loadStoredRoutines()   A

Complexity

Conditions 5
Paths 8

Size

Total Lines 52
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 7.0935

Importance

Changes 0
Metric Value
cc 5
eloc 32
c 0
b 0
f 0
nc 8
nop 0
dl 0
loc 52
ccs 18
cts 32
cp 0.5625
crap 7.0935
rs 9.0968

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\StratumMetadataHelper;
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 StratumMetadataHelper
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 StratumMetadataHelper($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
      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
      $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