Completed
Push — master ( 3bb391...1a7c41 )
by P.R.
03:42
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\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