Passed
Push — master ( 6f8879...b9048c )
by P.R.
04:07
created

RoutineLoaderHelper::extractSyntax()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 5.3906

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 9
c 1
b 0
f 0
nc 3
nop 0
dl 0
loc 19
ccs 6
cts 8
cp 0.75
crap 5.3906
rs 9.6111
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