Passed
Push — master ( aedf30...d48c95 )
by P.R.
04:03
created

RoutineLoaderHelper::updateMetadata()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 14
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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