Completed
Push — master ( 3bb391...1a7c41 )
by P.R.
03:42
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\Helper\Cast;
8
use SetBased\Helper\InvalidCastException;
9
use SetBased\Stratum\Backend\StratumStyle;
10
use SetBased\Stratum\Common\DocBlock\DocBlockReflection;
11
use SetBased\Stratum\Common\Exception\RoutineLoaderException;
12
use SetBased\Stratum\Middle\Exception\ResultException;
13
use SetBased\Stratum\MySql\Exception\MySqlQueryErrorException;
14
use SetBased\Stratum\MySql\MySqlMetaDataLayer;
15
use Symfony\Component\Console\Formatter\OutputFormatter;
16
17
/**
18
 * Class for loading a single stored routine into a MySQL instance from pseudo SQL file.
19
 */
20
class RoutineLoaderHelper
21
{
22
  //--------------------------------------------------------------------------------------------------------------------
23
  /**
24
   * MySQL's and MariaDB's SQL/PSM syntax.
25
   */
26
  const SQL_PSM_SYNTAX = 1;
27
28
  /**
29
   * Oracle PL/SQL syntax.
30
   */
31
  const PL_SQL_SYNTAX = 2;
32
33
  //--------------------------------------------------------------------------------------------------------------------
34
  /**
35
   * The metadata of the table columns of the table for bulk insert.
36
   *
37
   * @var array[]
38
   */
39
  private $bulkInsertColumns;
40
41
  /**
42
   * The keys in the nested array for bulk inserting data.
43
   *
44
   * @var string[]
45
   */
46
  private $bulkInsertKeys;
47
48
  /**
49
   * The name of table for bulk insert.
50
   *
51
   * @var string
52
   */
53
  private $bulkInsertTableName;
54
55
  /**
56
   * The default character set under which the stored routine will be loaded and run.
57
   *
58
   * @var string
59
   */
60
  private $characterSet;
61
62
  /**
63
   * The default collate under which the stored routine will be loaded and run.
64
   *
65
   * @var string
66
   */
67
  private $collate;
68
69
  /**
70
   * The designation type of the stored routine.
71
   *
72
   * @var string
73
   */
74
  private $designationType;
75
76
  /**
77
   * The meta data layer.
78
   *
79
   * @var MySqlMetaDataLayer
80
   */
81
  private $dl;
82
83
  /**
84
   * The DocBlock reflection object.
85
   *
86
   * @var DocBlockReflection|null
87
   */
88
  private $docBlockReflection;
89
90
  /**
91
   * Information about parameters with specific format (string in CSV format etc.).
92
   *
93
   * @var array
94
   */
95
  private $extendedParameters;
96
97
  /**
98
   * The last modification time of the source file.
99
   *
100
   * @var int
101
   */
102
  private $filemtime;
103
104
  /**
105
   * The key or index columns (depending on the designation type) of the stored routine.
106
   *
107
   * @var string[]
108
   */
109
  private $indexColumns;
110
111
  /**
112
   * The Output decorator
113
   *
114
   * @var StratumStyle
115
   */
116
  private $io;
117
118
  /**
119
   * The information about the parameters of the stored routine.
120
   *
121
   * @var array[]
122
   */
123
  private $parameters = [];
124
125
  /**
126
   * The metadata of the stored routine. Note: this data is stored in the metadata file and is generated by PhpStratum.
127
   *
128
   * @var array
129
   */
130
  private $phpStratumMetadata;
131
132
  /**
133
   * The old metadata of the stored routine.  Note: this data comes from the metadata file.
134
   *
135
   * @var array
136
   */
137
  private $phpStratumOldMetadata;
138
139
  /**
140
   * The old metadata of the stored routine. Note: this data comes from information_schema.ROUTINES.
141
   *
142
   * @var array
143
   */
144
  private $rdbmsOldRoutineMetadata;
145
146
  /**
147
   * The replace pairs (i.e. placeholders and their actual values, see strst).
148
   *
149
   * @var array
150
   */
151
  private $replace = [];
152
153
  /**
154
   * A map from placeholders to their actual values.
155
   *
156
   * @var array
157
   */
158
  private $replacePairs;
159
160
  /**
161
   * The return type of the stored routine (only if designation type singleton0, singleton1, or function).
162
   *
163
   * @var string|null
164
   */
165
  private $returnType;
166
167
  /**
168
   * The name of the stored routine.
169
   *
170
   * @var string
171
   */
172
  private $routineName;
173
174
  /**
175
   * The source code as a single string of the stored routine.
176
   *
177
   * @var string
178
   */
179
  private $routineSourceCode;
180
181
  /**
182
   * The source code as an array of lines string of the stored routine.
183
   *
184
   * @var array
185
   */
186
  private $routineSourceCodeLines;
187
188
  /**
189
   * The source filename holding the stored routine.
190
   *
191
   * @var string
192
   */
193
  private $sourceFilename;
194
195
  /**
196
   * The SQL mode helper object.
197
   *
198
   * @var SqlModeHelper
199
   */
200
  private $sqlModeHelper;
201
202
  /**
203
   * The syntax of the stored routine. Either SQL_PSM_SYNTAX or PL_SQL_SYNTAX.
204
   *
205
   * @var int
206
   */
207
  private $syntax;
208
209
  //--------------------------------------------------------------------------------------------------------------------
210
  /**
211
   * Object constructor.
212
   *
213
   * @param MySqlMetaDataLayer $dl                      The meta data layer.
214
   * @param StratumStyle       $io                      The output for log messages.
215
   * @param SqlModeHelper      $sqlModeHelper
216
   * @param string             $routineFilename         The filename of the source of the stored routine.
217
   * @param array              $phpStratumMetadata      The metadata of the stored routine from PhpStratum.
218
   * @param array              $replacePairs            A map from placeholders to their actual values.
219
   * @param array              $rdbmsOldRoutineMetadata The old metadata of the stored routine from MySQL.
220
   * @param string             $characterSet            The default character set under which the stored routine will
221
   *                                                    be loaded and run.
222
   * @param string             $collate                 The key or index columns (depending on the designation type) of
223
   *                                                    the stored routine.
224
   */
225 1
  public function __construct(MySqlMetaDataLayer $dl,
226
                              StratumStyle $io,
227
                              SqlModeHelper $sqlModeHelper,
228
                              string $routineFilename,
229
                              array $phpStratumMetadata,
230
                              array $replacePairs,
231
                              array $rdbmsOldRoutineMetadata,
232
                              string $characterSet,
233
                              string $collate)
234
  {
235 1
    $this->dl                      = $dl;
236 1
    $this->io                      = $io;
237 1
    $this->sqlModeHelper           = $sqlModeHelper;
238 1
    $this->sourceFilename          = $routineFilename;
239 1
    $this->phpStratumMetadata      = $phpStratumMetadata;
240 1
    $this->replacePairs            = $replacePairs;
241 1
    $this->rdbmsOldRoutineMetadata = $rdbmsOldRoutineMetadata;
242 1
    $this->characterSet            = $characterSet;
243 1
    $this->collate                 = $collate;
244 1
  }
245
246
  //--------------------------------------------------------------------------------------------------------------------
247
  /**
248
   * Extract column metadata from the rows returned by the SQL statement 'describe table'.
249
   *
250
   * @param array $description The description of the table.
251
   *
252
   * @return array
253
   *
254
   * @throws InvalidCastException
255
   */
256
  private static function extractColumnsFromTableDescription(array $description): array
257
  {
258
    $ret = [];
259
260
    foreach ($description as $column)
261
    {
262
      preg_match('/^(?<data_type>\w+)(?<extra>.*)?$/', $column['Type'], $parts1);
263
264
      $tmp = ['column_name'       => $column['Field'],
265
              'data_type'         => $parts1['data_type'],
266
              'numeric_precision' => null,
267
              'numeric_scale'     => null,
268
              'dtd_identifier'    => $column['Type']];
269
270
      switch ($parts1[1])
271
      {
272
        case 'tinyint':
273
          preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2);
274
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 4);
275
          $tmp['numeric_scale']     = 0;
276
          break;
277
278
        case 'smallint':
279
          preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2);
280
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 6);
281
          $tmp['numeric_scale']     = 0;
282
          break;
283
284
        case 'mediumint':
285
          preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2);
286
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 9);
287
          $tmp['numeric_scale']     = 0;
288
          break;
289
290
        case 'int':
291
          preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2);
292
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 11);
293
          $tmp['numeric_scale']     = 0;
294
          break;
295
296
        case 'bigint':
297
          preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2);
298
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 20);
299
          $tmp['numeric_scale']     = 0;
300
          break;
301
302
        case 'year':
303
          // Nothing to do.
304
          break;
305
306
        case 'float':
307
          $tmp['numeric_precision'] = 12;
308
          break;
309
310
        case 'double':
311
          $tmp['numeric_precision'] = 22;
312
          break;
313
314
        case 'binary':
315
        case 'char':
316
        case 'varbinary':
317
        case 'varchar':
318
          // Nothing to do (binary) strings.
319
          break;
320
321
        case 'decimal':
322
          preg_match('/^\((?<precision>\d+),(<?scale>\d+)\)$/', $parts1['extra'], $parts2);
323
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 65);
324
          $tmp['numeric_scale']     = Cast::toManInt($parts2['scale'] ?? 0);
325
          break;
326
327
        case 'time':
328
        case 'timestamp':
329
        case 'date':
330
        case 'datetime':
331
          // Nothing to do date and time.
332
          break;
333
334
        case 'enum':
335
        case 'set':
336
          // Nothing to do sets.
337
          break;
338
339
        case 'bit':
340
          preg_match('/^\((?<precision>\d+)\)$/', $parts1['extra'], $parts2);
341
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision']);
342
          break;
343
344
        case 'tinytext':
345
        case 'text':
346
        case 'mediumtext':
347
        case 'longtext':
348
        case 'tinyblob':
349
        case 'blob':
350
        case 'mediumblob':
351
        case 'longblob':
352
          // Nothing to do CLOBs and BLOBs.
353
          break;
354
355
        default:
356
          throw new FallenException('data type', $parts1[1]);
357
      }
358
359
      $ret[] = $tmp;
360
    }
361
362
    return $ret;
363
  }
364
365
  //--------------------------------------------------------------------------------------------------------------------
366
  /**
367
   * Loads the stored routine into the instance of MySQL and returns the metadata of the stored routine.
368
   *
369
   * @return array
370
   *
371
   * @throws RoutineLoaderException
372
   * @throws MySqlQueryErrorException
373
   * @throws ResultException
374
   * @throws InvalidCastException
375
   */
376 1
  public function loadStoredRoutine(): array
377
  {
378 1
    $this->routineName           = pathinfo($this->sourceFilename, PATHINFO_FILENAME);
379 1
    $this->phpStratumOldMetadata = $this->phpStratumMetadata;
380 1
    $this->filemtime             = filemtime($this->sourceFilename);
381
382 1
    $load = $this->mustLoadStoredRoutine();
383 1
    if ($load)
384
    {
385
      $this->io->text(sprintf('Loading routine <dbo>%s</dbo>', OutputFormatter::escape($this->routineName)));
386
387
      $this->readSourceCode();
388
      $this->extractPlaceholders();
389
      $this->extractDesignationType();
390
      $this->extractReturnType();
391
      $this->extractRoutineTypeAndName();
392
      $this->extractSyntax();
393
      $this->validateReturnType();
394
      $this->loadRoutineFile();
395
      $this->extractBulkInsertTableColumnsInfo();
396
      $this->extractExtendedParametersInfo();
397
      $this->extractRoutineParametersInfo();
398
      $this->extractDocBlockPartsWrapper();
399
      $this->validateParameterLists();
400
      $this->updateMetadata();
401
    }
402
403 1
    return $this->phpStratumMetadata;
404
  }
405
406
  //--------------------------------------------------------------------------------------------------------------------
407
  /**
408
   * Drops the stored routine if it exists.
409
   *
410
   * @throws MySqlQueryErrorException
411
   */
412
  private function dropRoutineIfExists(): void
413
  {
414
    if (!empty($this->rdbmsOldRoutineMetadata))
415
    {
416
      $this->dl->dropRoutine($this->rdbmsOldRoutineMetadata['routine_type'], $this->routineName);
417
    }
418
  }
419
420
  //--------------------------------------------------------------------------------------------------------------------
421
  /**
422
   *  Extracts the column names and column types of the current table for bulk insert.
423
   *
424
   * @throws InvalidCastException
425
   * @throws MySqlQueryErrorException
426
   * @throws ResultException
427
   * @throws RoutineLoaderException
428
   */
429
  private function extractBulkInsertTableColumnsInfo(): void
430
  {
431
    // Return immediately if designation type is not appropriate for this method.
432
    if ($this->designationType!='bulk_insert') return;
433
434
    // Check if table is a temporary table or a non-temporary table.
435
    $table_is_non_temporary = $this->dl->checkTableExists($this->bulkInsertTableName);
436
437
    // Create temporary table if table is non-temporary table.
438
    if (!$table_is_non_temporary)
439
    {
440
      $this->dl->callProcedure($this->routineName);
441
    }
442
443
    // Get information about the columns of the table.
444
    $description = $this->dl->describeTable($this->bulkInsertTableName);
445
446
    // Drop temporary table if table is non-temporary.
447
    if (!$table_is_non_temporary)
448
    {
449
      $this->dl->dropTemporaryTable($this->bulkInsertTableName);
450
    }
451
452
    // Check number of columns in the table match the number of fields given in the designation type.
453
    $n1 = sizeof($this->bulkInsertKeys);
454
    $n2 = sizeof($description);
455
    if ($n1!=$n2)
456
    {
457
      throw new RoutineLoaderException("Number of fields %d and number of columns %d don't match.", $n1, $n2);
458
    }
459
460
    $this->bulkInsertColumns = self::extractColumnsFromTableDescription($description);
461
  }
462
463
  //--------------------------------------------------------------------------------------------------------------------
464
  /**
465
   * Extracts the designation type of the stored routine.
466
   *
467
   * @throws RoutineLoaderException
468
   */
469
  private function extractDesignationType(): void
470
  {
471
    $tags = $this->docBlockReflection->getTags('type');
1 ignored issue
show
Bug introduced by
The method getTags() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

471
    /** @scrutinizer ignore-call */ 
472
    $tags = $this->docBlockReflection->getTags('type');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
472
    if (count($tags)===0)
473
    {
474
      throw new RoutineLoaderException('Tag @type not found in DocBlock.');
475
    }
476
    elseif (count($tags)>1)
477
    {
478
      throw new RoutineLoaderException('Multiple @type tags found in DocBlock.');
479
    }
480
481
    $tag                   = $tags[0];
482
    $this->designationType = $tag['arguments'][0];
483
    switch ($this->designationType)
484
    {
485
      case 'bulk_insert':
486
        if ($tag['arguments'][1]==='' || $tag['arguments'][2]==='' || $tag['description'][0]!='')
487
        {
488
          throw new RoutineLoaderException('Invalid @type tag. Expected: @type bulk_insert <table_name> <columns>');
489
        }
490
        $this->bulkInsertTableName = $tag['arguments'][1];
491
        $this->bulkInsertKeys      = explode(',', $tag['arguments'][2]);
492
        break;
493
494
      case 'rows_with_key':
495
      case 'rows_with_index':
496
        if ($tag['arguments'][1]==='' || $tag['arguments'][2]!=='')
497
        {
498
          throw new RoutineLoaderException('Invalid @type tag. Expected: @type %s <columns>', $this->designationType);
499
        }
500
        $this->indexColumns = explode(',', $tag['arguments'][1]);
501
        break;
502
503
      default:
504
        if ($tag['arguments'][1]!=='')
505
        {
506
          throw new RoutineLoaderException('Error: Expected: @type %s', $this->designationType);
507
        }
508
    }
509
  }
510
511
  //--------------------------------------------------------------------------------------------------------------------
512
  /**
513
   * Detects the syntax of the stored procedure. Either SQL/PSM or PL/SQL.
514
   *
515
   * @throws RoutineLoaderException
516
   */
517
  private function extractSyntax(): void
518
  {
519
    if ($this->sqlModeHelper->hasOracleMode())
520
    {
521
      if ($this->findFirstMatchingLine('/^\s*(modifies|reads)\s+sql\s+data\s*$/i')!==null)
522
      {
523
        $this->syntax = self::SQL_PSM_SYNTAX;
524
      }
525
      else
526
      {
527
        $key1 = $this->findFirstMatchingLine('/^\s*(as|is)\s*$/i');
528
        $key2 = $this->findFirstMatchingLine('/^\s*begin\s*$/i');
529
530
        if ($key1!==null and $key2!==null and $key1 < $key2)
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

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