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

RoutineLoaderHelper::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 9
nc 1
nop 9
dl 0
loc 19
ccs 10
cts 10
cp 1
crap 1
rs 9.9666
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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