Passed
Push — master ( b9048c...863dbf )
by P.R.
03:51
created

extractColumnsFromTableDescription()   D

Complexity

Conditions 30
Paths 30

Size

Total Lines 107
Code Lines 77

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 65
CRAP Score 32.1317

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 30
eloc 77
c 1
b 0
f 0
nc 30
nop 1
dl 0
loc 107
ccs 65
cts 75
cp 0.8667
crap 32.1317
rs 4.1666

How to fix   Long Method    Complexity   

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\Helper;
5
6
use SetBased\Exception\FallenException;
7
use SetBased\Helper\Cast;
8
use SetBased\Helper\InvalidCastException;
9
use SetBased\Stratum\Backend\StratumStyle;
10
use SetBased\Stratum\Common\DocBlock\DocBlockReflection;
11
use SetBased\Stratum\Common\Exception\RoutineLoaderException;
12
use SetBased\Stratum\Middle\Exception\ResultException;
13
use SetBased\Stratum\MySql\Exception\MySqlQueryErrorException;
14
use SetBased\Stratum\MySql\MySqlMetaDataLayer;
15
use Symfony\Component\Console\Formatter\OutputFormatter;
16
17
/**
18
 * Class for loading a single stored routine into a MySQL instance from pseudo SQL file.
19
 */
20
class RoutineLoaderHelper
21
{
22
  //--------------------------------------------------------------------------------------------------------------------
23
  /**
24
   * MySQL's and MariaDB's SQL/PSM syntax.
25
   */
26
  const SQL_PSM_SYNTAX = 1;
27
28
  /**
29
   * Oracle PL/SQL syntax.
30
   */
31
  const PL_SQL_SYNTAX = 2;
32
33
  /**
34
   * The revision of the metadata of the stored routines.
35
   */
36
  const METADATA_REVISION = '3';
37
38
  /**
39
   * The metadata of the table columns of the table for bulk insert.
40
   *
41
   * @var array[]
42
   */
43
  private $bulkInsertColumns;
44
45
  /**
46
   * The keys in the nested array for bulk inserting data.
47
   *
48
   * @var string[]
49
   */
50
  private $bulkInsertKeys;
51
52
  /**
53
   * The name of table for bulk insert.
54
   *
55
   * @var string
56
   */
57
  private $bulkInsertTableName;
58
59
  /**
60
   * The default character set under which the stored routine will be loaded and run.
61
   *
62
   * @var string
63
   */
64
  private $characterSet;
65
66
  /**
67
   * The default collate under which the stored routine will be loaded and run.
68
   *
69
   * @var string
70
   */
71
  private $collate;
72
73
  /**
74
   * The designation type of the stored routine.
75
   *
76
   * @var string
77
   */
78
  private $designationType;
79
80
  /**
81
   * The meta data layer.
82
   *
83
   * @var MySqlMetaDataLayer
84
   */
85
  private $dl;
86
87
  /**
88
   * The DocBlock reflection object.
89
   *
90
   * @var DocBlockReflection|null
91
   */
92
  private $docBlockReflection;
93
94
  /**
95
   * The last modification time of the source file.
96
   *
97
   * @var int
98
   */
99
  private $filemtime;
100
101
  /**
102
   * The key or index columns (depending on the designation type) of the stored routine.
103
   *
104
   * @var string[]
105
   */
106
  private $indexColumns;
107
108
  /**
109
   * The Output decorator.
110
   *
111
   * @var StratumStyle
112
   */
113
  private $io;
114
115
  /**
116
   * The metadata of the stored routine. Note: this data is stored in the metadata file and is generated by PhpStratum.
117
   *
118
   * @var array
119
   */
120
  private $phpStratumMetadata;
121
122
  /**
123
   * The old metadata of the stored routine.  Note: this data comes from the metadata file.
124
   *
125
   * @var array
126
   */
127
  private $phpStratumOldMetadata;
128
129
  /**
130
   * The old metadata of the stored routine. Note: this data comes from information_schema.ROUTINES.
131
   *
132
   * @var array
133
   */
134
  private $rdbmsOldRoutineMetadata;
135
136
  /**
137
   * The replace pairs (i.e. placeholders and their actual values, see strst).
138
   *
139
   * @var array
140
   */
141
  private $replace = [];
142
143
  /**
144
   * A map from placeholders to their actual values.
145
   *
146
   * @var array
147
   */
148
  private $replacePairs;
149
150
  /**
151
   * The return type of the stored routine (only if designation type singleton0, singleton1, or function).
152
   *
153
   * @var string|null
154
   */
155
  private $returnType;
156
157
  /**
158
   * The name of the stored routine.
159
   *
160
   * @var string
161
   */
162
  private $routineName;
163
164
  /**
165
   * The routine parameters.
166
   *
167
   * @var RoutineParametersHelper|null
168
   */
169
  private $routineParameters = null;
170
171
  /**
172
   * The source code as a single string of the stored routine.
173
   *
174
   * @var string
175
   */
176
  private $routineSourceCode;
177
178
  /**
179
   * The source code as an array of lines string of the stored routine.
180
   *
181
   * @var array
182
   */
183
  private $routineSourceCodeLines;
184
185
  /**
186
   * The source filename holding the stored routine.
187
   *
188
   * @var string
189
   */
190
  private $sourceFilename;
191
192
  /**
193
   * The SQL mode helper object.
194
   *
195
   * @var SqlModeHelper
196
   */
197
  private $sqlModeHelper;
198
199
  /**
200
   * The syntax of the stored routine. Either SQL_PSM_SYNTAX or PL_SQL_SYNTAX.
201
   *
202
   * @var int
203
   */
204
  private $syntax;
205
206
  //--------------------------------------------------------------------------------------------------------------------
207
  /**
208
   * Object constructor.
209
   *
210
   * @param MySqlMetaDataLayer $dl                      The meta data layer.
211
   * @param StratumStyle       $io                      The output for log messages.
212
   * @param SqlModeHelper      $sqlModeHelper
213
   * @param string             $routineFilename         The filename of the source of the stored routine.
214
   * @param array              $phpStratumMetadata      The metadata of the stored routine from PhpStratum.
215
   * @param array              $replacePairs            A map from placeholders to their actual values.
216
   * @param array              $rdbmsOldRoutineMetadata The old metadata of the stored routine from MySQL.
217
   * @param string             $characterSet            The default character set under which the stored routine will
218
   *                                                    be loaded and run.
219
   * @param string             $collate                 The key or index columns (depending on the designation type) of
220
   *                                                    the stored routine.
221
   */
222 1
  public function __construct(MySqlMetaDataLayer $dl,
223
                              StratumStyle $io,
224
                              SqlModeHelper $sqlModeHelper,
225
                              string $routineFilename,
226
                              array $phpStratumMetadata,
227
                              array $replacePairs,
228
                              array $rdbmsOldRoutineMetadata,
229
                              string $characterSet,
230
                              string $collate)
231
  {
232 1
    $this->dl                      = $dl;
233 1
    $this->io                      = $io;
234 1
    $this->sqlModeHelper           = $sqlModeHelper;
235 1
    $this->sourceFilename          = $routineFilename;
236 1
    $this->phpStratumMetadata      = $phpStratumMetadata;
237 1
    $this->replacePairs            = $replacePairs;
238 1
    $this->rdbmsOldRoutineMetadata = $rdbmsOldRoutineMetadata;
239 1
    $this->characterSet            = $characterSet;
240 1
    $this->collate                 = $collate;
241 1
  }
242
243
  //--------------------------------------------------------------------------------------------------------------------
244
  /**
245
   * Extract column metadata from the rows returned by the SQL statement 'describe table'.
246
   *
247
   * @param array $description The description of the table.
248
   *
249
   * @return array
250
   *
251
   * @throws InvalidCastException
252
   */
253 1
  private static function extractColumnsFromTableDescription(array $description): array
254
  {
255 1
    $ret = [];
256
257 1
    foreach ($description as $column)
258
    {
259 1
      preg_match('/^(?<data_type>\w+)(?<extra>.*)?$/', $column['Type'], $parts1);
260
261 1
      $tmp = ['column_name'       => $column['Field'],
262 1
              'data_type'         => $parts1['data_type'],
263
              'numeric_precision' => null,
264
              'numeric_scale'     => null,
265 1
              'dtd_identifier'    => $column['Type']];
266
267 1
      switch ($parts1[1])
268
      {
269 1
        case 'tinyint':
270 1
          preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2);
271 1
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 4);
272 1
          $tmp['numeric_scale']     = 0;
273 1
          break;
274
275 1
        case 'smallint':
276 1
          preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2);
277 1
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 6);
278 1
          $tmp['numeric_scale']     = 0;
279 1
          break;
280
281 1
        case 'mediumint':
282 1
          preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2);
283 1
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 9);
284 1
          $tmp['numeric_scale']     = 0;
285 1
          break;
286
287 1
        case 'int':
288 1
          preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2);
289 1
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 11);
290 1
          $tmp['numeric_scale']     = 0;
291 1
          break;
292
293 1
        case 'bigint':
294 1
          preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2);
295 1
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 20);
296 1
          $tmp['numeric_scale']     = 0;
297 1
          break;
298
299 1
        case 'year':
300
          // Nothing to do.
301 1
          break;
302
303 1
        case 'float':
304 1
          $tmp['numeric_precision'] = 12;
305 1
          break;
306
307 1
        case 'double':
308 1
          $tmp['numeric_precision'] = 22;
309 1
          break;
310
311 1
        case 'binary':
312 1
        case 'char':
313 1
        case 'varbinary':
314 1
        case 'varchar':
315
          // Nothing to do (binary) strings.
316 1
          break;
317
318 1
        case 'decimal':
319 1
          preg_match('/^\((?<precision>\d+),(<?scale>\d+)\)$/', $parts1['extra'], $parts2);
320 1
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 65);
321 1
          $tmp['numeric_scale']     = Cast::toManInt($parts2['scale'] ?? 0);
322 1
          break;
323
324 1
        case 'time':
325 1
        case 'timestamp':
326 1
        case 'date':
327 1
        case 'datetime':
328
          // Nothing to do date and time.
329 1
          break;
330
331 1
        case 'enum':
332 1
        case 'set':
333
          // Nothing to do sets.
334 1
          break;
335
336 1
        case 'bit':
337 1
          preg_match('/^\((?<precision>\d+)\)$/', $parts1['extra'], $parts2);
338 1
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision']);
339 1
          break;
340
341
        case 'tinytext':
342
        case 'text':
343
        case 'mediumtext':
344
        case 'longtext':
345
        case 'tinyblob':
346
        case 'blob':
347
        case 'mediumblob':
348
        case 'longblob':
349
          // Nothing to do CLOBs and BLOBs.
350
          break;
351
352
        default:
353
          throw new FallenException('data type', $parts1[1]);
354
      }
355
356 1
      $ret[] = $tmp;
357
    }
358
359 1
    return $ret;
360
  }
361
362
  //--------------------------------------------------------------------------------------------------------------------
363
  /**
364
   * Loads the stored routine into the instance of MySQL and returns the metadata of the stored routine.
365
   *
366
   * @return array
367
   *
368
   * @throws RoutineLoaderException
369
   * @throws MySqlQueryErrorException
370
   * @throws ResultException
371
   * @throws InvalidCastException
372
   */
373 1
  public function loadStoredRoutine(): array
374
  {
375 1
    $this->routineName           = pathinfo($this->sourceFilename, PATHINFO_FILENAME);
376 1
    $this->phpStratumOldMetadata = $this->phpStratumMetadata;
377 1
    $this->filemtime             = filemtime($this->sourceFilename);
378
379 1
    $load = $this->mustLoadStoredRoutine();
380 1
    if ($load)
381
    {
382 1
      $this->io->text(sprintf('Loading routine <dbo>%s</dbo>', OutputFormatter::escape($this->routineName)));
383
384 1
      $this->readSourceCode();
385 1
      $this->extractPlaceholders();
386 1
      $this->extractDesignationType();
387 1
      $this->extractReturnType();
388 1
      $this->extractRoutineTypeAndName();
389 1
      $this->extractSyntax();
390 1
      $this->validateReturnType();
391
392 1
      $this->loadRoutineFile();
393
394 1
      $this->extractBulkInsertTableColumnsInfo();
395 1
      $this->extractParameters();
396 1
      $this->updateMetadata();
397
    }
398
399 1
    return $this->phpStratumMetadata;
400
  }
401
402
  //--------------------------------------------------------------------------------------------------------------------
403
  /**
404
   * Drops the stored routine if it exists.
405
   *
406
   * @throws MySqlQueryErrorException
407
   */
408 1
  private function dropRoutineIfExists(): void
409
  {
410 1
    if (!empty($this->rdbmsOldRoutineMetadata))
411
    {
412
      $this->dl->dropRoutine($this->rdbmsOldRoutineMetadata['routine_type'], $this->routineName);
413
    }
414 1
  }
415
416
  //--------------------------------------------------------------------------------------------------------------------
417
  /**
418
   *  Extracts the column names and column types of the current table for bulk insert.
419
   *
420
   * @throws InvalidCastException
421
   * @throws MySqlQueryErrorException
422
   * @throws ResultException
423
   * @throws RoutineLoaderException
424
   */
425 1
  private function extractBulkInsertTableColumnsInfo(): void
426
  {
427
    // Return immediately if designation type is not appropriate for this method.
428 1
    if ($this->designationType!='bulk_insert') return;
429
430
    // Check if table is a temporary table or a non-temporary table.
431 1
    $tableIsNonTemporary = $this->dl->checkTableExists($this->bulkInsertTableName);
432
433
    // Create temporary table if table is non-temporary table.
434 1
    if (!$tableIsNonTemporary)
435
    {
436 1
      $this->dl->callProcedure($this->routineName);
437
    }
438
439
    // Get information about the columns of the table.
440 1
    $description = $this->dl->describeTable($this->bulkInsertTableName);
441
442
    // Drop temporary table if table is non-temporary.
443 1
    if (!$tableIsNonTemporary)
444
    {
445 1
      $this->dl->dropTemporaryTable($this->bulkInsertTableName);
446
    }
447
448
    // Check number of columns in the table match the number of fields given in the designation type.
449 1
    $n1 = sizeof($this->bulkInsertKeys);
450 1
    $n2 = sizeof($description);
451 1
    if ($n1!=$n2)
452
    {
453
      throw new RoutineLoaderException("Number of fields %d and number of columns %d don't match.", $n1, $n2);
454
    }
455
456 1
    $this->bulkInsertColumns = self::extractColumnsFromTableDescription($description);
457 1
  }
458
459
  //--------------------------------------------------------------------------------------------------------------------
460
  /**
461
   * Extracts the designation type of the stored routine.
462
   *
463
   * @throws RoutineLoaderException
464
   */
465 1
  private function extractDesignationType(): void
466
  {
467 1
    $tags = $this->docBlockReflection->getTags('type');
468 1
    if (count($tags)===0)
469
    {
470
      throw new RoutineLoaderException('Tag @type not found in DocBlock.');
471
    }
472 1
    elseif (count($tags)>1)
473
    {
474
      throw new RoutineLoaderException('Multiple @type tags found in DocBlock.');
475
    }
476
477 1
    $tag                   = $tags[0];
478 1
    $this->designationType = $tag['arguments']['type'];
479 1
    switch ($this->designationType)
480
    {
481 1
      case 'bulk_insert':
482 1
        $tag['arguments']['table']   = $tag['arguments']['ex1'];
483 1
        $tag['arguments']['columns'] = $tag['arguments']['ex2'];
484 1
        if ($tag['arguments']['table']==='' || $tag['arguments']['columns']==='' || $tag['description'][0]!='')
485
        {
486
          throw new RoutineLoaderException('Invalid @type tag. Expected: @type bulk_insert <table_name> <columns>');
487
        }
488 1
        $this->bulkInsertTableName = $tag['arguments']['table'];
489 1
        $this->bulkInsertKeys      = explode(',', $tag['arguments']['columns']);
490 1
        break;
491
492 1
      case 'rows_with_key':
493 1
      case 'rows_with_index':
494 1
        $tag['arguments']['columns'] = $tag['arguments']['ex1'];
495 1
        if ($tag['arguments']['columns']==='' || $tag['arguments']['ex2']!=='')
496
        {
497
          throw new RoutineLoaderException('Invalid @type tag. Expected: @type %s <columns>', $this->designationType);
498
        }
499 1
        $this->indexColumns = explode(',', $tag['arguments']['columns']);
500 1
        break;
501
502
      default:
503 1
        if ($tag['arguments']['ex1']!=='' || $tag['arguments']['ex2']!=='')
504
        {
505
          throw new RoutineLoaderException('Error: Expected: @type %s', $this->designationType);
506
        }
507
    }
508 1
  }
509
510
  //--------------------------------------------------------------------------------------------------------------------
511
  /**
512
   * Extracts DocBlock parts to be used by the wrapper generator.
513
   */
514 1
  private function extractDocBlockPartsWrapper(): array
515
  {
516 1
    return ['short_description' => $this->docBlockReflection->getShortDescription(),
517 1
            'long_description'  => $this->docBlockReflection->getLongDescription(),
518 1
            'parameters'        => $this->routineParameters->extractDocBlockPartsWrapper()];
519
  }
520
521
  //--------------------------------------------------------------------------------------------------------------------
522
  /**
523
   * Extracts routine parameters.
524
   *
525
   * @throws MySqlQueryErrorException
526
   * @throws RoutineLoaderException
527
   */
528 1
  private function extractParameters()
529
  {
530 1
    $this->routineParameters = new RoutineParametersHelper($this->dl,
531 1
                                                           $this->io,
532 1
                                                           $this->docBlockReflection,
533 1
                                                           $this->routineName);
534
535 1
    $this->routineParameters->extractRoutineParameters();
536 1
  }
537
538
  //--------------------------------------------------------------------------------------------------------------------
539
  /**
540
   * Extracts the placeholders from the stored routine source.
541
   *
542
   * @throws RoutineLoaderException
543
   */
544 1
  private function extractPlaceholders(): void
545
  {
546 1
    $unknown = [];
547
548 1
    preg_match_all('(@[A-Za-z0-9_.]+(%(type|sort))?@)', $this->routineSourceCode, $matches);
549 1
    if (!empty($matches[0]))
550
    {
551 1
      foreach ($matches[0] as $placeholder)
552
      {
553 1
        if (isset($this->replacePairs[strtoupper($placeholder)]))
554
        {
555 1
          $this->replace[$placeholder] = $this->replacePairs[strtoupper($placeholder)];
556
        }
557
        else
558
        {
559
          $unknown[] = $placeholder;
560
        }
561
      }
562
    }
563
564 1
    $this->logUnknownPlaceholders($unknown);
565 1
  }
566
567
  //--------------------------------------------------------------------------------------------------------------------
568
  /**
569
   * Extracts the return type of the stored routine.
570
   *
571
   * @throws RoutineLoaderException
572
   */
573 1
  private function extractReturnType(): void
574
  {
575 1
    $tags = $this->docBlockReflection->getTags('return');
576
577 1
    switch ($this->designationType)
578
    {
579 1
      case 'function':
580 1
      case 'singleton0':
581 1
      case 'singleton1':
582 1
        if (count($tags)===0)
583
        {
584
          throw new RoutineLoaderException('Tag @return not found in DocBlock.');
585
        }
586 1
        $tag = $tags[0];
587 1
        if ($tag['arguments']['type']==='')
588
        {
589
          throw new RoutineLoaderException('Invalid return tag. Expected: @return <type>.');
590
        }
591 1
        $this->returnType = $tag['arguments']['type'];
592 1
        break;
593
594
      default:
595 1
        if (count($tags)!==0)
596
        {
597
          throw new RoutineLoaderException('Redundant @type tag found in DocBlock.');
598
        }
599
    }
600 1
  }
601
602
  //--------------------------------------------------------------------------------------------------------------------
603
  /**
604
   * Extracts the name of the stored routine and the stored routine type (i.e. procedure or function) source.
605
   *
606
   * @throws RoutineLoaderException
607
   */
608 1
  private function extractRoutineTypeAndName(): void
609
  {
610 1
    $n = preg_match('/create\\s+(procedure|function)\\s+([a-zA-Z0-9_]+)/i', $this->routineSourceCode, $matches);
611 1
    if ($n==1)
612
    {
613 1
      if ($this->routineName!=$matches[2])
614
      {
615 1
        throw new RoutineLoaderException("Stored routine name '%s' does not corresponds with filename", $matches[2]);
616
      }
617
    }
618
    else
619
    {
620
      throw new RoutineLoaderException('Unable to find the stored routine name and type');
621
    }
622 1
  }
623
624
  //--------------------------------------------------------------------------------------------------------------------
625
  /**
626
   * Detects the syntax of the stored procedure. Either SQL/PSM or PL/SQL.
627
   */
628 1
  private function extractSyntax(): void
629
  {
630 1
    if ($this->sqlModeHelper->hasOracleMode())
631
    {
632 1
      $key1 = $this->findFirstMatchingLine('/^\s*(as|is)\s*$/i');
633 1
      $key2 = $this->findFirstMatchingLine('/^\s*begin\s*$/i');
634
635 1
      if ($key1!==null && $key2!==null && $key1<$key2)
636
      {
637
        $this->syntax = self::PL_SQL_SYNTAX;
638
      }
639
      else
640
      {
641 1
        $this->syntax = self::SQL_PSM_SYNTAX;
642
      }
643
    }
644
    else
645
    {
646
      $this->syntax = self::SQL_PSM_SYNTAX;
647
    }
648 1
  }
649
650
  //--------------------------------------------------------------------------------------------------------------------
651
  /**
652
   * Returns the key of the source line that match a regex pattern.
653
   *
654
   * @param string $pattern The regex pattern.
655
   *
656
   * @return int|null
657
   */
658 1
  private function findFirstMatchingLine(string $pattern): ?int
659
  {
660 1
    foreach ($this->routineSourceCodeLines as $key => $line)
661
    {
662 1
      if (preg_match($pattern, $line)===1)
663
      {
664 1
        return $key;
665
      }
666
    }
667
668 1
    return null;
669
  }
670
671
  //--------------------------------------------------------------------------------------------------------------------
672
  /**
673
   * Loads the stored routine into the database.
674
   *
675
   * @throws MySqlQueryErrorException
676
   */
677 1
  private function loadRoutineFile(): void
678
  {
679 1
    if ($this->syntax===self::PL_SQL_SYNTAX)
680
    {
681
      $this->sqlModeHelper->addIfRequiredOracleMode();
682
    }
683
    else
684
    {
685 1
      $this->sqlModeHelper->removeIfRequiredOracleMode();
686
    }
687
688 1
    $routineSource = $this->substitutePlaceHolders();
689 1
    $this->dropRoutineIfExists();
690 1
    $this->dl->setCharacterSet($this->characterSet, $this->collate);
691 1
    $this->dl->loadRoutine($routineSource);
692 1
  }
693
694
  //--------------------------------------------------------------------------------------------------------------------
695
  /**
696
   * Logs the unknown placeholder (if any).
697
   *
698
   * @param array $unknown The unknown placeholders.
699
   *
700
   * @throws RoutineLoaderException
701
   */
702 1
  private function logUnknownPlaceholders(array $unknown): void
703
  {
704
    // Return immediately if there are no unknown placeholders.
705 1
    if (empty($unknown)) return;
706
707
    sort($unknown);
708
    $this->io->text('Unknown placeholder(s):');
709
    $this->io->listing($unknown);
710
711
    $replace = [];
712
    foreach ($unknown as $placeholder)
713
    {
714
      $replace[$placeholder] = '<error>'.$placeholder.'</error>';
715
    }
716
    $code = strtr(OutputFormatter::escape($this->routineSourceCode), $replace);
717
718
    $this->io->text(explode(PHP_EOL, $code));
719
720
    throw new RoutineLoaderException('Unknown placeholder(s) found');
721
  }
722
723
  //--------------------------------------------------------------------------------------------------------------------
724
  /**
725
   * Returns true if the source file must be load or reloaded. Otherwise returns false.
726
   *
727
   * @return bool
728
   */
729 1
  private function mustLoadStoredRoutine(): bool
730
  {
731
    // If this is the first time we see the source file it must be loaded.
732 1
    if (empty($this->phpStratumOldMetadata)) return true;
733
734
    // If the source file has changed the source file must be loaded.
735
    if ($this->phpStratumOldMetadata['timestamp']!==$this->filemtime) return true;
736
737
    // If the value of a placeholder has changed the source file must be loaded.
738
    foreach ($this->phpStratumOldMetadata['replace'] as $placeHolder => $oldValue)
739
    {
740
      if (!isset($this->replacePairs[strtoupper($placeHolder)]) ||
741
        $this->replacePairs[strtoupper($placeHolder)]!==$oldValue)
742
      {
743
        return true;
744
      }
745
    }
746
747
    // If stored routine not exists in database the source file must be loaded.
748
    if (empty($this->rdbmsOldRoutineMetadata)) return true;
749
750
    // If current sql-mode is different the source file must reload.
751
    if (!$this->sqlModeHelper->compare($this->rdbmsOldRoutineMetadata['sql_mode'])) return true;
752
753
    // If current character set is different the source file must reload.
754
    if ($this->rdbmsOldRoutineMetadata['character_set_client']!==$this->characterSet) return true;
755
756
    // If current collation is different the source file must reload.
757
    if ($this->rdbmsOldRoutineMetadata['collation_connection']!==$this->collate) return true;
758
759
    return false;
760
  }
761
762
  //--------------------------------------------------------------------------------------------------------------------
763
  /**
764
   * Reads the source code of the stored routine.
765
   *
766
   * @throws RoutineLoaderException
767
   */
768 1
  private function readSourceCode(): void
769
  {
770 1
    $this->routineSourceCode      = file_get_contents($this->sourceFilename);
771 1
    $this->routineSourceCodeLines = explode(PHP_EOL, $this->routineSourceCode);
772
773 1
    if ($this->routineSourceCodeLines===false)
774
    {
775
      throw new RoutineLoaderException('Source file is empty');
776
    }
777
778 1
    $start = $this->findFirstMatchingLine('/^\s*\/\*\*\s*$/');
779 1
    $end   = $this->findFirstMatchingLine('/^\s*\*\/\s*$/');
780 1
    if ($start!==null && $end!==null && $start<$end)
781
    {
782 1
      $lines    = array_slice($this->routineSourceCodeLines, $start, $end - $start + 1);
783 1
      $docBlock = implode(PHP_EOL, (array)$lines);
784
    }
785
    else
786
    {
787
      $docBlock = '';
788
    }
789
790 1
    DocBlockReflection::setTagParameters('param', ['name']);
791 1
    DocBlockReflection::setTagParameters('type', ['type', 'ex1', 'ex2']);
792 1
    DocBlockReflection::setTagParameters('return', ['type']);
793 1
    DocBlockReflection::setTagParameters('paramAddendum', ['name', 'type', 'delimiter', 'enclosure', 'escape']);
794
795 1
    $this->docBlockReflection = new DocBlockReflection($docBlock);
796 1
  }
797
798
  //--------------------------------------------------------------------------------------------------------------------
799
  /**
800
   * Returns the source of the routine with all placeholders substituted with their values.
801
   *
802
   * @return string
803
   */
804 1
  private function substitutePlaceHolders(): string
805
  {
806 1
    $realpath = realpath($this->sourceFilename);
807
808 1
    $this->replace['__FILE__']    = "'".$this->dl->realEscapeString($realpath)."'";
809 1
    $this->replace['__ROUTINE__'] = "'".$this->routineName."'";
810 1
    $this->replace['__DIR__']     = "'".$this->dl->realEscapeString(dirname($realpath))."'";
811
812 1
    $lines         = explode(PHP_EOL, $this->routineSourceCode);
813 1
    $routineSource = [];
814 1
    foreach ($lines as $i => $line)
815
    {
816 1
      $this->replace['__LINE__'] = $i + 1;
817 1
      $routineSource[$i]         = strtr($line, $this->replace);
818
    }
819 1
    $routineSource = implode(PHP_EOL, $routineSource);
820
821 1
    unset($this->replace['__FILE__']);
822 1
    unset($this->replace['__ROUTINE__']);
823 1
    unset($this->replace['__DIR__']);
824 1
    unset($this->replace['__LINE__']);
825
826 1
    return $routineSource;
827
  }
828
829
  //--------------------------------------------------------------------------------------------------------------------
830
  /**
831
   * Updates the metadata for the stored routine.
832
   */
833 1
  private function updateMetadata(): void
834
  {
835 1
    $this->phpStratumMetadata['routine_name'] = $this->routineName;
836 1
    $this->phpStratumMetadata['designation']  = $this->designationType;
837 1
    $this->phpStratumMetadata['return']       = $this->returnType;
838 1
    $this->phpStratumMetadata['parameters']   = $this->routineParameters->getParameters();
839 1
    $this->phpStratumMetadata['timestamp']    = $this->filemtime;
840 1
    $this->phpStratumMetadata['replace']      = $this->replace;
841 1
    $this->phpStratumMetadata['phpdoc']       = $this->extractDocBlockPartsWrapper();
842
843 1
    if (in_array($this->designationType, ['rows_with_index', 'rows_with_key']))
844
    {
845 1
      $this->phpStratumMetadata['index_columns'] = $this->indexColumns;
846
    }
847
848 1
    if ($this->designationType==='bulk_insert')
849
    {
850 1
      $this->phpStratumMetadata['bulk_insert_table_name'] = $this->bulkInsertTableName;
851 1
      $this->phpStratumMetadata['bulk_insert_columns']    = $this->bulkInsertColumns;
852 1
      $this->phpStratumMetadata['bulk_insert_keys']       = $this->bulkInsertKeys;
853
    }
854 1
  }
855
856
  //--------------------------------------------------------------------------------------------------------------------
857
  /**
858
   * Validates the specified return type of the stored routine.
859
   *
860
   * @throws RoutineLoaderException
861
   */
862 1
  private function validateReturnType(): void
863
  {
864
    // Return immediately if designation type is not appropriate for this method.
865 1
    if (!in_array($this->designationType, ['function', 'singleton0', 'singleton1'])) return;
866
867 1
    $types = explode('|', $this->returnType);
868 1
    $diff  = array_diff($types, ['string', 'int', 'float', 'double', 'bool', 'null']);
869
870 1
    if (!($this->returnType=='mixed' || $this->returnType=='bool' || empty($diff)))
871
    {
872
      throw new RoutineLoaderException("Return type must be 'mixed', 'bool', or a combination of 'int', 'float', 'string', and 'null'");
873
    }
874
875
    // The following tests are applicable for singleton0 routines only.
876 1
    if (!in_array($this->designationType, ['singleton0'])) return;
877
878
    // Return mixed is OK.
879 1
    if (in_array($this->returnType, ['bool', 'mixed'])) return;
880
881
    // In all other cases return type must contain null.
882 1
    $parts = explode('|', $this->returnType);
883 1
    $key   = array_search('null', $parts);
884 1
    if ($key===false)
885
    {
886
      throw new RoutineLoaderException("Return type must be 'mixed', 'bool', or contain 'null' (with a combination of 'int', 'float', and 'string')");
887
    }
888 1
  }
889
890
  //--------------------------------------------------------------------------------------------------------------------
891
}
892
893
//----------------------------------------------------------------------------------------------------------------------
894