Passed
Push — master ( e0c5e8...7926db )
by P.R.
04:00
created

RoutineLoaderHelper::logUnknownPlaceholders()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 3
nop 1
dl 0
loc 19
ccs 0
cts 11
cp 0
crap 12
rs 9.9332
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\Stratum\Backend\StratumStyle;
8
use SetBased\Stratum\Common\Exception\RoutineLoaderException;
9
use SetBased\Stratum\Middle\Exception\ResultException;
10
use SetBased\Stratum\MySql\Exception\MySqlQueryErrorException;
11
use SetBased\Stratum\MySql\MySqlMetaDataLayer;
12
use Symfony\Component\Console\Formatter\OutputFormatter;
13
use Zend\Code\Reflection\DocBlock\Tag\ParamTag;
14
use Zend\Code\Reflection\DocBlockReflection;
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 stored routine type (i.e. procedure or function) of the stored routine.
185
   *
186
   * @var string
187
   */
188
  private $routineType;
189
190
  /**
191
   * The source filename holding the stored routine.
192
   *
193
   * @var string
194
   */
195
  private $sourceFilename;
196
197
  /**
198
   * The SQL mode under which the stored routine will be loaded and run.
199
   *
200
   * @var string
201
   */
202
  private $sqlMode;
203
204
  //--------------------------------------------------------------------------------------------------------------------
205
  /**
206
   * Object constructor.
207
   *
208
   * @param MySqlMetaDataLayer $dl                      The meta data layer.
209
   * @param StratumStyle       $io                      The output for log messages.
210
   * @param string             $routineFilename         The filename of the source of the stored routine.
211
   * @param array              $phpStratumMetadata      The metadata of the stored routine from PhpStratum.
212
   * @param array              $replacePairs            A map from placeholders to their actual values.
213
   * @param array              $rdbmsOldRoutineMetadata The old metadata of the stored routine from MySQL.
214
   * @param string             $sqlMode                 The SQL mode under which the stored routine will be loaded and
215
   *                                                    run.
216
   * @param string             $characterSet            The default character set under which the stored routine will
217
   *                                                    be loaded and run.
218
   * @param string             $collate                 The key or index columns (depending on the designation type) of
219
   *                                                    the stored routine.
220
   */
221 1
  public function __construct(MySqlMetaDataLayer $dl,
222
                              StratumStyle $io,
223
                              string $routineFilename,
224
                              array $phpStratumMetadata,
225
                              array $replacePairs,
226
                              array $rdbmsOldRoutineMetadata,
227
                              string $sqlMode,
228
                              string $characterSet,
229
                              string $collate)
230
  {
231 1
    $this->dl                      = $dl;
232 1
    $this->io                      = $io;
233 1
    $this->sourceFilename          = $routineFilename;
234 1
    $this->phpStratumMetadata      = $phpStratumMetadata;
235 1
    $this->replacePairs            = $replacePairs;
236 1
    $this->rdbmsOldRoutineMetadata = $rdbmsOldRoutineMetadata;
237 1
    $this->sqlMode                 = $sqlMode;
238 1
    $this->characterSet            = $characterSet;
239 1
    $this->collate                 = $collate;
240 1
  }
241
242
  //--------------------------------------------------------------------------------------------------------------------
243
  /**
244
   * Extract column metadata from the rows returned by the SQL statement 'describe table'.
245
   *
246
   * @param array $description The description of the table.
247
   *
248
   * @return array
249
   */
250
  private static function extractColumnsFromTableDescription(array $description): array
251
  {
252
    $ret = [];
253
254
    foreach ($description as $column)
255
    {
256
      preg_match('/^(\w+)(.*)?$/', $column['Type'], $parts1);
257
258
      $tmp = ['column_name'       => $column['Field'],
259
              'data_type'         => $parts1[1],
260
              'numeric_precision' => null,
261
              'numeric_scale'     => null,
262
              'dtd_identifier'    => $column['Type']];
263
264
      switch ($parts1[1])
265
      {
266
        case 'tinyint':
267
        case 'smallint':
268
        case 'mediumint':
269
        case 'int':
270
        case 'bigint':
271
          preg_match('/^\((\d+)\)/', $parts1[2], $parts2);
272
          $tmp['numeric_precision'] = (int)$parts2[1];
273
          $tmp['numeric_scale']     = 0;
274
          break;
275
276
        case 'year':
277
          // Nothing to do.
278
          break;
279
280
        case 'float':
281
          $tmp['numeric_precision'] = 12;
282
          break;
283
284
        case 'double':
285
          $tmp['numeric_precision'] = 22;
286
          break;
287
288
        case 'binary':
289
        case 'char':
290
        case 'varbinary':
291
        case 'varchar':
292
          // Nothing to do (binary) strings.
293
          break;
294
295
        case 'decimal':
296
          preg_match('/^\((\d+),(\d+)\)$/', $parts1[2], $parts2);
297
          $tmp['numeric_precision'] = (int)$parts2[1];
298
          $tmp['numeric_scale']     = (int)$parts2[2];;
299
          break;
300
301
        case 'time':
302
        case 'timestamp':
303
        case 'date':
304
        case 'datetime':
305
          // Nothing to do date and time.
306
          break;
307
308
        case 'enum':
309
        case 'set':
310
          // Nothing to do sets.
311
          break;
312
313
        case 'bit':
314
          preg_match('/^\((\d+)\)$/', $parts1[2], $parts2);
315
          $tmp['numeric_precision'] = (int)$parts2[1];
316
          break;
317
318
        case 'tinytext':
319
        case 'text':
320
        case 'mediumtext':
321
        case 'longtext':
322
        case 'tinyblob':
323
        case 'blob':
324
        case 'mediumblob':
325
        case 'longblob':
326
          // Nothing to do CLOBs and BLOBs.
327
          break;
328
329
        default:
330
          throw new FallenException('data type', $parts1[1]);
331
      }
332
333
      $ret[] = $tmp;
334
    }
335
336
    return $ret;
337
  }
338
339
  //--------------------------------------------------------------------------------------------------------------------
340
  /**
341
   * Loads the stored routine into the instance of MySQL and returns the metadata of the stored routine.
342
   *
343
   * @return array
344
   *
345
   * @throws RoutineLoaderException
346
   * @throws MySqlQueryErrorException
347
   * @throws ResultException
348
   */
349 1
  public function loadStoredRoutine(): array
350
  {
351 1
    $this->routineName           = pathinfo($this->sourceFilename, PATHINFO_FILENAME);
352 1
    $this->phpStratumOldMetadata = $this->phpStratumMetadata;
353 1
    $this->filemtime             = filemtime($this->sourceFilename);
354
355 1
    $load = $this->mustLoadStoredRoutine();
356 1
    if ($load)
357
    {
358
      $this->io->text(sprintf('Loading routine <dbo>%s</dbo>', OutputFormatter::escape($this->routineName)));
359
360
      $this->readSourceCode();
361
      $this->extractPlaceholders();
362
      $this->extractDesignationType();
363
      $this->extractReturnType();
364
      $this->extractRoutineTypeAndName();
365
      $this->validateReturnType();
366
      $this->loadRoutineFile();
367
      $this->extractBulkInsertTableColumnsInfo();
368
      $this->extractExtendedParametersInfo();
369
      $this->extractRoutineParametersInfo();
370
      $this->extractDocBlockPartsWrapper();
371
      $this->validateParameterLists();
372
      $this->updateMetadata();
373
    }
374
375 1
    return $this->phpStratumMetadata;
376
  }
377
378
  //--------------------------------------------------------------------------------------------------------------------
379
  /**
380
   * Drops the stored routine if it exists.
381
   *
382
   * @throws MySqlQueryErrorException
383
   */
384
  private function dropRoutine(): void
385
  {
386
    if (!empty($this->rdbmsOldRoutineMetadata))
387
    {
388
      $this->dl->dropRoutine($this->rdbmsOldRoutineMetadata['routine_type'], $this->routineName);
389
    }
390
  }
391
392
  //--------------------------------------------------------------------------------------------------------------------
393
  /**
394
   *  Extracts the column names and column types of the current table for bulk insert.
395
   *
396
   * @throws RoutineLoaderException
397
   * @throws MySqlQueryErrorException
398
   *
399
   * @throws ResultException
400
   */
401
  private function extractBulkInsertTableColumnsInfo(): void
402
  {
403
    // Return immediately if designation type is not appropriate for this method.
404
    if ($this->designationType!='bulk_insert') return;
405
406
    // Check if table is a temporary table or a non-temporary table.
407
    $table_is_non_temporary = $this->dl->checkTableExists($this->bulkInsertTableName);
408
409
    // Create temporary table if table is non-temporary table.
410
    if (!$table_is_non_temporary)
411
    {
412
      $this->dl->callProcedure($this->routineName);
413
    }
414
415
    // Get information about the columns of the table.
416
    $description = $this->dl->describeTable($this->bulkInsertTableName);
417
418
    // Drop temporary table if table is non-temporary.
419
    if (!$table_is_non_temporary)
420
    {
421
      $this->dl->dropTemporaryTable($this->bulkInsertTableName);
422
    }
423
424
    // Check number of columns in the table match the number of fields given in the designation type.
425
    $n1 = sizeof($this->bulkInsertKeys);
426
    $n2 = sizeof($description);
427
    if ($n1!=$n2)
428
    {
429
      throw new RoutineLoaderException("Number of fields %d and number of columns %d don't match.", $n1, $n2);
430
    }
431
432
    $this->bulkInsertColumns = self::extractColumnsFromTableDescription($description);
433
  }
434
435
  //--------------------------------------------------------------------------------------------------------------------
436
  /**
437
   * Extracts the designation type of the stored routine.
438
   *
439
   * @throws RoutineLoaderException
440
   */
441
  private function extractDesignationType(): void
442
  {
443
    $found = true;
444
    $key   = array_search('begin', $this->routineSourceCodeLines);
445
446
    if ($key!==false)
447
    {
448
      for ($i = 1; $i<$key; $i++)
449
      {
450
        $n = preg_match('/^\s*--\s+type:\s*(\w+)\s*(.+)?\s*$/',
451
                        $this->routineSourceCodeLines[$key - $i],
452
                        $matches);
453
        if ($n==1)
454
        {
455
          $this->designationType = $matches[1];
456
          switch ($this->designationType)
457
          {
458
            case 'bulk_insert':
459
              $m = preg_match('/^([a-zA-Z0-9_]+)\s+([a-zA-Z0-9_,]+)$/',
460
                              $matches[2],
461
                              $info);
462
              if ($m==0)
463
              {
464
                throw new RoutineLoaderException('Error: Expected: -- type: bulk_insert <table_name> <columns>');
465
              }
466
              $this->bulkInsertTableName = $info[1];
467
              $this->bulkInsertKeys      = explode(',', $info[2]);
468
              break;
469
470
            case 'rows_with_key':
471
            case 'rows_with_index':
472
              $this->indexColumns = explode(',', $matches[2]);
473
              break;
474
475
            default:
476
              if (isset($matches[2])) $found = false;
477
          }
478
          break;
479
        }
480
        if ($i==($key - 1)) $found = false;
481
      }
482
    }
483
    else
484
    {
485
      $found = false;
486
    }
487
488
    if ($found===false)
489
    {
490
      throw new RoutineLoaderException('Unable to find the designation type of the stored routine');
491
    }
492
  }
493
494
  //--------------------------------------------------------------------------------------------------------------------
495
  /**
496
   *  Extracts the DocBlock (in parts) from the source of the stored routine.
497
   */
498
  private function extractDocBlockPartsSource(): void
499
  {
500
    // Get the DocBlock for the source.
501
    $tmp = PHP_EOL;
502
    foreach ($this->routineSourceCodeLines as $line)
503
    {
504
      $n = preg_match('/create\\s+(procedure|function)\\s+([a-zA-Z0-9_]+)/i', $line);
505
      if ($n) break;
506
507
      $tmp .= $line;
508
      $tmp .= PHP_EOL;
509
    }
510
511
    $phpdoc = new DocBlockReflection($tmp);
512
513
    // Get the short description.
514
    $this->docBlockPartsSource['sort_description'] = $phpdoc->getShortDescription();
515
516
    // Get the long description.
517
    $this->docBlockPartsSource['long_description'] = $phpdoc->getLongDescription();
518
519
    // Get the description for each parameter of the stored routine.
520
    foreach ($phpdoc->getTags() as $key => $tag)
521
    {
522
      if ($tag->getName()=='param')
523
      {
524
        /* @var $tag ParamTag */
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
525
        $this->docBlockPartsSource['parameters'][$key] = ['name'        => $tag->getTypes()[0],
526
                                                          'description' => $tag->getDescription()];
527
      }
528
    }
529
  }
530
531
  //--------------------------------------------------------------------------------------------------------------------
532
  /**
533
   *  Extracts DocBlock parts to be used by the wrapper generator.
534
   */
535
  private function extractDocBlockPartsWrapper(): void
536
  {
537
    // Get the DocBlock parts from the source of the stored routine.
538
    $this->extractDocBlockPartsSource();
539
540
    // Generate the parameters parts of the DocBlock to be used by the wrapper.
541
    $parameters = [];
542
    foreach ($this->parameters as $parameter_info)
543
    {
544
      $parameters[] = ['parameter_name'       => $parameter_info['parameter_name'],
545
                       'php_type'             => DataTypeHelper::columnTypeToPhpTypeHinting($parameter_info).'|null',
546
                       'data_type_descriptor' => $parameter_info['data_type_descriptor'],
547
                       'description'          => $this->getParameterDocDescription($parameter_info['parameter_name'])];
548
    }
549
550
    // Compose all the DocBlock parts to be used by the wrapper generator.
551
    $this->docBlockPartsWrapper = ['sort_description' => $this->docBlockPartsSource['sort_description'],
552
                                   'long_description' => $this->docBlockPartsSource['long_description'],
553
                                   'parameters'       => $parameters];
554
  }
555
556
  //--------------------------------------------------------------------------------------------------------------------
557
  /**
558
   * Extracts extended info of the routine parameters.
559
   *
560
   * @throws RoutineLoaderException
561
   */
562
  private function extractExtendedParametersInfo(): void
563
  {
564
    $key = array_search('begin', $this->routineSourceCodeLines);
565
566
    if ($key!==false)
567
    {
568
      for ($i = 1; $i<$key; $i++)
569
      {
570
        $k = preg_match('/^\s*--\s+param:(?:\s*(\w+)\s+(\w+)(?:(?:\s+([^\s-])\s+([^\s-])\s+([^\s-])\s*$)|(?:\s*$)))?/',
571
                        $this->routineSourceCodeLines[$key - $i + 1],
572
                        $matches);
573
574
        if ($k==1)
575
        {
576
          $count = sizeof($matches);
577
          if ($count==3 || $count==6)
578
          {
579
            $parameter_name = $matches[1];
580
            $data_type      = $matches[2];
581
582
            if ($count==6)
583
            {
584
              $list_delimiter = $matches[3];
585
              $list_enclosure = $matches[4];
586
              $list_escape    = $matches[5];
587
            }
588
            else
589
            {
590
              $list_delimiter = ',';
591
              $list_enclosure = '"';
592
              $list_escape    = '\\';
593
            }
594
595
            if (!isset($this->extendedParameters[$parameter_name]))
596
            {
597
              $this->extendedParameters[$parameter_name] = ['name'      => $parameter_name,
598
                                                            'data_type' => $data_type,
599
                                                            'delimiter' => $list_delimiter,
600
                                                            'enclosure' => $list_enclosure,
601
                                                            'escape'    => $list_escape];
602
            }
603
            else
604
            {
605
              throw new RoutineLoaderException("Duplicate parameter '%s'", $parameter_name);
606
            }
607
          }
608
          else
609
          {
610
            throw new RoutineLoaderException('Error: Expected: -- param: <field_name> <type_of_list> [delimiter enclosure escape]');
611
          }
612
        }
613
      }
614
    }
615
  }
616
617
  //--------------------------------------------------------------------------------------------------------------------
618
  /**
619
   * Extracts the placeholders from the stored routine source.
620
   *
621
   * @throws RoutineLoaderException
622
   */
623
  private function extractPlaceholders(): void
624
  {
625
    $unknown = [];
626
627
    preg_match_all('(@[A-Za-z0-9_.]+(%(type|sort))?@)', $this->routineSourceCode, $matches);
628
    if (!empty($matches[0]))
629
    {
630
      foreach ($matches[0] as $placeholder)
631
      {
632
        if (isset($this->replacePairs[strtoupper($placeholder)]))
633
        {
634
          $this->replace[$placeholder] = $this->replacePairs[strtoupper($placeholder)];
635
        }
636
        else
637
        {
638
          $unknown[] = $placeholder;
639
        }
640
      }
641
    }
642
643
    $this->logUnknownPlaceholders($unknown);
644
  }
645
646
  //--------------------------------------------------------------------------------------------------------------------
647
  /**
648
   * Extracts the return type of the stored routine.
649
   */
650
  private function extractReturnType(): void
651
  {
652
    // Return immediately if designation type is not appropriate for this method.
653
    if (!in_array($this->designationType, ['function', 'singleton0', 'singleton1'])) return;
654
655
    $key = array_search('begin', $this->routineSourceCodeLines);
656
657
    if ($key!==false)
658
    {
659
      for ($i = 1; $i<$key; $i++)
660
      {
661
        $n = preg_match('/^\s*--\s+return:\s*((\w|\|)+)\s*$/',
662
                        $this->routineSourceCodeLines[$key - $i],
663
                        $matches);
664
        if ($n==1)
665
        {
666
          $this->returnType = $matches[1];
667
668
          break;
669
        }
670
      }
671
    }
672
673
    if ($this->returnType===null)
674
    {
675
      $this->returnType = 'mixed';
676
677
      $this->io->logNote('Unable to find the return type of stored routine');
678
    }
679
  }
680
681
  //--------------------------------------------------------------------------------------------------------------------
682
  /**
683
   * Extracts info about the parameters of the stored routine.
684
   *
685
   * @throws RoutineLoaderException
686
   * @throws MySqlQueryErrorException
687
   */
688
  private function extractRoutineParametersInfo(): void
689
  {
690
    $routine_parameters = $this->dl->routineParameters($this->routineName);
691
    foreach ($routine_parameters as $key => $routine_parameter)
692
    {
693
      if ($routine_parameter['parameter_name'])
694
      {
695
        $data_type_descriptor = $routine_parameter['dtd_identifier'];
696
        if (isset($routine_parameter['character_set_name']))
697
        {
698
          $data_type_descriptor .= ' character set '.$routine_parameter['character_set_name'];
699
        }
700
        if (isset($routine_parameter['collation_name']))
701
        {
702
          $data_type_descriptor .= ' collation '.$routine_parameter['collation_name'];
703
        }
704
705
        $routine_parameter['data_type_descriptor'] = $data_type_descriptor;
706
707
        $this->parameters[$key] = $routine_parameter;
708
      }
709
    }
710
711
    $this->updateParametersInfo();
712
  }
713
714
  //--------------------------------------------------------------------------------------------------------------------
715
  /**
716
   * Extracts the name of the stored routine and the stored routine type (i.e. procedure or function) source.
717
   *
718
   * @throws RoutineLoaderException
719
   */
720
  private function extractRoutineTypeAndName(): void
721
  {
722
    $n = preg_match('/create\\s+(procedure|function)\\s+([a-zA-Z0-9_]+)/i', $this->routineSourceCode, $matches);
723
    if ($n==1)
724
    {
725
      $this->routineType = strtolower($matches[1]);
726
727
      if ($this->routineName!=$matches[2])
728
      {
729
        throw new RoutineLoaderException("Stored routine name '%s' does not corresponds with filename", $matches[2]);
730
      }
731
    }
732
    else
733
    {
734
      throw new RoutineLoaderException('Unable to find the stored routine name and type');
735
    }
736
  }
737
738
  //--------------------------------------------------------------------------------------------------------------------
739
  /**
740
   * Gets description by name of the parameter as found in the DocBlock of the stored routine.
741
   *
742
   * @param string $name Name of the parameter.
743
   *
744
   * @return string|null
745
   */
746
  private function getParameterDocDescription(string $name): ?string
747
  {
748
    if (isset($this->docBlockPartsSource['parameters']))
749
    {
750
      foreach ($this->docBlockPartsSource['parameters'] as $parameter_doc_info)
751
      {
752
        if ($parameter_doc_info['name']===$name) return $parameter_doc_info['description'];
753
      }
754
    }
755
756
    return null;
757
  }
758
759
  //--------------------------------------------------------------------------------------------------------------------
760
  /**
761
   * Loads the stored routine into the database.
762
   *
763
   * @throws MySqlQueryErrorException
764
   */
765
  private function loadRoutineFile(): void
766
  {
767
    // Set magic constants specific for this stored routine.
768
    $this->setMagicConstants();
769
770
    // Replace all place holders with their values.
771
    $lines          = explode("\n", $this->routineSourceCode);
772
    $routine_source = [];
773
    foreach ($lines as $i => &$line)
774
    {
775
      $this->replace['__LINE__'] = $i + 1;
776
      $routine_source[$i]        = strtr($line, $this->replace);
777
    }
778
    $routine_source = implode("\n", $routine_source);
779
780
    // Unset magic constants specific for this stored routine.
781
    $this->unsetMagicConstants();
782
783
    // Drop the stored procedure or function if its exists.
784
    $this->dropRoutine();
785
786
    // Set the SQL-mode under which the stored routine will run.
787
    $this->dl->setSqlMode($this->sqlMode);
788
789
    // Set the default character set and collate under which the store routine will run.
790
    $this->dl->setCharacterSet($this->characterSet, $this->collate);
791
792
    // Finally, execute the SQL code for loading the stored routine.
793
    $this->dl->loadRoutine($routine_source);
794
  }
795
796
  //--------------------------------------------------------------------------------------------------------------------
797
  /**
798
   * Logs the unknown placeholder (if any).
799
   *
800
   * @param array $unknown The unknown placeholders.
801
   *
802
   * @throws RoutineLoaderException
803
   */
804
  private function logUnknownPlaceholders(array $unknown): void
805
  {
806
    // Return immediately if there are no unknown placeholders.
807
    if (empty($unknown)) return;
808
809
    sort($unknown);
810
    $this->io->text('Unknown placeholder(s):');
811
    $this->io->listing($unknown);
812
813
    $replace = [];
814
    foreach ($unknown as $placeholder)
815
    {
816
      $replace[$placeholder] = '<error>'.$placeholder.'</error>';
817
    }
818
    $code = strtr(OutputFormatter::escape($this->routineSourceCode), $replace);
819
820
    $this->io->text(explode(PHP_EOL, $code));
821
822
    throw new RoutineLoaderException('Unknown placeholder(s) found');
823
  }
824
825
  //--------------------------------------------------------------------------------------------------------------------
826
  /**
827
   * Returns true if the source file must be load or reloaded. Otherwise returns false.
828
   *
829
   * @return bool
830
   */
831 1
  private function mustLoadStoredRoutine(): bool
832
  {
833
    // If this is the first time we see the source file it must be loaded.
834 1
    if (empty($this->phpStratumOldMetadata)) return true;
835
836
    // If the source file has changed the source file must be loaded.
837 1
    if ($this->phpStratumOldMetadata['timestamp']!=$this->filemtime) return true;
838
839
    // If the value of a placeholder has changed the source file must be loaded.
840 1
    foreach ($this->phpStratumOldMetadata['replace'] as $place_holder => $old_value)
841
    {
842 1
      if (!isset($this->replacePairs[strtoupper($place_holder)]) ||
843 1
        $this->replacePairs[strtoupper($place_holder)]!==$old_value)
844
      {
845
        return true;
846
      }
847
    }
848
849
    // If stored routine not exists in database the source file must be loaded.
850 1
    if (empty($this->rdbmsOldRoutineMetadata)) return true;
851
852
    // If current sql-mode is different the source file must reload.
853 1
    if ($this->rdbmsOldRoutineMetadata['sql_mode']!=$this->sqlMode) return true;
854
855
    // If current character set is different the source file must reload.
856 1
    if ($this->rdbmsOldRoutineMetadata['character_set_client']!=$this->characterSet) return true;
857
858
    // If current collation is different the source file must reload.
859 1
    if ($this->rdbmsOldRoutineMetadata['collation_connection']!=$this->collate) return true;
860
861 1
    return false;
862
  }
863
864
  //--------------------------------------------------------------------------------------------------------------------
865
  /**
866
   * Reads the source code of the stored routine.
867
   *
868
   * @throws RoutineLoaderException
869
   */
870
  private function readSourceCode(): void
871
  {
872
    $this->routineSourceCode      = file_get_contents($this->sourceFilename);
873
    $this->routineSourceCodeLines = explode("\n", $this->routineSourceCode);
874
875
    if ($this->routineSourceCodeLines===false)
876
    {
877
      throw new RoutineLoaderException('Source file is empty');
878
    }
879
  }
880
881
  //--------------------------------------------------------------------------------------------------------------------
882
  /**
883
   * Adds magic constants to replace list.
884
   */
885
  private function setMagicConstants(): void
886
  {
887
    $real_path = realpath($this->sourceFilename);
888
889
    $this->replace['__FILE__']    = "'".$this->dl->realEscapeString($real_path)."'";
890
    $this->replace['__ROUTINE__'] = "'".$this->routineName."'";
891
    $this->replace['__DIR__']     = "'".$this->dl->realEscapeString(dirname($real_path))."'";
892
  }
893
894
  //--------------------------------------------------------------------------------------------------------------------
895
  /**
896
   * Removes magic constants from current replace list.
897
   */
898
  private function unsetMagicConstants(): void
899
  {
900
    unset($this->replace['__FILE__']);
901
    unset($this->replace['__ROUTINE__']);
902
    unset($this->replace['__DIR__']);
903
    unset($this->replace['__LINE__']);
904
  }
905
906
  //--------------------------------------------------------------------------------------------------------------------
907
  /**
908
   * Updates the metadata for the stored routine.
909
   */
910
  private function updateMetadata(): void
911
  {
912
    $this->phpStratumMetadata['routine_name']           = $this->routineName;
913
    $this->phpStratumMetadata['designation']            = $this->designationType;
914
    $this->phpStratumMetadata['return']                 = $this->returnType;
915
    $this->phpStratumMetadata['parameters']             = $this->parameters;
916
    $this->phpStratumMetadata['timestamp']              = $this->filemtime;
917
    $this->phpStratumMetadata['replace']                = $this->replace;
918
    $this->phpStratumMetadata['phpdoc']                 = $this->docBlockPartsWrapper;
919
    $this->phpStratumMetadata['spec_params']            = $this->extendedParameters;
920
    $this->phpStratumMetadata['index_columns']          = $this->indexColumns;
921
    $this->phpStratumMetadata['bulk_insert_table_name'] = $this->bulkInsertTableName;
922
    $this->phpStratumMetadata['bulk_insert_columns']    = $this->bulkInsertColumns;
923
    $this->phpStratumMetadata['bulk_insert_keys']       = $this->bulkInsertKeys;
924
  }
925
926
  //--------------------------------------------------------------------------------------------------------------------
927
  /**
928
   * Update information about specific parameters of stored routine.
929
   *
930
   * @throws RoutineLoaderException
931
   */
932
  private function updateParametersInfo(): void
933
  {
934
    if (!empty($this->extendedParameters))
935
    {
936
      foreach ($this->extendedParameters as $spec_param_name => $spec_param_info)
937
      {
938
        $param_not_exist = true;
939
        foreach ($this->parameters as $key => $param_info)
940
        {
941
          if ($param_info['parameter_name']==$spec_param_name)
942
          {
943
            $this->parameters[$key] = array_merge($this->parameters[$key], $spec_param_info);
944
            $param_not_exist        = false;
945
            break;
946
          }
947
        }
948
        if ($param_not_exist)
949
        {
950
          throw new RoutineLoaderException("Specific parameter '%s' does not exist", $spec_param_name);
951
        }
952
      }
953
    }
954
  }
955
956
  //--------------------------------------------------------------------------------------------------------------------
957
  /**
958
   * Validates the parameters found the DocBlock in the source of the stored routine against the parameters from the
959
   * metadata of MySQL and reports missing and unknown parameters names.
960
   */
961
  private function validateParameterLists(): void
962
  {
963
    // Make list with names of parameters used in database.
964
    $database_parameters_names = [];
965
    foreach ($this->parameters as $parameter_info)
966
    {
967
      $database_parameters_names[] = $parameter_info['parameter_name'];
968
    }
969
970
    // Make list with names of parameters used in dock block of routine.
971
    $doc_block_parameters_names = [];
972
    if (isset($this->docBlockPartsSource['parameters']))
973
    {
974
      foreach ($this->docBlockPartsSource['parameters'] as $parameter)
975
      {
976
        $doc_block_parameters_names[] = $parameter['name'];
977
      }
978
    }
979
980
    // Check and show warning if any parameters is missing in doc block.
981
    $tmp = array_diff($database_parameters_names, $doc_block_parameters_names);
982
    foreach ($tmp as $name)
983
    {
984
      $this->io->logNote('Parameter <dbo>%s</dbo> is missing from doc block', $name);
985
    }
986
987
    // Check and show warning if find unknown parameters in doc block.
988
    $tmp = array_diff($doc_block_parameters_names, $database_parameters_names);
989
    foreach ($tmp as $name)
990
    {
991
      $this->io->logNote('Unknown parameter <dbo>%s</dbo> found in doc block', $name);
992
    }
993
  }
994
995
  //--------------------------------------------------------------------------------------------------------------------
996
  /**
997
   * Validates the specified return type of the stored routine.
998
   *
999
   * @throws RoutineLoaderException
1000
   */
1001
  private function validateReturnType(): void
1002
  {
1003
    // Return immediately if designation type is not appropriate for this method.
1004
    if (!in_array($this->designationType, ['function', 'singleton0', 'singleton1'])) return;
1005
1006
    $types = explode('|', $this->returnType);
1007
    $diff  = array_diff($types, ['string', 'int', 'float', 'double', 'bool', 'null']);
1008
1009
    if (!($this->returnType=='mixed' || $this->returnType=='bool' || empty($diff)))
1010
    {
1011
      throw new RoutineLoaderException("Return type must be 'mixed', 'bool', or a combination of 'int', 'float', 'string', and 'null'");
1012
    }
1013
1014
    // The following tests are applicable for singleton0 routines only.
1015
    if (!in_array($this->designationType, ['singleton0'])) return;
1016
1017
    // Return mixed is OK.
1018
    if (in_array($this->returnType, ['bool', 'mixed'])) return;
1019
1020
    // In all other cases return type must contain null.
1021
    $parts = explode('|', $this->returnType);
1022
    $key   = array_search('null', $parts);
1023
    if ($key===false)
1024
    {
1025
      throw new RoutineLoaderException("Return type must be 'mixed', 'bool', or contain 'null' (with a combination of 'int', 'float', and 'string')");
1026
    }
1027
  }
1028
1029
  //--------------------------------------------------------------------------------------------------------------------
1030
}
1031
1032
//----------------------------------------------------------------------------------------------------------------------
1033