Passed
Push — master ( 56185b...6f8879 )
by P.R.
04:24
created

RoutineLoaderHelper::extractPlaceholders()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 21
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.0218

Importance

Changes 0
Metric Value
cc 4
eloc 9
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 21
ccs 8
cts 9
cp 0.8889
crap 4.0218
rs 9.9666
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
   * @throws RoutineLoaderException
626
   */
627 1
  private function extractSyntax(): void
628
  {
629 1
    if ($this->sqlModeHelper->hasOracleMode())
630
    {
631 1
      $key1 = $this->findFirstMatchingLine('/^\s*(as|is)\s*$/i');
632 1
      $key2 = $this->findFirstMatchingLine('/^\s*begin\s*$/i');
633
634 1
      if ($key1!==null && $key2!==null && $key1<$key2)
635
      {
636
        $this->syntax = self::PL_SQL_SYNTAX;
637
      }
638 1
      elseif ($key1===null && $key2!==null)
639
      {
640 1
        $this->syntax = self::SQL_PSM_SYNTAX;
641
      }
642
      else
643
      {
644 1
        throw new RoutineLoaderException('Unable to derive syntax (SQL/PSM or PL/SQL) from stored routine.');
645
      }
646
    }
647
    else
648
    {
649
      $this->syntax = self::SQL_PSM_SYNTAX;
650
    }
651 1
  }
652
653
  //--------------------------------------------------------------------------------------------------------------------
654
  /**
655
   * Returns the key of the source line that match a regex pattern.
656
   *
657
   * @param string $pattern The regex pattern.
658
   *
659
   * @return int|null
660
   */
661 1
  private function findFirstMatchingLine(string $pattern): ?int
662
  {
663 1
    foreach ($this->routineSourceCodeLines as $key => $line)
664
    {
665 1
      if (preg_match($pattern, $line)===1)
666
      {
667 1
        return $key;
668
      }
669
    }
670
671 1
    return null;
672
  }
673
674
  //--------------------------------------------------------------------------------------------------------------------
675
  /**
676
   * Loads the stored routine into the database.
677
   *
678
   * @throws MySqlQueryErrorException
679
   */
680 1
  private function loadRoutineFile(): void
681
  {
682 1
    if ($this->syntax===self::PL_SQL_SYNTAX)
683
    {
684
      $this->sqlModeHelper->addIfRequiredOracleMode();
685
    }
686
    else
687
    {
688 1
      $this->sqlModeHelper->removeIfRequiredOracleMode();
689
    }
690
691 1
    $routineSource = $this->substitutePlaceHolders();
692 1
    $this->dropRoutineIfExists();
693 1
    $this->dl->setCharacterSet($this->characterSet, $this->collate);
694 1
    $this->dl->loadRoutine($routineSource);
695 1
  }
696
697
  //--------------------------------------------------------------------------------------------------------------------
698
  /**
699
   * Logs the unknown placeholder (if any).
700
   *
701
   * @param array $unknown The unknown placeholders.
702
   *
703
   * @throws RoutineLoaderException
704
   */
705 1
  private function logUnknownPlaceholders(array $unknown): void
706
  {
707
    // Return immediately if there are no unknown placeholders.
708 1
    if (empty($unknown)) return;
709
710
    sort($unknown);
711
    $this->io->text('Unknown placeholder(s):');
712
    $this->io->listing($unknown);
713
714
    $replace = [];
715
    foreach ($unknown as $placeholder)
716
    {
717
      $replace[$placeholder] = '<error>'.$placeholder.'</error>';
718
    }
719
    $code = strtr(OutputFormatter::escape($this->routineSourceCode), $replace);
720
721
    $this->io->text(explode(PHP_EOL, $code));
722
723
    throw new RoutineLoaderException('Unknown placeholder(s) found');
724
  }
725
726
  //--------------------------------------------------------------------------------------------------------------------
727
  /**
728
   * Returns true if the source file must be load or reloaded. Otherwise returns false.
729
   *
730
   * @return bool
731
   */
732 1
  private function mustLoadStoredRoutine(): bool
733
  {
734
    // If this is the first time we see the source file it must be loaded.
735 1
    if (empty($this->phpStratumOldMetadata)) return true;
736
737
    // If the source file has changed the source file must be loaded.
738
    if ($this->phpStratumOldMetadata['timestamp']!==$this->filemtime) return true;
739
740
    // If the value of a placeholder has changed the source file must be loaded.
741
    foreach ($this->phpStratumOldMetadata['replace'] as $placeHolder => $oldValue)
742
    {
743
      if (!isset($this->replacePairs[strtoupper($placeHolder)]) ||
744
        $this->replacePairs[strtoupper($placeHolder)]!==$oldValue)
745
      {
746
        return true;
747
      }
748
    }
749
750
    // If stored routine not exists in database the source file must be loaded.
751
    if (empty($this->rdbmsOldRoutineMetadata)) return true;
752
753
    // If current sql-mode is different the source file must reload.
754
    if (!$this->sqlModeHelper->compare($this->rdbmsOldRoutineMetadata['sql_mode'])) return true;
755
756
    // If current character set is different the source file must reload.
757
    if ($this->rdbmsOldRoutineMetadata['character_set_client']!==$this->characterSet) return true;
758
759
    // If current collation is different the source file must reload.
760
    if ($this->rdbmsOldRoutineMetadata['collation_connection']!==$this->collate) return true;
761
762
    return false;
763
  }
764
765
  //--------------------------------------------------------------------------------------------------------------------
766
  /**
767
   * Reads the source code of the stored routine.
768
   *
769
   * @throws RoutineLoaderException
770
   */
771 1
  private function readSourceCode(): void
772
  {
773 1
    $this->routineSourceCode      = file_get_contents($this->sourceFilename);
774 1
    $this->routineSourceCodeLines = explode(PHP_EOL, $this->routineSourceCode);
775
776 1
    if ($this->routineSourceCodeLines===false)
777
    {
778
      throw new RoutineLoaderException('Source file is empty');
779
    }
780
781 1
    $start = $this->findFirstMatchingLine('/^\s*\/\*\*\s*$/');
782 1
    $end   = $this->findFirstMatchingLine('/^\s*\*\/\s*$/');
783 1
    if ($start!==null && $end!==null && $start<$end)
784
    {
785 1
      $lines    = array_slice($this->routineSourceCodeLines, $start, $end - $start + 1);
786 1
      $docBlock = implode(PHP_EOL, (array)$lines);
787
    }
788
    else
789
    {
790
      $docBlock = '';
791
    }
792
793 1
    DocBlockReflection::setTagParameters('param', 1);
794 1
    DocBlockReflection::setTagParameters('type', 3);
795 1
    DocBlockReflection::setTagParameters('return', 1);
796 1
    DocBlockReflection::setTagParameters('paramAddendum', 5);
797
798 1
    $this->docBlockReflection = new DocBlockReflection($docBlock);
799 1
  }
800
801
  //--------------------------------------------------------------------------------------------------------------------
802
  /**
803
   * Returns the source of the routine with all placeholders substituted with their values.
804
   *
805
   * @return string
806
   */
807 1
  private function substitutePlaceHolders(): string
808
  {
809 1
    $realpath = realpath($this->sourceFilename);
810
811 1
    $this->replace['__FILE__']    = "'".$this->dl->realEscapeString($realpath)."'";
812 1
    $this->replace['__ROUTINE__'] = "'".$this->routineName."'";
813 1
    $this->replace['__DIR__']     = "'".$this->dl->realEscapeString(dirname($realpath))."'";
814
815 1
    $lines         = explode(PHP_EOL, $this->routineSourceCode);
816 1
    $routineSource = [];
817 1
    foreach ($lines as $i => $line)
818
    {
819 1
      $this->replace['__LINE__'] = $i + 1;
820 1
      $routineSource[$i]         = strtr($line, $this->replace);
821
    }
822 1
    $routineSource = implode(PHP_EOL, $routineSource);
823
824 1
    unset($this->replace['__FILE__']);
825 1
    unset($this->replace['__ROUTINE__']);
826 1
    unset($this->replace['__DIR__']);
827 1
    unset($this->replace['__LINE__']);
828
829 1
    return $routineSource;
830
  }
831
832
  //--------------------------------------------------------------------------------------------------------------------
833
  /**
834
   * Updates the metadata for the stored routine.
835
   */
836 1
  private function updateMetadata(): void
837
  {
838 1
    $this->phpStratumMetadata['routine_name'] = $this->routineName;
839 1
    $this->phpStratumMetadata['designation']  = $this->designationType;
840 1
    $this->phpStratumMetadata['return']       = $this->returnType;
841 1
    $this->phpStratumMetadata['parameters']   = $this->routineParameters->getParameters();
842 1
    $this->phpStratumMetadata['timestamp']    = $this->filemtime;
843 1
    $this->phpStratumMetadata['replace']      = $this->replace;
844 1
    $this->phpStratumMetadata['phpdoc']       = $this->extractDocBlockPartsWrapper();
845
846 1
    if (in_array($this->designationType, ['rows_with_index', 'rows_with_key']))
847
    {
848 1
      $this->phpStratumMetadata['index_columns'] = $this->indexColumns;
849
    }
850
851 1
    if ($this->designationType==='bulk_insert')
852
    {
853 1
      $this->phpStratumMetadata['bulk_insert_table_name'] = $this->bulkInsertTableName;
854 1
      $this->phpStratumMetadata['bulk_insert_columns']    = $this->bulkInsertColumns;
855 1
      $this->phpStratumMetadata['bulk_insert_keys']       = $this->bulkInsertKeys;
856
    }
857 1
  }
858
859
  //--------------------------------------------------------------------------------------------------------------------
860
  /**
861
   * Validates the specified return type of the stored routine.
862
   *
863
   * @throws RoutineLoaderException
864
   */
865 1
  private function validateReturnType(): void
866
  {
867
    // Return immediately if designation type is not appropriate for this method.
868 1
    if (!in_array($this->designationType, ['function', 'singleton0', 'singleton1'])) return;
869
870 1
    $types = explode('|', $this->returnType);
871 1
    $diff  = array_diff($types, ['string', 'int', 'float', 'double', 'bool', 'null']);
872
873 1
    if (!($this->returnType=='mixed' || $this->returnType=='bool' || empty($diff)))
874
    {
875
      throw new RoutineLoaderException("Return type must be 'mixed', 'bool', or a combination of 'int', 'float', 'string', and 'null'");
876
    }
877
878
    // The following tests are applicable for singleton0 routines only.
879 1
    if (!in_array($this->designationType, ['singleton0'])) return;
880
881
    // Return mixed is OK.
882 1
    if (in_array($this->returnType, ['bool', 'mixed'])) return;
883
884
    // In all other cases return type must contain null.
885 1
    $parts = explode('|', $this->returnType);
886 1
    $key   = array_search('null', $parts);
887 1
    if ($key===false)
888
    {
889
      throw new RoutineLoaderException("Return type must be 'mixed', 'bool', or contain 'null' (with a combination of 'int', 'float', and 'string')");
890
    }
891 1
  }
892
893
  //--------------------------------------------------------------------------------------------------------------------
894
}
895
896
//----------------------------------------------------------------------------------------------------------------------
897