Passed
Push — master ( 47f1d1...f27cfc )
by P.R.
01:53
created

RoutineLoaderHelper::mustLoadStoredRoutine()   C

Complexity

Conditions 13
Paths 25

Size

Total Lines 57
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 14.9846

Importance

Changes 0
Metric Value
cc 13
eloc 19
nc 25
nop 0
dl 0
loc 57
ccs 17
cts 22
cp 0.7727
crap 14.9846
rs 6.6166
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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\Common\Helper\Util;
13
use SetBased\Stratum\Middle\Exception\ResultException;
14
use SetBased\Stratum\MySql\Exception\MySqlQueryErrorException;
15
use SetBased\Stratum\MySql\MySqlMetaDataLayer;
16
use Symfony\Component\Console\Formatter\OutputFormatter;
17
18
/**
19
 * Class for loading a single stored routine into a MySQL instance from pseudo SQL file.
20
 */
21
class RoutineLoaderHelper
22
{
23
  //--------------------------------------------------------------------------------------------------------------------
24
  /**
25
   * MySQL's and MariaDB's SQL/PSM syntax.
26
   */
27
  const SQL_PSM_SYNTAX = 1;
28
29
  /**
30
   * Oracle PL/SQL syntax.
31
   */
32
  const PL_SQL_SYNTAX = 2;
33
34
  /**
35
   * The revision of the metadata of the stored routines.
36
   */
37
  const METADATA_REVISION = '3';
38
39
  /**
40
   * The metadata of the table columns of the table for bulk insert.
41
   *
42
   * @var array[]
43
   */
44
  private array $bulkInsertColumns;
45
46
  /**
47
   * The keys in the nested array for bulk inserting data.
48
   *
49
   * @var string[]
50
   */
51
  private array $bulkInsertKeys;
52
53
  /**
54
   * The name of table for bulk insert.
55
   *
56
   * @var string
57
   */
58
  private string $bulkInsertTableName;
59
60
  /**
61
   * The default character set under which the stored routine will be loaded and run.
62
   *
63
   * @var string
64
   */
65
  private string $characterSet;
66
67
  /**
68
   * The default collate under which the stored routine will be loaded and run.
69
   *
70
   * @var string
71
   */
72
  private string $collate;
73
74
  /**
75
   * The designation type of the stored routine.
76
   *
77
   * @var string
78
   */
79
  private string $designationType;
80
81
  /**
82
   * The meta data layer.
83
   *
84
   * @var MySqlMetaDataLayer
85
   */
86
  private MySqlMetaDataLayer $dl;
87
88
  /**
89
   * The DocBlock reflection object.
90
   *
91
   * @var DocBlockReflection|null
92
   */
93
  private ?DocBlockReflection $docBlockReflection;
94
95
  /**
96
   * The last modification time of the source file.
97
   *
98
   * @var int
99
   */
100
  private int $filemtime;
101
102
  /**
103
   * The key or index columns (depending on the designation type) of the stored routine.
104
   *
105
   * @var string[]
106
   */
107
  private array $indexColumns;
108
109
  /**
110
   * The Output decorator.
111
   *
112
   * @var StratumStyle
113
   */
114
  private StratumStyle $io;
115
116
  /**
117
   * The metadata of the stored routine. Note: this data is stored in the metadata file and is generated by PhpStratum.
118
   *
119
   * @var array
120
   */
121
  private array $phpStratumMetadata;
122
123
  /**
124
   * The old metadata of the stored routine.  Note: this data comes from the metadata file.
125
   *
126
   * @var array
127
   */
128
  private array $phpStratumOldMetadata;
129
130
  /**
131
   * A map from all possible placeholders to their actual values.
132
   *
133
   * @var array
134
   */
135
  private array $placeholderPool;
136
137
  /**
138
   * A map from placeholders that are actually used in the stored routine to their values.
139
   *
140
   * @var array
141
   */
142
  private array $placeholders = [];
143
144
  /**
145
   * The old metadata of the stored routine. Note: this data comes from information_schema.ROUTINES.
146
   *
147
   * @var array
148
   */
149
  private array $rdbmsOldRoutineMetadata;
150
151
  /**
152
   * The return type of the stored routine (only if designation type singleton0, singleton1, or function).
153
   *
154
   * @var string|null
155
   */
156
  private ?string $returnType = null;
157
158
  /**
159
   * The name of the stored routine.
160
   *
161
   * @var string
162
   */
163
  private string $routineName;
164
165
  /**
166
   * The routine parameters.
167
   *
168
   * @var RoutineParametersHelper|null
169
   */
170
  private ?RoutineParametersHelper $routineParameters = null;
171
172
  /**
173
   * The source code as a single string of the stored routine.
174
   *
175
   * @var string
176
   */
177
  private string $routineSourceCode;
178
179
  /**
180
   * The source code as an array of lines string of the stored routine.
181
   *
182
   * @var array
183
   */
184
  private array $routineSourceCodeLines;
185
186
  /**
187
   * The source filename holding the stored routine.
188
   *
189
   * @var string
190
   */
191
  private string $sourceFilename;
192
193
  /**
194
   * The SQL mode helper object.
195
   *
196
   * @var SqlModeHelper
197
   */
198
  private SqlModeHelper $sqlModeHelper;
199
200
  /**
201
   * The syntax of the stored routine. Either SQL_PSM_SYNTAX or PL_SQL_SYNTAX.
202
   *
203
   * @var int
204
   */
205
  private int $syntax;
206
207
  /**
208
   * A map from all possible table and column names to their actual column type.
209
   *
210
   * @var array
211
   */
212
  private array $typeHintPool;
213
214
  /**
215
   * A map from the table and column names that are actually use as type hint in the stored routine to their actual
216
   * column type.
217
   *
218
   * @var array
219
   */
220
  private array $typeHints = [];
221
222 1
  //--------------------------------------------------------------------------------------------------------------------
223
224
  /**
225
   * Object constructor.
226
   *
227
   * @param MySqlMetaDataLayer $dl                      The meta data layer.
228
   * @param StratumStyle       $io                      The output for log messages.
229
   * @param SqlModeHelper      $sqlModeHelper
230
   * @param string             $routineFilename         The filename of the source of the stored routine.
231
   * @param array              $phpStratumMetadata      The metadata of the stored routine from PhpStratum.
232 1
   * @param array              $placeholderPool         A map from placeholders to their actual values.
233 1
   * @param array              $typeHintPool            A map from type hints to their actual types.
234 1
   * @param array              $rdbmsOldRoutineMetadata The old metadata of the stored routine from MySQL.
235 1
   * @param string             $characterSet            The default character set under which the stored routine will
236 1
   *                                                    be loaded and run.
237 1
   * @param string             $collate                 The key or index columns (depending on the designation type) of
238 1
   *                                                    the stored routine.
239 1
   */
240 1
  public function __construct(MySqlMetaDataLayer $dl,
241
                              StratumStyle       $io,
242
                              SqlModeHelper      $sqlModeHelper,
243
                              string             $routineFilename,
244
                              array              $phpStratumMetadata,
245
                              array              $placeholderPool,
246
                              array              $typeHintPool,
247
                              array              $rdbmsOldRoutineMetadata,
248
                              string             $characterSet,
249
                              string             $collate)
250
  {
251
    $this->dl                      = $dl;
252
    $this->io                      = $io;
253 1
    $this->sqlModeHelper           = $sqlModeHelper;
254
    $this->sourceFilename          = $routineFilename;
255 1
    $this->phpStratumMetadata      = $phpStratumMetadata;
256
    $this->placeholderPool         = $placeholderPool;
257 1
    $this->typeHintPool            = $typeHintPool;
258
    $this->rdbmsOldRoutineMetadata = $rdbmsOldRoutineMetadata;
259 1
    $this->characterSet            = $characterSet;
260
    $this->collate                 = $collate;
261 1
  }
262 1
263
  //--------------------------------------------------------------------------------------------------------------------
264
  /**
265 1
   * Extract column metadata from the rows returned by the SQL statement 'describe table'.
266
   *
267 1
   * @param array $description The description of the table.
268
   *
269 1
   * @return array
270 1
   *
271 1
   * @throws InvalidCastException
272 1
   */
273 1
  private static function extractColumnsFromTableDescription(array $description): array
274
  {
275 1
    $ret = [];
276 1
277 1
    foreach ($description as $column)
278 1
    {
279 1
      preg_match('/^(?<data_type>\w+)(?<extra>.*)?$/', $column['Type'], $parts1);
280
281 1
      $tmp = ['column_name'       => $column['Field'],
282 1
              'data_type'         => $parts1['data_type'],
283 1
              'numeric_precision' => null,
284 1
              'numeric_scale'     => null,
285 1
              'dtd_identifier'    => $column['Type']];
286
287 1
      switch ($parts1[1])
288 1
      {
289 1
        case 'tinyint':
290 1
          preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2);
291 1
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 4);
292
          $tmp['numeric_scale']     = 0;
293 1
          break;
294 1
295 1
        case 'smallint':
296 1
          preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2);
297 1
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 6);
298
          $tmp['numeric_scale']     = 0;
299 1
          break;
300
301 1
        case 'mediumint':
302
          preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2);
303 1
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 9);
304 1
          $tmp['numeric_scale']     = 0;
305 1
          break;
306
307 1
        case 'int':
308 1
          preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2);
309 1
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 11);
310
          $tmp['numeric_scale']     = 0;
311 1
          break;
312 1
313 1
        case 'bigint':
314 1
          preg_match('/^\((?<precision>\d+)\)/', $parts1['extra'], $parts2);
315
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 20);
316 1
          $tmp['numeric_scale']     = 0;
317
          break;
318 1
319 1
        case 'year':
320 1
          // Nothing to do.
321 1
          break;
322 1
323
        case 'float':
324 1
          $tmp['numeric_precision'] = 12;
325 1
          break;
326 1
327 1
        case 'double':
328
          $tmp['numeric_precision'] = 22;
329 1
          break;
330
331 1
        case 'binary':
332 1
        case 'char':
333
        case 'varbinary':
334 1
        case 'varchar':
335
          // Nothing to do (binary) strings.
336 1
          break;
337 1
338 1
        case 'decimal':
339 1
          preg_match('/^\((?<precision>\d+),(<?scale>\d+)\)$/', $parts1['extra'], $parts2);
340
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision'] ?? 65);
341
          $tmp['numeric_scale']     = Cast::toManInt($parts2['scale'] ?? 0);
342
          break;
343
344
        case 'time':
345
        case 'timestamp':
346
        case 'date':
347
        case 'datetime':
348
          // Nothing to do date and time.
349
          break;
350
351
        case 'enum':
352
        case 'set':
353
          // Nothing to do sets.
354
          break;
355
356 1
        case 'bit':
357
          preg_match('/^\((?<precision>\d+)\)$/', $parts1['extra'], $parts2);
358
          $tmp['numeric_precision'] = Cast::toManInt($parts2['precision']);
359 1
          break;
360
361
        case 'tinytext':
362
        case 'text':
363
        case 'mediumtext':
364
        case 'longtext':
365
        case 'tinyblob':
366
        case 'blob':
367
        case 'mediumblob':
368
        case 'longblob':
369
          // Nothing to do CLOBs and BLOBs.
370
          break;
371
372
        default:
373 1
          throw new FallenException('data type', $parts1[1]);
374
      }
375 1
376 1
      $ret[] = $tmp;
377 1
    }
378
379 1
    return $ret;
380 1
  }
381
382 1
  //--------------------------------------------------------------------------------------------------------------------
383
  /**
384 1
   * Loads the stored routine into the instance of MySQL and returns the metadata of the stored routine.
385 1
   *
386 1
   * @return array
387 1
   *
388 1
   * @throws RoutineLoaderException
389 1
   * @throws MySqlQueryErrorException
390 1
   * @throws ResultException
391
   * @throws InvalidCastException
392 1
   */
393
  public function loadStoredRoutine(): array
394 1
  {
395 1
    $this->routineName           = pathinfo($this->sourceFilename, PATHINFO_FILENAME);
0 ignored issues
show
Documentation Bug introduced by
It seems like pathinfo($this->sourceFi...lper\PATHINFO_FILENAME) can also be of type array. However, the property $routineName is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
396 1
    $this->phpStratumOldMetadata = $this->phpStratumMetadata;
397
    $this->filemtime             = filemtime($this->sourceFilename);
398
399 1
    $load = $this->mustLoadStoredRoutine();
400
    if ($load)
401
    {
402
      $this->io->text(sprintf('Loading routine <dbo>%s</dbo>', OutputFormatter::escape($this->routineName)));
403
404
      $this->readSourceCode();
405
      $this->updateSourceTypeHints();
406
      $this->extractPlaceholders();
407
      $this->extractDesignationType();
408 1
      $this->extractReturnType();
409
      $this->extractRoutineTypeAndName();
410 1
      $this->extractSyntax();
411
      $this->validateReturnType();
412
      $this->loadRoutineFile();
413
      $this->extractBulkInsertTableColumnsInfo();
414
      $this->extractParameters();
415
      $this->updateMetadata();
416
    }
417
418
    return $this->phpStratumMetadata;
419
  }
420
421
  //--------------------------------------------------------------------------------------------------------------------
422
  /**
423
   * Drops the stored routine if it exists.
424
   *
425 1
   * @throws MySqlQueryErrorException
426
   */
427
  private function dropRoutineIfExists(): void
428 1
  {
429
    if (!empty($this->rdbmsOldRoutineMetadata))
430
    {
431 1
      $this->dl->dropRoutine($this->rdbmsOldRoutineMetadata['routine_type'], $this->routineName);
432
    }
433
  }
434 1
435
  //--------------------------------------------------------------------------------------------------------------------
436 1
  /**
437
   *  Extracts the column names and column types of the current table for bulk insert.
438
   *
439
   * @throws InvalidCastException
440 1
   * @throws MySqlQueryErrorException
441
   * @throws ResultException
442
   * @throws RoutineLoaderException
443 1
   */
444
  private function extractBulkInsertTableColumnsInfo(): void
445 1
  {
446
    // Return immediately if designation type is not appropriate for this method.
447
    if ($this->designationType!='bulk_insert')
448
    {
449 1
      return;
450 1
    }
451 1
452
    // Check if table is a temporary table or a non-temporary table.
453
    $tableIsNonTemporary = $this->dl->checkTableExists($this->bulkInsertTableName);
454
455
    // Create temporary table if table is non-temporary table.
456 1
    if (!$tableIsNonTemporary)
457
    {
458
      $this->dl->callProcedure($this->routineName);
459
    }
460
461
    // Get information about the columns of the table.
462
    $description = $this->dl->describeTable($this->bulkInsertTableName);
463
464
    // Drop temporary table if table is non-temporary.
465 1
    if (!$tableIsNonTemporary)
466
    {
467 1
      $this->dl->dropTemporaryTable($this->bulkInsertTableName);
468 1
    }
469
470
    // Check number of columns in the table match the number of fields given in the designation type.
471
    $n1 = sizeof($this->bulkInsertKeys);
472 1
    $n2 = sizeof($description);
473
    if ($n1!=$n2)
474
    {
475
      throw new RoutineLoaderException("Number of fields %d and number of columns %d don't match.", $n1, $n2);
476
    }
477 1
478 1
    $this->bulkInsertColumns = self::extractColumnsFromTableDescription($description);
479 1
  }
480
481 1
  //--------------------------------------------------------------------------------------------------------------------
482 1
  /**
483 1
   * Extracts the designation type of the stored routine.
484 1
   *
485
   * @throws RoutineLoaderException
486
   */
487
  private function extractDesignationType(): void
488 1
  {
489 1
    $tags = $this->docBlockReflection->getTags('type');
490 1
    if (count($tags)===0)
491
    {
492 1
      throw new RoutineLoaderException('Tag @type not found in DocBlock.');
493 1
    }
494 1
    elseif (count($tags)>1)
495 1
    {
496
      throw new RoutineLoaderException('Multiple @type tags found in DocBlock.');
497
    }
498
499 1
    $tag                   = $tags[0];
500 1
    $this->designationType = $tag['arguments']['type'];
501
    switch ($this->designationType)
502
    {
503 1
      case 'bulk_insert':
504
        $tag['arguments']['table']   = $tag['arguments']['ex1'];
505
        $tag['arguments']['columns'] = $tag['arguments']['ex2'];
506
        if ($tag['arguments']['table']==='' || $tag['arguments']['columns']==='' || $tag['description'][0]!='')
507
        {
508
          throw new RoutineLoaderException('Invalid @type tag. Expected: @type bulk_insert <table_name> <columns>');
509
        }
510
        $this->bulkInsertTableName = $tag['arguments']['table'];
511
        $this->bulkInsertKeys      = explode(',', $tag['arguments']['columns']);
512
        break;
513
514 1
      case 'rows_with_key':
515
      case 'rows_with_index':
516 1
        $tag['arguments']['columns'] = $tag['arguments']['ex1'];
517 1
        if ($tag['arguments']['columns']==='' || $tag['arguments']['ex2']!=='')
518 1
        {
519
          throw new RoutineLoaderException('Invalid @type tag. Expected: @type %s <columns>', $this->designationType);
520
        }
521
        $this->indexColumns = explode(',', $tag['arguments']['columns']);
522
        break;
523
524
      default:
525
        if ($tag['arguments']['ex1']!=='' || $tag['arguments']['ex2']!=='')
526
        {
527
          throw new RoutineLoaderException('Error: Expected: @type %s', $this->designationType);
528 1
        }
529
    }
530 1
  }
531 1
532 1
  //--------------------------------------------------------------------------------------------------------------------
533 1
  /**
534
   * Extracts DocBlock parts to be used by the wrapper generator.
535 1
   */
536
  private function extractDocBlockPartsWrapper(): array
537
  {
538
    return ['short_description' => $this->docBlockReflection->getShortDescription(),
539
            'long_description'  => $this->docBlockReflection->getLongDescription(),
540
            'parameters'        => $this->routineParameters->extractDocBlockPartsWrapper()];
541
  }
542
543
  //--------------------------------------------------------------------------------------------------------------------
544 1
  /**
545
   * Extracts routine parameters.
546 1
   *
547
   * @throws MySqlQueryErrorException
548 1
   * @throws RoutineLoaderException
549 1
   */
550
  private function extractParameters(): void
551 1
  {
552
    $this->routineParameters = new RoutineParametersHelper($this->dl,
553 1
                                                           $this->io,
554
                                                           $this->docBlockReflection,
555 1
                                                           $this->routineName);
556
557
    $this->routineParameters->extractRoutineParameters();
558
  }
559
560
  //--------------------------------------------------------------------------------------------------------------------
561
  /**
562
   * Extracts the placeholders from the stored routine source.
563
   *
564 1
   * @throws RoutineLoaderException
565
   */
566
  private function extractPlaceholders(): void
567
  {
568
    $unknown = [];
569
570
    preg_match_all('(@[A-Za-z0-9_.]+(%(type|sort))?@)', $this->routineSourceCode, $matches);
571
    if (!empty($matches[0]))
572
    {
573 1
      foreach ($matches[0] as $placeholder)
574
      {
575 1
        if (isset($this->placeholderPool[strtoupper($placeholder)]))
576
        {
577 1
          $this->placeholders[$placeholder] = $this->placeholderPool[strtoupper($placeholder)];
578
        }
579 1
        else
580 1
        {
581 1
          $unknown[] = $placeholder;
582 1
        }
583
      }
584
    }
585
586 1
    $this->logUnknownPlaceholders($unknown);
587 1
  }
588
589
  //--------------------------------------------------------------------------------------------------------------------
590
  /**
591 1
   * Extracts the return type of the stored routine.
592 1
   *
593
   * @throws RoutineLoaderException
594
   */
595 1
  private function extractReturnType(): void
596
  {
597
    $tags = $this->docBlockReflection->getTags('return');
598
599
    switch ($this->designationType)
600
    {
601
      case 'function':
602
      case 'singleton0':
603
      case 'singleton1':
604
        if (count($tags)===0)
605
        {
606
          throw new RoutineLoaderException('Tag @return not found in DocBlock.');
607
        }
608 1
        $tag = $tags[0];
609
        if ($tag['arguments']['type']==='')
610 1
        {
611 1
          throw new RoutineLoaderException('Invalid return tag. Expected: @return <type>.');
612
        }
613 1
        $this->returnType = $tag['arguments']['type'];
614
        break;
615 1
616
      default:
617
        if (count($tags)!==0)
618
        {
619
          throw new RoutineLoaderException('Redundant @type tag found in DocBlock.');
620
        }
621
    }
622
  }
623
624
  //--------------------------------------------------------------------------------------------------------------------
625
  /**
626
   * Extracts the name of the stored routine and the stored routine type (i.e. procedure or function) source.
627
   *
628 1
   * @throws RoutineLoaderException
629
   */
630 1
  private function extractRoutineTypeAndName(): void
631
  {
632 1
    $n = preg_match('/create\\s+(procedure|function)\\s+([a-zA-Z0-9_]+)/i', $this->routineSourceCode, $matches);
633 1
    if ($n==1)
634
    {
635 1
      if ($this->routineName!=$matches[2])
636
      {
637
        throw new RoutineLoaderException("Stored routine name '%s' does not correspond with filename.", $matches[2]);
638
      }
639
    }
640
    else
641 1
    {
642
      throw new RoutineLoaderException('Unable to find the stored routine name and type.');
643
    }
644
  }
645
646
  //--------------------------------------------------------------------------------------------------------------------
647
  /**
648
   * Detects the syntax of the stored procedure. Either SQL/PSM or PL/SQL.
649
   */
650
  private function extractSyntax(): void
651
  {
652
    if ($this->sqlModeHelper->hasOracleMode())
653
    {
654
      $key1 = $this->findFirstMatchingLine('/^\s*(as|is)\s*$/i');
655
      $key2 = $this->findFirstMatchingLine('/^\s*begin\s*$/i');
656
657
      if ($key1!==null && $key2!==null && $key1<$key2)
658 1
      {
659
        $this->syntax = self::PL_SQL_SYNTAX;
660 1
      }
661
      else
662 1
      {
663
        $this->syntax = self::SQL_PSM_SYNTAX;
664 1
      }
665
    }
666
    else
667
    {
668 1
      $this->syntax = self::SQL_PSM_SYNTAX;
669
    }
670
  }
671
672
  //--------------------------------------------------------------------------------------------------------------------
673
  /**
674
   * Returns the key of the source line that match a regex pattern.
675
   *
676
   * @param string $pattern The regex pattern.
677 1
   *
678
   * @return int|null
679 1
   */
680
  private function findFirstMatchingLine(string $pattern): ?int
681
  {
682
    foreach ($this->routineSourceCodeLines as $key => $line)
683
    {
684
      if (preg_match($pattern, $line)===1)
685 1
      {
686
        return $key;
687
      }
688 1
    }
689 1
690 1
    return null;
691 1
  }
692
  /**
693
   * Loads the stored routine into the database.
694
   *
695
   * @throws MySqlQueryErrorException
696
   */
697
  private function loadRoutineFile(): void
698
  {
699
    if ($this->syntax===self::PL_SQL_SYNTAX)
700
    {
701
      $this->sqlModeHelper->addIfRequiredOracleMode();
702 1
    }
703
    else
704
    {
705 1
      $this->sqlModeHelper->removeIfRequiredOracleMode();
706
    }
707
708
    $routineSource = $this->substitutePlaceHolders();
709
    $this->dropRoutineIfExists();
710
    $this->dl->setCharacterSet($this->characterSet, $this->collate);
711
    $this->dl->loadRoutine($routineSource);
712
  }
713
714
  //--------------------------------------------------------------------------------------------------------------------
715
  /**
716
   * Logs the unknown placeholder (if any).
717
   *
718
   * @param array $unknown The unknown placeholders.
719
   *
720
   * @throws RoutineLoaderException
721
   */
722
  private function logUnknownPlaceholders(array $unknown): void
723
  {
724
    // Return immediately if there are no unknown placeholders.
725
    if (empty($unknown))
726
    {
727
      return;
728
    }
729 1
730
    sort($unknown);
731
    $this->io->text('Unknown placeholder(s):');
732 1
    $this->io->listing($unknown);
733
734
    $replace = [];
735
    foreach ($unknown as $placeholder)
736
    {
737
      $replace[$placeholder] = '<error>'.$placeholder.'</error>';
738
    }
739
    $code = strtr(OutputFormatter::escape($this->routineSourceCode), $replace);
740
741
    $this->io->text(explode(PHP_EOL, $code));
742
743
    throw new RoutineLoaderException('Unknown placeholder(s) found.');
744
  }
745
746
  //--------------------------------------------------------------------------------------------------------------------
747
  /**
748
   * Returns whether the source file must be load or reloaded.
749
   *
750
   * @return bool
751
   */
752
  private function mustLoadStoredRoutine(): bool
753
  {
754
    // If this is the first time we see the source file it must be loaded.
755
    if (empty($this->phpStratumOldMetadata))
756
    {
757
      return true;
758
    }
759
760
    // If the source file has changed the source file must be loaded.
761
    if ($this->phpStratumOldMetadata['timestamp']!==$this->filemtime)
762
    {
763
      return true;
764
    }
765
766
    // If the value of a placeholder has changed the source file must be loaded.
767
    foreach ($this->phpStratumOldMetadata['placeholders'] as $placeHolder => $oldValue)
768 1
    {
769
      if (!isset($this->placeholderPool[strtoupper($placeHolder)]) || $this->placeholderPool[strtoupper($placeHolder)]!==$oldValue)
770 1
      {
771 1
        return true;
772
      }
773 1
    }
774
775
    // If the value of a type has changed the source file must be loaded.
776
    foreach ($this->phpStratumOldMetadata['type_hints'] as $typeHint => $oldValue)
777
    {
778 1
      if (!isset($this->typeHintPool[$typeHint]) || $this->typeHintPool[$typeHint]!==$oldValue)
779 1
      {
780 1
        return true;
781
      }
782 1
    }
783 1
784
    // If stored routine not exists in database the source file must be loaded.
785
    if (empty($this->rdbmsOldRoutineMetadata))
786
    {
787
      return true;
788
    }
789
790 1
    // If current sql-mode is different the source file must reload.
791 1
    if (!$this->sqlModeHelper->compare($this->rdbmsOldRoutineMetadata['sql_mode']))
792 1
    {
793 1
      return true;
794
    }
795 1
796
    // If current character set is different the source file must reload.
797
    if ($this->rdbmsOldRoutineMetadata['character_set_client']!==$this->characterSet)
798
    {
799
      return true;
800
    }
801
802
    // If current collation is different the source file must reload.
803
    if ($this->rdbmsOldRoutineMetadata['collation_connection']!==$this->collate)
804 1
    {
805
      return true;
806 1
    }
807
808 1
    return false;
809 1
  }
810 1
811
  //--------------------------------------------------------------------------------------------------------------------
812 1
  /**
813 1
   * Reads the source code of the stored routine.
814 1
   *
815
   * @throws RoutineLoaderException
816 1
   */
817 1
  private function readSourceCode(): void
818
  {
819 1
    $this->routineSourceCode      = file_get_contents($this->sourceFilename);
820
    $this->routineSourceCodeLines = explode(PHP_EOL, $this->routineSourceCode);
821 1
822 1
    if ($this->routineSourceCodeLines===false)
823 1
    {
824 1
      throw new RoutineLoaderException('Source file is empty.');
825
    }
826 1
827
    $start = $this->findFirstMatchingLine('/^\s*\/\*\*\s*$/');
828
    $end   = $this->findFirstMatchingLine('/^\s*\*\/\s*$/');
829
    if ($start!==null && $end!==null && $start<$end)
830
    {
831
      $lines    = array_slice($this->routineSourceCodeLines, $start, $end - $start + 1);
832
      $docBlock = implode(PHP_EOL, $lines);
833 1
    }
834
    else
835 1
    {
836 1
      $docBlock = '';
837 1
    }
838 1
839 1
    DocBlockReflection::setTagParameters('param', ['name']);
840 1
    DocBlockReflection::setTagParameters('type', ['type', 'ex1', 'ex2']);
841 1
    DocBlockReflection::setTagParameters('return', ['type']);
842
    DocBlockReflection::setTagParameters('paramAddendum', ['name', 'type', 'delimiter', 'enclosure', 'escape']);
843 1
844
    $this->docBlockReflection = new DocBlockReflection($docBlock);
845 1
  }
846
847
  //--------------------------------------------------------------------------------------------------------------------
848 1
  /**
849
   * Returns the source of the routine with all placeholders substituted with their values.
850 1
   *
851 1
   * @return string
852 1
   */
853
  private function substitutePlaceHolders(): string
854
  {
855
    $realpath = realpath($this->sourceFilename);
856
857
    $this->placeholders['__FILE__']    = "'".$this->dl->realEscapeString($realpath)."'";
858
    $this->placeholders['__ROUTINE__'] = "'".$this->routineName."'";
859
    $this->placeholders['__DIR__']     = "'".$this->dl->realEscapeString(dirname($realpath))."'";
860
861
    $lines         = explode(PHP_EOL, $this->routineSourceCode);
862 1
    $routineSource = [];
863
    foreach ($lines as $i => $line)
864
    {
865 1
      $this->placeholders['__LINE__'] = $i + 1;
866
      $routineSource[$i]              = strtr($line, $this->placeholders);
867 1
    }
868 1
    $routineSource = implode(PHP_EOL, $routineSource);
869
870 1
    unset($this->placeholders['__FILE__']);
871
    unset($this->placeholders['__ROUTINE__']);
872
    unset($this->placeholders['__DIR__']);
873
    unset($this->placeholders['__LINE__']);
874
875
    return $routineSource;
876 1
  }
877
878
  //--------------------------------------------------------------------------------------------------------------------
879 1
  /**
880
   * Updates the metadata for the stored routine.
881
   */
882 1
  private function updateMetadata(): void
883 1
  {
884 1
    $this->phpStratumMetadata['routine_name'] = $this->routineName;
885
    $this->phpStratumMetadata['designation']  = $this->designationType;
886
    $this->phpStratumMetadata['return']       = $this->returnType;
887
    $this->phpStratumMetadata['parameters']   = $this->routineParameters->getParameters();
888
    $this->phpStratumMetadata['timestamp']    = $this->filemtime;
889
    $this->phpStratumMetadata['placeholders'] = $this->placeholders;
890
    $this->phpStratumMetadata['type_hints']   = $this->typeHints;
891
    $this->phpStratumMetadata['phpdoc']       = $this->extractDocBlockPartsWrapper();
892
893
    if (in_array($this->designationType, ['rows_with_index', 'rows_with_key']))
894
    {
895
      $this->phpStratumMetadata['index_columns'] = $this->indexColumns;
896
    }
897
898
    if ($this->designationType==='bulk_insert')
899
    {
900
      $this->phpStratumMetadata['bulk_insert_table_name'] = $this->bulkInsertTableName;
901
      $this->phpStratumMetadata['bulk_insert_columns']    = $this->bulkInsertColumns;
902
      $this->phpStratumMetadata['bulk_insert_keys']       = $this->bulkInsertKeys;
903
    }
904
  }
905
906
  //--------------------------------------------------------------------------------------------------------------------
907
  /**
908
   * Updates the source of the stored routine based on the values of type hints.
909
   *
910
   * @throws RoutineLoaderException
911
   */
912
  private function updateSourceTypeHints(): void
913
  {
914
    $types   = ['int',
915
                'smallint',
916
                'tinyint',
917
                'mediumint',
918
                'bigint',
919
                'decimal',
920
                'float',
921
                'double',
922
                'bit',
923
                'date',
924
                'datetime',
925
                'timestamp',
926
                'time',
927
                'year',
928
                'char',
929
                'varchar',
930
                'binary',
931
                'varbinary',
932
                'enum',
933
                'set',
934
                'inet4',
935
                'inet6',
936
                'tinyblob',
937
                'blob',
938
                'mediumblob',
939
                'longblob',
940
                'tinytext',
941
                'text',
942
                'mediumtext',
943
                'longtext'];
944
    $parts   = ['whitespace'  => '(?<whitespace>\s+)',
945
                'type_list'   => str_replace('type-list',
946
                                             implode('|', $types),
947
                                             '(?<datatype>(type-list).*)'),
948
                'nullable'    => '(?<nullable>not\s+null)?',
949
                'punctuation' => '(?<punctuation>\s*[,;])?',
950
                'hint'        => '(?<hint>\s+--\s+type:\s+(\w+\.)?\w+\.\w+\s*)'];
951
    $pattern = '/'.implode('', $parts).'$/i';
952
953
    foreach ($this->routineSourceCodeLines as $index => $line)
954
    {
955
      if (preg_match('/'.$parts['hint'].'/i', $line))
956
      {
957
        $n = preg_match($pattern, $line, $matches);
958
        if ($n===0)
959
        {
960
          throw new RoutineLoaderException("Found a type hint at line %d, but unable to find data type.", $index + 1);
961
        }
962
963
        $hint = trim(preg_replace('/\s+--\s+type:\s+/i', '', $matches['hint']));
964
        if (!isset($this->typeHintPool[$hint]))
965
        {
966
          throw new RoutineLoaderException("Unknown type hint '%s' found at line %d.", $hint, $index + 1);
967
        }
968
969
        $actualType                           = $this->typeHintPool[$hint];
970
        $this->routineSourceCodeLines[$index] = sprintf('%s%s%s%s%s%s',
971
                                                        mb_substr($line, 0, -mb_strlen($matches[0])),
972
                                                        $matches['whitespace'],
973
                                                        $actualType, // <== the real replacement
974
                                                        $matches['nullable'],
975
                                                        $matches['punctuation'],
976
                                                        $matches['hint']);
977
        $this->typeHints[$hint]               = $actualType;
978
      }
979
    }
980
981
    $routineSourceCode = implode(PHP_EOL, $this->routineSourceCodeLines);
982
    if ($this->routineSourceCode!==$routineSourceCode)
983
    {
984
      $this->routineSourceCode = $routineSourceCode;
985
      Util::writeTwoPhases($this->sourceFilename, $this->routineSourceCode, $this->io);
986
    }
987
  }
988
989
  //--------------------------------------------------------------------------------------------------------------------
990
  /**
991
   * Validates the specified return type of the stored routine.
992
   *
993
   * @throws RoutineLoaderException
994
   */
995
  private function validateReturnType(): void
996
  {
997
    // Return immediately if designation type is not appropriate for this method.
998
    if (!in_array($this->designationType, ['function', 'singleton0', 'singleton1']))
999
    {
1000
      return;
1001
    }
1002
1003
    $types = explode('|', $this->returnType);
0 ignored issues
show
Bug introduced by
It seems like $this->returnType can also be of type null; however, parameter $string of explode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

1003
    $types = explode('|', /** @scrutinizer ignore-type */ $this->returnType);
Loading history...
1004
    $diff  = array_diff($types, ['string', 'int', 'float', 'double', 'bool', 'null']);
1005
1006
    if (!($this->returnType=='mixed' || $this->returnType=='bool' || empty($diff)))
1007
    {
1008
      throw new RoutineLoaderException("Return type must be 'mixed', 'bool', or a combination of 'int', 'float', 'string', and 'null'.");
1009
    }
1010
1011
    // The following tests are applicable for singleton0 routines only.
1012
    if ($this->designationType!=='singleton0')
1013
    {
1014
      return;
1015
    }
1016
1017
    // Return mixed is OK.
1018
    if (in_array($this->returnType, ['bool', 'mixed']))
1019
    {
1020
      return;
1021
    }
1022
1023
    // In all other cases return type must contain null.
1024
    $parts = explode('|', $this->returnType);
1025
    $key   = in_array('null', $parts);
1026
    if ($key===false)
1027
    {
1028
      throw new RoutineLoaderException("Return type must be 'mixed', 'bool', or contain 'null' (with a combination of 'int', 'float', and 'string').");
1029
    }
1030
  }
1031
1032
  //--------------------------------------------------------------------------------------------------------------------
1033
}
1034
1035
//----------------------------------------------------------------------------------------------------------------------
1036