Passed
Push — master ( 304ce4...56185b )
by P.R.
04:05
created

extractColumnsFromTableDescription()   D

Complexity

Conditions 30
Paths 30

Size

Total Lines 107
Code Lines 77

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 65
CRAP Score 32.1317

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 30
eloc 77
c 1
b 0
f 0
nc 30
nop 1
dl 0
loc 107
ccs 65
cts 75
cp 0.8667
crap 32.1317
rs 4.1666

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

515
            'parameters'        => $this->routineParameters->/** @scrutinizer ignore-call */ extractDocBlockPartsWrapper()];

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...
516
  }
517
518
  //--------------------------------------------------------------------------------------------------------------------
519
  /**
520
   * Extracts routine parameters.
521
   *
522
   * @throws MySqlQueryErrorException
523
   * @throws RoutineLoaderException
524
   */
525 1
  private function extractParameters()
526
  {
527 1
    $this->routineParameters = new RoutineParametersHelper($this->dl,
528 1
                                                           $this->io,
529 1
                                                           $this->docBlockReflection,
1 ignored issue
show
Bug introduced by
It seems like $this->docBlockReflection can also be of type null; however, parameter $docBlockReflection of SetBased\Stratum\MySql\H...rsHelper::__construct() does only seem to accept SetBased\Stratum\Common\...lock\DocBlockReflection, 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

529
                                                           /** @scrutinizer ignore-type */ $this->docBlockReflection,
Loading history...
530 1
                                                           $this->routineName);
531
532 1
    $this->routineParameters->extractRoutineParameters();
533 1
  }
534
535
  //--------------------------------------------------------------------------------------------------------------------
536
  /**
537
   * Extracts the placeholders from the stored routine source.
538
   *
539
   * @throws RoutineLoaderException
540
   */
541 1
  private function extractPlaceholders(): void
542
  {
543 1
    $unknown = [];
544
545 1
    preg_match_all('(@[A-Za-z0-9_.]+(%(type|sort))?@)', $this->routineSourceCode, $matches);
546 1
    if (!empty($matches[0]))
547
    {
548 1
      foreach ($matches[0] as $placeholder)
549
      {
550 1
        if (isset($this->replacePairs[strtoupper($placeholder)]))
551
        {
552 1
          $this->replace[$placeholder] = $this->replacePairs[strtoupper($placeholder)];
553
        }
554
        else
555
        {
556
          $unknown[] = $placeholder;
557
        }
558
      }
559
    }
560
561 1
    $this->logUnknownPlaceholders($unknown);
562 1
  }
563
564
  //--------------------------------------------------------------------------------------------------------------------
565
  /**
566
   * Extracts the return type of the stored routine.
567
   *
568
   * @throws RoutineLoaderException
569
   */
570 1
  private function extractReturnType(): void
571
  {
572 1
    $tags = $this->docBlockReflection->getTags('return');
573
574 1
    switch ($this->designationType)
575
    {
576 1
      case 'function':
577 1
      case 'singleton0':
578 1
      case 'singleton1':
579 1
        if (count($tags)===0)
580
        {
581
          throw new RoutineLoaderException('Tag @return not found in DocBlock.');
582
        }
583 1
        $tag = $tags[0];
584 1
        if ($tag['arguments'][0]==='')
585
        {
586
          throw new RoutineLoaderException('Invalid return tag. Expected: @return <type>.');
587
        }
588 1
        $this->returnType = $tag['arguments'][0];
589 1
        break;
590
591
      default:
592 1
        if (count($tags)!==0)
593
        {
594
          throw new RoutineLoaderException('Redundant @type tag found in DocBlock.');
595
        }
596
    }
597 1
  }
598
599
  //--------------------------------------------------------------------------------------------------------------------
600
  /**
601
   * Extracts the name of the stored routine and the stored routine type (i.e. procedure or function) source.
602
   *
603
   * @throws RoutineLoaderException
604
   */
605 1
  private function extractRoutineTypeAndName(): void
606
  {
607 1
    $n = preg_match('/create\\s+(procedure|function)\\s+([a-zA-Z0-9_]+)/i', $this->routineSourceCode, $matches);
608 1
    if ($n==1)
609
    {
610 1
      if ($this->routineName!=$matches[2])
611
      {
612 1
        throw new RoutineLoaderException("Stored routine name '%s' does not corresponds with filename", $matches[2]);
613
      }
614
    }
615
    else
616
    {
617
      throw new RoutineLoaderException('Unable to find the stored routine name and type');
618
    }
619 1
  }
620
621
  //--------------------------------------------------------------------------------------------------------------------
622
  /**
623
   * Detects the syntax of the stored procedure. Either SQL/PSM or PL/SQL.
624
   *
625
   * @throws RoutineLoaderException
626
   */
627 1
  private function extractSyntax(): void
628
  {
629 1
    if ($this->sqlModeHelper->hasOracleMode())
630
    {
631 1
      if ($this->findFirstMatchingLine('/^\s*(modifies|reads)\s+sql\s+data\s*$/i')!==null)
632
      {
633 1
        $this->syntax = self::SQL_PSM_SYNTAX;
634
      }
635
      else
636
      {
637
        $key1 = $this->findFirstMatchingLine('/^\s*(as|is)\s*$/i');
638
        $key2 = $this->findFirstMatchingLine('/^\s*begin\s*$/i');
639
640
        if ($key1!==null && $key2!==null && $key1<$key2)
641
        {
642
          $this->syntax = self::PL_SQL_SYNTAX;
643
        }
644
        elseif ($key1===null && $key2!==null)
645
        {
646
          $this->syntax = self::SQL_PSM_SYNTAX;
647
        }
648
        else
649
        {
650 1
          throw new RoutineLoaderException('Unable to derive syntax (SQL/PSM or PL/SQL) from stored routine.');
651
        }
652
      }
653
    }
654
    else
655
    {
656
      $this->syntax = self::SQL_PSM_SYNTAX;
657
    }
658 1
  }
659
660
  //--------------------------------------------------------------------------------------------------------------------
661
  /**
662
   * Returns the key of the source line that match a regex pattern.
663
   *
664
   * @param string $pattern The regex pattern.
665
   *
666
   * @return int|null
667
   */
668 1
  private function findFirstMatchingLine(string $pattern): ?int
669
  {
670 1
    foreach ($this->routineSourceCodeLines as $key => $line)
671
    {
672 1
      if (preg_match($pattern, $line)===1)
673
      {
674 1
        return $key;
675
      }
676
    }
677
678
    return null;
679
  }
680
681
  //--------------------------------------------------------------------------------------------------------------------
682
  /**
683
   * Loads the stored routine into the database.
684
   *
685
   * @throws MySqlQueryErrorException
686
   */
687 1
  private function loadRoutineFile(): void
688
  {
689 1
    if ($this->syntax===self::PL_SQL_SYNTAX)
690
    {
691
      $this->sqlModeHelper->addIfRequiredOracleMode();
692
    }
693
    else
694
    {
695 1
      $this->sqlModeHelper->removeIfRequiredOracleMode();
696
    }
697
698 1
    $routineSource = $this->substitutePlaceHolders();
699 1
    $this->dropRoutineIfExists();
700 1
    $this->dl->setCharacterSet($this->characterSet, $this->collate);
701 1
    $this->dl->loadRoutine($routineSource);
702 1
  }
703
704
  //--------------------------------------------------------------------------------------------------------------------
705
  /**
706
   * Logs the unknown placeholder (if any).
707
   *
708
   * @param array $unknown The unknown placeholders.
709
   *
710
   * @throws RoutineLoaderException
711
   */
712 1
  private function logUnknownPlaceholders(array $unknown): void
713
  {
714
    // Return immediately if there are no unknown placeholders.
715 1
    if (empty($unknown)) return;
716
717
    sort($unknown);
718
    $this->io->text('Unknown placeholder(s):');
719
    $this->io->listing($unknown);
720
721
    $replace = [];
722
    foreach ($unknown as $placeholder)
723
    {
724
      $replace[$placeholder] = '<error>'.$placeholder.'</error>';
725
    }
726
    $code = strtr(OutputFormatter::escape($this->routineSourceCode), $replace);
727
728
    $this->io->text(explode(PHP_EOL, $code));
729
730
    throw new RoutineLoaderException('Unknown placeholder(s) found');
731
  }
732
733
  //--------------------------------------------------------------------------------------------------------------------
734
  /**
735
   * Returns true if the source file must be load or reloaded. Otherwise returns false.
736
   *
737
   * @return bool
738
   */
739 1
  private function mustLoadStoredRoutine(): bool
740
  {
741
    // If this is the first time we see the source file it must be loaded.
742 1
    if (empty($this->phpStratumOldMetadata)) return true;
743
744
    // If the source file has changed the source file must be loaded.
745
    if ($this->phpStratumOldMetadata['timestamp']!==$this->filemtime) return true;
746
747
    // If the value of a placeholder has changed the source file must be loaded.
748
    foreach ($this->phpStratumOldMetadata['replace'] as $placeHolder => $oldValue)
749
    {
750
      if (!isset($this->replacePairs[strtoupper($placeHolder)]) ||
751
        $this->replacePairs[strtoupper($placeHolder)]!==$oldValue)
752
      {
753
        return true;
754
      }
755
    }
756
757
    // If stored routine not exists in database the source file must be loaded.
758
    if (empty($this->rdbmsOldRoutineMetadata)) return true;
759
760
    // If current sql-mode is different the source file must reload.
761
    if (!$this->sqlModeHelper->compare($this->rdbmsOldRoutineMetadata['sql_mode'])) return true;
762
763
    // If current character set is different the source file must reload.
764
    if ($this->rdbmsOldRoutineMetadata['character_set_client']!==$this->characterSet) return true;
765
766
    // If current collation is different the source file must reload.
767
    if ($this->rdbmsOldRoutineMetadata['collation_connection']!==$this->collate) return true;
768
769
    return false;
770
  }
771
772
  //--------------------------------------------------------------------------------------------------------------------
773
  /**
774
   * Reads the source code of the stored routine.
775
   *
776
   * @throws RoutineLoaderException
777
   */
778 1
  private function readSourceCode(): void
779
  {
780 1
    $this->routineSourceCode      = file_get_contents($this->sourceFilename);
781 1
    $this->routineSourceCodeLines = explode(PHP_EOL, $this->routineSourceCode);
782
783 1
    if ($this->routineSourceCodeLines===false)
784
    {
785
      throw new RoutineLoaderException('Source file is empty');
786
    }
787
788 1
    $start = $this->findFirstMatchingLine('/^\s*\/\*\*\s*$/');
789 1
    $end   = $this->findFirstMatchingLine('/^\s*\*\/\s*$/');
790 1
    if ($start!==null && $end!==null && $start<$end)
791
    {
792 1
      $lines    = array_slice($this->routineSourceCodeLines, $start, $end - $start + 1);
793 1
      $docBlock = implode(PHP_EOL, (array)$lines);
794
    }
795
    else
796
    {
797
      $docBlock = '';
798
    }
799
800 1
    DocBlockReflection::setTagParameters('param', 1);
801 1
    DocBlockReflection::setTagParameters('type', 3);
802 1
    DocBlockReflection::setTagParameters('return', 1);
803 1
    DocBlockReflection::setTagParameters('paramAddendum', 5);
804
805 1
    $this->docBlockReflection = new DocBlockReflection($docBlock);
806 1
  }
807
808
  //--------------------------------------------------------------------------------------------------------------------
809
  /**
810
   * Returns the source of the routine with all placeholders substituted with their values.
811
   *
812
   * @return string
813
   */
814 1
  private function substitutePlaceHolders(): string
815
  {
816 1
    $realpath = realpath($this->sourceFilename);
817
818 1
    $this->replace['__FILE__']    = "'".$this->dl->realEscapeString($realpath)."'";
819 1
    $this->replace['__ROUTINE__'] = "'".$this->routineName."'";
820 1
    $this->replace['__DIR__']     = "'".$this->dl->realEscapeString(dirname($realpath))."'";
821
822 1
    $lines         = explode(PHP_EOL, $this->routineSourceCode);
823 1
    $routineSource = [];
824 1
    foreach ($lines as $i => $line)
825
    {
826 1
      $this->replace['__LINE__'] = $i + 1;
827 1
      $routineSource[$i]         = strtr($line, $this->replace);
828
    }
829 1
    $routineSource = implode(PHP_EOL, $routineSource);
830
831 1
    unset($this->replace['__FILE__']);
832 1
    unset($this->replace['__ROUTINE__']);
833 1
    unset($this->replace['__DIR__']);
834 1
    unset($this->replace['__LINE__']);
835
836 1
    return $routineSource;
837
  }
838
839
  //--------------------------------------------------------------------------------------------------------------------
840
  /**
841
   * Updates the metadata for the stored routine.
842
   */
843 1
  private function updateMetadata(): void
844
  {
845 1
    $this->phpStratumMetadata['routine_name']           = $this->routineName;
846 1
    $this->phpStratumMetadata['designation']            = $this->designationType;
847 1
    $this->phpStratumMetadata['return']                 = $this->returnType;
848 1
    $this->phpStratumMetadata['parameters']             = $this->routineParameters->getParameters();
849 1
    $this->phpStratumMetadata['timestamp']              = $this->filemtime;
850 1
    $this->phpStratumMetadata['replace']                = $this->replace;
851 1
    $this->phpStratumMetadata['phpdoc']                 = $this->extractDocBlockPartsWrapper();
852
853 1
    if (in_array($this->designationType, ['rows_with_index', 'rows_with_key']))
854
    {
855 1
      $this->phpStratumMetadata['index_columns'] = $this->indexColumns;
856
    }
857
858 1
    if ($this->designationType==='bulk_insert')
859
    {
860 1
      $this->phpStratumMetadata['bulk_insert_table_name'] = $this->bulkInsertTableName;
861 1
      $this->phpStratumMetadata['bulk_insert_columns']    = $this->bulkInsertColumns;
862 1
      $this->phpStratumMetadata['bulk_insert_keys']       = $this->bulkInsertKeys;
863
    }
864 1
  }
865
866
  //--------------------------------------------------------------------------------------------------------------------
867
  /**
868
   * Validates the specified return type of the stored routine.
869
   *
870
   * @throws RoutineLoaderException
871
   */
872 1
  private function validateReturnType(): void
873
  {
874
    // Return immediately if designation type is not appropriate for this method.
875 1
    if (!in_array($this->designationType, ['function', 'singleton0', 'singleton1'])) return;
876
877 1
    $types = explode('|', $this->returnType);
878 1
    $diff  = array_diff($types, ['string', 'int', 'float', 'double', 'bool', 'null']);
879
880 1
    if (!($this->returnType=='mixed' || $this->returnType=='bool' || empty($diff)))
881
    {
882
      throw new RoutineLoaderException("Return type must be 'mixed', 'bool', or a combination of 'int', 'float', 'string', and 'null'");
883
    }
884
885
    // The following tests are applicable for singleton0 routines only.
886 1
    if (!in_array($this->designationType, ['singleton0'])) return;
887
888
    // Return mixed is OK.
889 1
    if (in_array($this->returnType, ['bool', 'mixed'])) return;
890
891
    // In all other cases return type must contain null.
892 1
    $parts = explode('|', $this->returnType);
893 1
    $key   = array_search('null', $parts);
894 1
    if ($key===false)
895
    {
896
      throw new RoutineLoaderException("Return type must be 'mixed', 'bool', or contain 'null' (with a combination of 'int', 'float', and 'string')");
897
    }
898 1
  }
899
900
  //--------------------------------------------------------------------------------------------------------------------
901
}
902
903
//----------------------------------------------------------------------------------------------------------------------
904