Passed
Push — master ( caf7cf...f71b04 )
by P.R.
02:07
created

RoutineLoaderHelper::updateSourceTypeHints()   D

Complexity

Conditions 14
Paths 182

Size

Total Lines 124
Code Lines 84

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 210

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 14
eloc 84
c 1
b 0
f 0
nc 182
nop 0
dl 0
loc 124
ccs 0
cts 0
cp 0
crap 210
rs 4.8157

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 = '4';
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
    $isTemporaryTable = !$this->dl->checkTableExists($this->bulkInsertTableName);
454
455
    // Create temporary table if table is a temporary table.
456 1
    if ($isTemporaryTable)
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 temporary.
465 1
    if ($isTemporaryTable)
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
  //--------------------------------------------------------------------------------------------------------------------
694
  /**
695
   * Loads the stored routine into the database.
696
   *
697
   * @throws MySqlQueryErrorException
698
   */
699
  private function loadRoutineFile(): void
700
  {
701
    if ($this->syntax===self::PL_SQL_SYNTAX)
702 1
    {
703
      $this->sqlModeHelper->addIfRequiredOracleMode();
704
    }
705 1
    else
706
    {
707
      $this->sqlModeHelper->removeIfRequiredOracleMode();
708
    }
709
710
    $routineSource = $this->substitutePlaceHolders();
711
    $this->dropRoutineIfExists();
712
    $this->dl->setCharacterSet($this->characterSet, $this->collate);
713
    $this->dl->loadRoutine($routineSource);
714
  }
715
716
  //--------------------------------------------------------------------------------------------------------------------
717
  /**
718
   * Logs the unknown placeholder (if any).
719
   *
720
   * @param array $unknown The unknown placeholders.
721
   *
722
   * @throws RoutineLoaderException
723
   */
724
  private function logUnknownPlaceholders(array $unknown): void
725
  {
726
    // Return immediately if there are no unknown placeholders.
727
    if (empty($unknown))
728
    {
729 1
      return;
730
    }
731
732 1
    sort($unknown);
733
    $this->io->text('Unknown placeholder(s):');
734
    $this->io->listing($unknown);
735
736
    $replace = [];
737
    foreach ($unknown as $placeholder)
738
    {
739
      $replace[$placeholder] = '<error>'.$placeholder.'</error>';
740
    }
741
    $code = strtr(OutputFormatter::escape($this->routineSourceCode), $replace);
742
743
    $this->io->text(explode(PHP_EOL, $code));
744
745
    throw new RoutineLoaderException('Unknown placeholder(s) found.');
746
  }
747
748
  //--------------------------------------------------------------------------------------------------------------------
749
  /**
750
   * Returns whether the source file must be load or reloaded.
751
   *
752
   * @return bool
753
   */
754
  private function mustLoadStoredRoutine(): bool
755
  {
756
    // If this is the first time we see the source file it must be loaded.
757
    if (empty($this->phpStratumOldMetadata))
758
    {
759
      return true;
760
    }
761
762
    // If the source file has changed the source file must be loaded.
763
    if ($this->phpStratumOldMetadata['timestamp']!==$this->filemtime)
764
    {
765
      return true;
766
    }
767
768 1
    // If the value of a placeholder has changed the source file must be loaded.
769
    foreach ($this->phpStratumOldMetadata['placeholders'] as $placeHolder => $oldValue)
770 1
    {
771 1
      if (!isset($this->placeholderPool[strtoupper($placeHolder)]) || $this->placeholderPool[strtoupper($placeHolder)]!==$oldValue)
772
      {
773 1
        return true;
774
      }
775
    }
776
777
    // If the value of a type has changed the source file must be loaded.
778 1
    foreach ($this->phpStratumOldMetadata['type_hints'] as $typeHint => $oldValue)
779 1
    {
780 1
      if (!isset($this->typeHintPool[$typeHint]) || $this->typeHintPool[$typeHint]!==$oldValue)
781
      {
782 1
        return true;
783 1
      }
784
    }
785
786
    // If stored routine not exists in database the source file must be loaded.
787
    if (empty($this->rdbmsOldRoutineMetadata))
788
    {
789
      return true;
790 1
    }
791 1
792 1
    // If current sql-mode is different the source file must reload.
793 1
    if (!$this->sqlModeHelper->compare($this->rdbmsOldRoutineMetadata['sql_mode']))
794
    {
795 1
      return true;
796
    }
797
798
    // If current character set is different the source file must reload.
799
    if ($this->rdbmsOldRoutineMetadata['character_set_client']!==$this->characterSet)
800
    {
801
      return true;
802
    }
803
804 1
    // If current collation is different the source file must reload.
805
    if ($this->rdbmsOldRoutineMetadata['collation_connection']!==$this->collate)
806 1
    {
807
      return true;
808 1
    }
809 1
810 1
    return false;
811
  }
812 1
813 1
  //--------------------------------------------------------------------------------------------------------------------
814 1
  /**
815
   * Reads the source code of the stored routine.
816 1
   *
817 1
   * @throws RoutineLoaderException
818
   */
819 1
  private function readSourceCode(): void
820
  {
821 1
    $this->routineSourceCode      = file_get_contents($this->sourceFilename);
822 1
    $this->routineSourceCodeLines = explode(PHP_EOL, $this->routineSourceCode);
823 1
824 1
    if ($this->routineSourceCodeLines===false)
825
    {
826 1
      throw new RoutineLoaderException('Source file is empty.');
827
    }
828
829
    $start = $this->findFirstMatchingLine('/^\s*\/\*\*\s*$/');
830
    $end   = $this->findFirstMatchingLine('/^\s*\*\/\s*$/');
831
    if ($start!==null && $end!==null && $start<$end)
832
    {
833 1
      $lines    = array_slice($this->routineSourceCodeLines, $start, $end - $start + 1);
834
      $docBlock = implode(PHP_EOL, $lines);
835 1
    }
836 1
    else
837 1
    {
838 1
      $docBlock = '';
839 1
    }
840 1
841 1
    DocBlockReflection::setTagParameters('param', ['name']);
842
    DocBlockReflection::setTagParameters('type', ['type', 'ex1', 'ex2']);
843 1
    DocBlockReflection::setTagParameters('return', ['type']);
844
    DocBlockReflection::setTagParameters('paramAddendum', ['name', 'type', 'delimiter', 'enclosure', 'escape']);
845 1
846
    $this->docBlockReflection = new DocBlockReflection($docBlock);
847
  }
848 1
849
  //--------------------------------------------------------------------------------------------------------------------
850 1
  /**
851 1
   * Returns the source of the routine with all placeholders substituted with their values.
852 1
   *
853
   * @return string
854
   */
855
  private function substitutePlaceHolders(): string
856
  {
857
    $realpath = realpath($this->sourceFilename);
858
859
    $this->placeholders['__FILE__']    = "'".$this->dl->realEscapeString($realpath)."'";
860
    $this->placeholders['__ROUTINE__'] = "'".$this->routineName."'";
861
    $this->placeholders['__DIR__']     = "'".$this->dl->realEscapeString(dirname($realpath))."'";
862 1
863
    $lines         = explode(PHP_EOL, $this->routineSourceCode);
864
    $routineSource = [];
865 1
    foreach ($lines as $i => $line)
866
    {
867 1
      $this->placeholders['__LINE__'] = $i + 1;
868 1
      $routineSource[$i]              = strtr($line, $this->placeholders);
869
    }
870 1
    $routineSource = implode(PHP_EOL, $routineSource);
871
872
    unset($this->placeholders['__FILE__']);
873
    unset($this->placeholders['__ROUTINE__']);
874
    unset($this->placeholders['__DIR__']);
875
    unset($this->placeholders['__LINE__']);
876 1
877
    return $routineSource;
878
  }
879 1
880
  //--------------------------------------------------------------------------------------------------------------------
881
  /**
882 1
   * Updates the metadata for the stored routine.
883 1
   */
884 1
  private function updateMetadata(): void
885
  {
886
    $this->phpStratumMetadata['routine_name'] = $this->routineName;
887
    $this->phpStratumMetadata['designation']  = $this->designationType;
888
    $this->phpStratumMetadata['return']       = $this->returnType;
889
    $this->phpStratumMetadata['parameters']   = $this->routineParameters->getParameters();
890
    $this->phpStratumMetadata['timestamp']    = $this->filemtime;
891
    $this->phpStratumMetadata['placeholders'] = $this->placeholders;
892
    $this->phpStratumMetadata['type_hints']   = $this->typeHints;
893
    $this->phpStratumMetadata['phpdoc']       = $this->extractDocBlockPartsWrapper();
894
895
    if (in_array($this->designationType, ['rows_with_index', 'rows_with_key']))
896
    {
897
      $this->phpStratumMetadata['index_columns'] = $this->indexColumns;
898
    }
899
900
    if ($this->designationType==='bulk_insert')
901
    {
902
      $this->phpStratumMetadata['bulk_insert_table_name'] = $this->bulkInsertTableName;
903
      $this->phpStratumMetadata['bulk_insert_columns']    = $this->bulkInsertColumns;
904
      $this->phpStratumMetadata['bulk_insert_keys']       = $this->bulkInsertKeys;
905
    }
906
  }
907
908
  //--------------------------------------------------------------------------------------------------------------------
909
  /**
910
   * Updates the source of the stored routine based on the values of type hints.
911
   *
912
   * @throws RoutineLoaderException
913
   */
914
  private function updateSourceTypeHints(): void
915
  {
916
    $types   = ['int',
917
                'smallint',
918
                'tinyint',
919
                'mediumint',
920
                'bigint',
921
                'decimal',
922
                'float',
923
                'double',
924
                'bit',
925
                'date',
926
                'datetime',
927
                'timestamp',
928
                'time',
929
                'year',
930
                'char',
931
                'varchar',
932
                'binary',
933
                'varbinary',
934
                'enum',
935
                'set',
936
                'inet4',
937
                'inet6',
938
                'tinyblob',
939
                'blob',
940
                'mediumblob',
941
                'longblob',
942
                'tinytext',
943
                'text',
944
                'mediumtext',
945
                'longtext'];
946
    $parts   = ['whitespace' => '(?<whitespace>\s+)',
947
                'type_list'  => str_replace('type-list',
948
                                            implode('|', $types),
949
                                            '(?<datatype>(type-list).*)'),
950
                'nullable'   => '(?<nullable>not\s+null)?',
951
                'hint'       => '(?<hint>\s+--\s+type:\s+(\w+\.)?\w+\.\w+\s*)'];
952
    $pattern = '/'.implode('', $parts).'$/i';
953
954
    foreach ($this->routineSourceCodeLines as $index => $line)
955
    {
956
      if (preg_match('/'.$parts['hint'].'/i', $line))
957
      {
958
        $n = preg_match($pattern, $line, $matches);
959
        if ($n===0)
960
        {
961
          throw new RoutineLoaderException("Found a type hint at line %d, but unable to find data type.", $index + 1);
962
        }
963
964
        $hint = trim(preg_replace('/\s+--\s+type:\s+/i', '', $matches['hint']));
965
        if (!isset($this->typeHintPool[$hint]))
966
        {
967
          throw new RoutineLoaderException("Unknown type hint '%s' found at line %d.", $hint, $index + 1);
968
        }
969
970
        if (preg_match('/(?<punctuation>\s*[;,]\s*)$/', $matches['datatype'], $other))
971
        {
972
          $punctuation = $other['punctuation'];
973
        }
974
        else
975
        {
976
          $punctuation = '';
977
        }
978
979
        $actualType             = $this->typeHintPool[$hint];
980
        $newLine                = sprintf('%s%s%s%s%s%s',
981
                                          mb_substr($line, 0, -mb_strlen($matches[0])),
982
                                          $matches['whitespace'],
983
                                          $actualType, // <== the real replacement
984
                                          $matches['nullable'],
985
                                          $punctuation,
986
                                          $matches['hint']);
987
        $this->typeHints[$hint] = $actualType;
988
989
        if (str_replace(' ', '', $line)!==str_replace(' ', '', $newLine))
990
        {
991
          $this->routineSourceCodeLines[$index] = $newLine;
992
        }
993
      }
994
    }
995
996
    $blocks = [];
997
    $start  = null;
998
    $length = 0;
999
    foreach ($this->routineSourceCodeLines as $index => $line)
1000
    {
1001
      $n = preg_match('/--\s+type:\s+.*$/', $line, $matches);
1002
      if ($n!==0)
1003
      {
1004
        if ($start===null)
1005
        {
1006
          $start = $index;
1007
        }
1008
        $length = max($length, mb_strlen($line) - mb_strlen($matches[0]) + 2);
1009
      }
1010
      else
1011
      {
1012
        if ($start!==null)
1013
        {
1014
          $blocks[] = ['first' => $start, 'last' => $index, 'length' => $length];
1015
          $start    = null;
1016
          $length   = 0;
1017
        }
1018
      }
1019
    }
1020
1021
    foreach ($blocks as $block)
1022
    {
1023
      for ($index = $block['first']; $index<$block['last']; $index++)
1024
      {
1025
        preg_match('/\s+type:\s+.*$/', $this->routineSourceCodeLines[$index], $matches);
1026
        $leftPart = mb_substr($this->routineSourceCodeLines[$index], 0, -mb_strlen($matches[0]));
1027
        $leftPart = $leftPart.str_repeat(' ', $block['length'] - mb_strlen($leftPart) + 1);
1028
1029
        $this->routineSourceCodeLines[$index] = $leftPart.ltrim($matches[0]);
1030
      }
1031
    }
1032
1033
    $routineSourceCode = implode(PHP_EOL, $this->routineSourceCodeLines);
1034
    if ($this->routineSourceCode!==$routineSourceCode)
1035
    {
1036
      $this->routineSourceCode = $routineSourceCode;
1037
      Util::writeTwoPhases($this->sourceFilename, $this->routineSourceCode, $this->io);
1038
    }
1039
  }
1040
1041
  //--------------------------------------------------------------------------------------------------------------------
1042
  /**
1043
   * Validates the specified return type of the stored routine.
1044
   *
1045
   * @throws RoutineLoaderException
1046
   */
1047
  private function validateReturnType(): void
1048
  {
1049
    // Return immediately if designation type is not appropriate for this method.
1050
    if (!in_array($this->designationType, ['function', 'singleton0', 'singleton1']))
1051
    {
1052
      return;
1053
    }
1054
1055
    $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

1055
    $types = explode('|', /** @scrutinizer ignore-type */ $this->returnType);
Loading history...
1056
    $diff  = array_diff($types, ['string', 'int', 'float', 'double', 'bool', 'null']);
1057
1058
    if (!($this->returnType=='mixed' || $this->returnType=='bool' || empty($diff)))
1059
    {
1060
      throw new RoutineLoaderException("Return type must be 'mixed', 'bool', or a combination of 'int', 'float', 'string', and 'null'.");
1061
    }
1062
1063
    // The following tests are applicable for singleton0 routines only.
1064
    if ($this->designationType!=='singleton0')
1065
    {
1066
      return;
1067
    }
1068
1069
    // Return mixed is OK.
1070
    if (in_array($this->returnType, ['bool', 'mixed']))
1071
    {
1072
      return;
1073
    }
1074
1075
    // In all other cases return type must contain null.
1076
    $parts = explode('|', $this->returnType);
1077
    $key   = in_array('null', $parts);
1078
    if ($key===false)
1079
    {
1080
      throw new RoutineLoaderException("Return type must be 'mixed', 'bool', or contain 'null' (with a combination of 'int', 'float', and 'string').");
1081
    }
1082
  }
1083
1084
  //--------------------------------------------------------------------------------------------------------------------
1085
}
1086
1087
//----------------------------------------------------------------------------------------------------------------------
1088