Passed
Push — master ( 6f8879...b9048c )
by P.R.
04:07
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'][0];
479 1
    switch ($this->designationType)
480
    {
481 1
      case 'bulk_insert':
482 1
        if ($tag['arguments'][1]==='' || $tag['arguments'][2]==='' || $tag['description'][0]!='')
483
        {
484
          throw new RoutineLoaderException('Invalid @type tag. Expected: @type bulk_insert <table_name> <columns>');
485
        }
486 1
        $this->bulkInsertTableName = $tag['arguments'][1];
487 1
        $this->bulkInsertKeys      = explode(',', $tag['arguments'][2]);
488 1
        break;
489
490 1
      case 'rows_with_key':
491 1
      case 'rows_with_index':
492 1
        if ($tag['arguments'][1]==='' || $tag['arguments'][2]!=='')
493
        {
494
          throw new RoutineLoaderException('Invalid @type tag. Expected: @type %s <columns>', $this->designationType);
495
        }
496 1
        $this->indexColumns = explode(',', $tag['arguments'][1]);
497 1
        break;
498
499
      default:
500 1
        if ($tag['arguments'][1]!=='')
501
        {
502
          throw new RoutineLoaderException('Error: Expected: @type %s', $this->designationType);
503
        }
504
    }
505 1
  }
506
507
  //--------------------------------------------------------------------------------------------------------------------
508
  /**
509
   * Extracts DocBlock parts to be used by the wrapper generator.
510
   */
511 1
  private function extractDocBlockPartsWrapper(): array
512
  {
513 1
    return ['short_description' => $this->docBlockReflection->getShortDescription(),
514 1
            'long_description'  => $this->docBlockReflection->getLongDescription(),
515 1
            'parameters'        => $this->routineParameters->extractDocBlockPartsWrapper()];
516
  }
517
518
  //--------------------------------------------------------------------------------------------------------------------
519
  /**
520
   * Extracts routine parameters.
521
   *
522
   * @throws MySqlQueryErrorException
523
   * @throws RoutineLoaderException
524
   */
525 1
  private function extractParameters()
526
  {
527 1
    $this->routineParameters = new RoutineParametersHelper($this->dl,
528 1
                                                           $this->io,
529 1
                                                           $this->docBlockReflection,
530 1
                                                           $this->routineName);
531
532 1
    $this->routineParameters->extractRoutineParameters();
533 1
  }
534
535
  //--------------------------------------------------------------------------------------------------------------------
536
  /**
537
   * Extracts the placeholders from the stored routine source.
538
   *
539
   * @throws RoutineLoaderException
540
   */
541 1
  private function extractPlaceholders(): void
542
  {
543 1
    $unknown = [];
544
545 1
    preg_match_all('(@[A-Za-z0-9_.]+(%(type|sort))?@)', $this->routineSourceCode, $matches);
546 1
    if (!empty($matches[0]))
547
    {
548 1
      foreach ($matches[0] as $placeholder)
549
      {
550 1
        if (isset($this->replacePairs[strtoupper($placeholder)]))
551
        {
552 1
          $this->replace[$placeholder] = $this->replacePairs[strtoupper($placeholder)];
553
        }
554
        else
555
        {
556
          $unknown[] = $placeholder;
557
        }
558
      }
559
    }
560
561 1
    $this->logUnknownPlaceholders($unknown);
562 1
  }
563
564
  //--------------------------------------------------------------------------------------------------------------------
565
  /**
566
   * Extracts the return type of the stored routine.
567
   *
568
   * @throws RoutineLoaderException
569
   */
570 1
  private function extractReturnType(): void
571
  {
572 1
    $tags = $this->docBlockReflection->getTags('return');
573
574 1
    switch ($this->designationType)
575
    {
576 1
      case 'function':
577 1
      case 'singleton0':
578 1
      case 'singleton1':
579 1
        if (count($tags)===0)
580
        {
581
          throw new RoutineLoaderException('Tag @return not found in DocBlock.');
582
        }
583 1
        $tag = $tags[0];
584 1
        if ($tag['arguments'][0]==='')
585
        {
586
          throw new RoutineLoaderException('Invalid return tag. Expected: @return <type>.');
587
        }
588 1
        $this->returnType = $tag['arguments'][0];
589 1
        break;
590
591
      default:
592 1
        if (count($tags)!==0)
593
        {
594
          throw new RoutineLoaderException('Redundant @type tag found in DocBlock.');
595
        }
596
    }
597 1
  }
598
599
  //--------------------------------------------------------------------------------------------------------------------
600
  /**
601
   * Extracts the name of the stored routine and the stored routine type (i.e. procedure or function) source.
602
   *
603
   * @throws RoutineLoaderException
604
   */
605 1
  private function extractRoutineTypeAndName(): void
606
  {
607 1
    $n = preg_match('/create\\s+(procedure|function)\\s+([a-zA-Z0-9_]+)/i', $this->routineSourceCode, $matches);
608 1
    if ($n==1)
609
    {
610 1
      if ($this->routineName!=$matches[2])
611
      {
612 1
        throw new RoutineLoaderException("Stored routine name '%s' does not corresponds with filename", $matches[2]);
613
      }
614
    }
615
    else
616
    {
617
      throw new RoutineLoaderException('Unable to find the stored routine name and type');
618
    }
619 1
  }
620
621
  //--------------------------------------------------------------------------------------------------------------------
622
  /**
623
   * Detects the syntax of the stored procedure. Either SQL/PSM or PL/SQL.
624
   */
625 1
  private function extractSyntax(): void
626
  {
627 1
    if ($this->sqlModeHelper->hasOracleMode())
628
    {
629 1
      $key1 = $this->findFirstMatchingLine('/^\s*(as|is)\s*$/i');
630 1
      $key2 = $this->findFirstMatchingLine('/^\s*begin\s*$/i');
631
632 1
      if ($key1!==null && $key2!==null && $key1<$key2)
633
      {
634
        $this->syntax = self::PL_SQL_SYNTAX;
635
      }
636
      else
637
      {
638 1
        $this->syntax = self::SQL_PSM_SYNTAX;
639
      }
640
    }
641
    else
642
    {
643
      $this->syntax = self::SQL_PSM_SYNTAX;
644
    }
645 1
  }
646
647
  //--------------------------------------------------------------------------------------------------------------------
648
  /**
649
   * Returns the key of the source line that match a regex pattern.
650
   *
651
   * @param string $pattern The regex pattern.
652
   *
653
   * @return int|null
654
   */
655 1
  private function findFirstMatchingLine(string $pattern): ?int
656
  {
657 1
    foreach ($this->routineSourceCodeLines as $key => $line)
658
    {
659 1
      if (preg_match($pattern, $line)===1)
660
      {
661 1
        return $key;
662
      }
663
    }
664
665 1
    return null;
666
  }
667
668
  //--------------------------------------------------------------------------------------------------------------------
669
  /**
670
   * Loads the stored routine into the database.
671
   *
672
   * @throws MySqlQueryErrorException
673
   */
674 1
  private function loadRoutineFile(): void
675
  {
676 1
    if ($this->syntax===self::PL_SQL_SYNTAX)
677
    {
678
      $this->sqlModeHelper->addIfRequiredOracleMode();
679
    }
680
    else
681
    {
682 1
      $this->sqlModeHelper->removeIfRequiredOracleMode();
683
    }
684
685 1
    $routineSource = $this->substitutePlaceHolders();
686 1
    $this->dropRoutineIfExists();
687 1
    $this->dl->setCharacterSet($this->characterSet, $this->collate);
688 1
    $this->dl->loadRoutine($routineSource);
689 1
  }
690
691
  //--------------------------------------------------------------------------------------------------------------------
692
  /**
693
   * Logs the unknown placeholder (if any).
694
   *
695
   * @param array $unknown The unknown placeholders.
696
   *
697
   * @throws RoutineLoaderException
698
   */
699 1
  private function logUnknownPlaceholders(array $unknown): void
700
  {
701
    // Return immediately if there are no unknown placeholders.
702 1
    if (empty($unknown)) return;
703
704
    sort($unknown);
705
    $this->io->text('Unknown placeholder(s):');
706
    $this->io->listing($unknown);
707
708
    $replace = [];
709
    foreach ($unknown as $placeholder)
710
    {
711
      $replace[$placeholder] = '<error>'.$placeholder.'</error>';
712
    }
713
    $code = strtr(OutputFormatter::escape($this->routineSourceCode), $replace);
714
715
    $this->io->text(explode(PHP_EOL, $code));
716
717
    throw new RoutineLoaderException('Unknown placeholder(s) found');
718
  }
719
720
  //--------------------------------------------------------------------------------------------------------------------
721
  /**
722
   * Returns true if the source file must be load or reloaded. Otherwise returns false.
723
   *
724
   * @return bool
725
   */
726 1
  private function mustLoadStoredRoutine(): bool
727
  {
728
    // If this is the first time we see the source file it must be loaded.
729 1
    if (empty($this->phpStratumOldMetadata)) return true;
730
731
    // If the source file has changed the source file must be loaded.
732
    if ($this->phpStratumOldMetadata['timestamp']!==$this->filemtime) return true;
733
734
    // If the value of a placeholder has changed the source file must be loaded.
735
    foreach ($this->phpStratumOldMetadata['replace'] as $placeHolder => $oldValue)
736
    {
737
      if (!isset($this->replacePairs[strtoupper($placeHolder)]) ||
738
        $this->replacePairs[strtoupper($placeHolder)]!==$oldValue)
739
      {
740
        return true;
741
      }
742
    }
743
744
    // If stored routine not exists in database the source file must be loaded.
745
    if (empty($this->rdbmsOldRoutineMetadata)) return true;
746
747
    // If current sql-mode is different the source file must reload.
748
    if (!$this->sqlModeHelper->compare($this->rdbmsOldRoutineMetadata['sql_mode'])) return true;
749
750
    // If current character set is different the source file must reload.
751
    if ($this->rdbmsOldRoutineMetadata['character_set_client']!==$this->characterSet) return true;
752
753
    // If current collation is different the source file must reload.
754
    if ($this->rdbmsOldRoutineMetadata['collation_connection']!==$this->collate) return true;
755
756
    return false;
757
  }
758
759
  //--------------------------------------------------------------------------------------------------------------------
760
  /**
761
   * Reads the source code of the stored routine.
762
   *
763
   * @throws RoutineLoaderException
764
   */
765 1
  private function readSourceCode(): void
766
  {
767 1
    $this->routineSourceCode      = file_get_contents($this->sourceFilename);
768 1
    $this->routineSourceCodeLines = explode(PHP_EOL, $this->routineSourceCode);
769
770 1
    if ($this->routineSourceCodeLines===false)
771
    {
772
      throw new RoutineLoaderException('Source file is empty');
773
    }
774
775 1
    $start = $this->findFirstMatchingLine('/^\s*\/\*\*\s*$/');
776 1
    $end   = $this->findFirstMatchingLine('/^\s*\*\/\s*$/');
777 1
    if ($start!==null && $end!==null && $start<$end)
778
    {
779 1
      $lines    = array_slice($this->routineSourceCodeLines, $start, $end - $start + 1);
780 1
      $docBlock = implode(PHP_EOL, (array)$lines);
781
    }
782
    else
783
    {
784
      $docBlock = '';
785
    }
786
787 1
    DocBlockReflection::setTagParameters('param', 1);
788 1
    DocBlockReflection::setTagParameters('type', 3);
789 1
    DocBlockReflection::setTagParameters('return', 1);
790 1
    DocBlockReflection::setTagParameters('paramAddendum', 5);
791
792 1
    $this->docBlockReflection = new DocBlockReflection($docBlock);
793 1
  }
794
795
  //--------------------------------------------------------------------------------------------------------------------
796
  /**
797
   * Returns the source of the routine with all placeholders substituted with their values.
798
   *
799
   * @return string
800
   */
801 1
  private function substitutePlaceHolders(): string
802
  {
803 1
    $realpath = realpath($this->sourceFilename);
804
805 1
    $this->replace['__FILE__']    = "'".$this->dl->realEscapeString($realpath)."'";
806 1
    $this->replace['__ROUTINE__'] = "'".$this->routineName."'";
807 1
    $this->replace['__DIR__']     = "'".$this->dl->realEscapeString(dirname($realpath))."'";
808
809 1
    $lines         = explode(PHP_EOL, $this->routineSourceCode);
810 1
    $routineSource = [];
811 1
    foreach ($lines as $i => $line)
812
    {
813 1
      $this->replace['__LINE__'] = $i + 1;
814 1
      $routineSource[$i]         = strtr($line, $this->replace);
815
    }
816 1
    $routineSource = implode(PHP_EOL, $routineSource);
817
818 1
    unset($this->replace['__FILE__']);
819 1
    unset($this->replace['__ROUTINE__']);
820 1
    unset($this->replace['__DIR__']);
821 1
    unset($this->replace['__LINE__']);
822
823 1
    return $routineSource;
824
  }
825
826
  //--------------------------------------------------------------------------------------------------------------------
827
  /**
828
   * Updates the metadata for the stored routine.
829
   */
830 1
  private function updateMetadata(): void
831
  {
832 1
    $this->phpStratumMetadata['routine_name'] = $this->routineName;
833 1
    $this->phpStratumMetadata['designation']  = $this->designationType;
834 1
    $this->phpStratumMetadata['return']       = $this->returnType;
835 1
    $this->phpStratumMetadata['parameters']   = $this->routineParameters->getParameters();
836 1
    $this->phpStratumMetadata['timestamp']    = $this->filemtime;
837 1
    $this->phpStratumMetadata['replace']      = $this->replace;
838 1
    $this->phpStratumMetadata['phpdoc']       = $this->extractDocBlockPartsWrapper();
839
840 1
    if (in_array($this->designationType, ['rows_with_index', 'rows_with_key']))
841
    {
842 1
      $this->phpStratumMetadata['index_columns'] = $this->indexColumns;
843
    }
844
845 1
    if ($this->designationType==='bulk_insert')
846
    {
847 1
      $this->phpStratumMetadata['bulk_insert_table_name'] = $this->bulkInsertTableName;
848 1
      $this->phpStratumMetadata['bulk_insert_columns']    = $this->bulkInsertColumns;
849 1
      $this->phpStratumMetadata['bulk_insert_keys']       = $this->bulkInsertKeys;
850
    }
851 1
  }
852
853
  //--------------------------------------------------------------------------------------------------------------------
854
  /**
855
   * Validates the specified return type of the stored routine.
856
   *
857
   * @throws RoutineLoaderException
858
   */
859 1
  private function validateReturnType(): void
860
  {
861
    // Return immediately if designation type is not appropriate for this method.
862 1
    if (!in_array($this->designationType, ['function', 'singleton0', 'singleton1'])) return;
863
864 1
    $types = explode('|', $this->returnType);
865 1
    $diff  = array_diff($types, ['string', 'int', 'float', 'double', 'bool', 'null']);
866
867 1
    if (!($this->returnType=='mixed' || $this->returnType=='bool' || empty($diff)))
868
    {
869
      throw new RoutineLoaderException("Return type must be 'mixed', 'bool', or a combination of 'int', 'float', 'string', and 'null'");
870
    }
871
872
    // The following tests are applicable for singleton0 routines only.
873 1
    if (!in_array($this->designationType, ['singleton0'])) return;
874
875
    // Return mixed is OK.
876 1
    if (in_array($this->returnType, ['bool', 'mixed'])) return;
877
878
    // In all other cases return type must contain null.
879 1
    $parts = explode('|', $this->returnType);
880 1
    $key   = array_search('null', $parts);
881 1
    if ($key===false)
882
    {
883
      throw new RoutineLoaderException("Return type must be 'mixed', 'bool', or contain 'null' (with a combination of 'int', 'float', and 'string')");
884
    }
885 1
  }
886
887
  //--------------------------------------------------------------------------------------------------------------------
888
}
889
890
//----------------------------------------------------------------------------------------------------------------------
891