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

RoutineLoaderHelper::substitutePlaceHolders()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 2

Importance

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