Completed
Pull Request — master (#117)
by
unknown
19:44
created

AbstractSubject::stripExceptionSuffix()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
3
/**
4
 * TechDivision\Import\Subjects\AbstractSubject
5
 *
6
 * NOTICE OF LICENSE
7
 *
8
 * This source file is subject to the Open Software License (OSL 3.0)
9
 * that is available through the world-wide-web at this URL:
10
 * http://opensource.org/licenses/osl-3.0.php
11
 *
12
 * PHP version 5
13
 *
14
 * @author    Tim Wagner <[email protected]>
15
 * @copyright 2016 TechDivision GmbH <[email protected]>
16
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
17
 * @link      https://github.com/techdivision/import
18
 * @link      http://www.techdivision.com
19
 */
20
21
namespace TechDivision\Import\Subjects;
22
23
use Doctrine\Common\Collections\Collection;
24
use TechDivision\Import\RowTrait;
25
use TechDivision\Import\HeaderTrait;
26
use TechDivision\Import\SystemLoggerTrait;
27
use TechDivision\Import\Utils\ScopeKeys;
28
use TechDivision\Import\Utils\ColumnKeys;
29
use TechDivision\Import\Utils\MemberNames;
30
use TechDivision\Import\Utils\RegistryKeys;
31
use TechDivision\Import\Utils\Generators\GeneratorInterface;
32
use TechDivision\Import\Callbacks\CallbackInterface;
33
use TechDivision\Import\Observers\ObserverInterface;
34
use TechDivision\Import\Adapter\ImportAdapterInterface;
35
use TechDivision\Import\Exceptions\WrappedColumnException;
36
use TechDivision\Import\Services\RegistryProcessorInterface;
37
use TechDivision\Import\Configuration\SubjectConfigurationInterface;
38
39
/**
40
 * An abstract subject implementation.
41
 *
42
 * @author    Tim Wagner <[email protected]>
43
 * @copyright 2016 TechDivision GmbH <[email protected]>
44
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
45
 * @link      https://github.com/techdivision/import
46
 * @link      http://www.techdivision.com
47
 */
48
abstract class AbstractSubject implements SubjectInterface
49
{
50
51
    /**
52
     * The trait that provides basic filesystem handling functionality.
53
     *
54
     * @var TechDivision\Import\Subjects\FilesystemTrait
55
     */
56
    use FilesystemTrait;
57
58
    /**
59
     * The trait that provides basic filesystem handling functionality.
60
     *
61
     * @var TechDivision\Import\SystemLoggerTrait
62
     */
63
    use SystemLoggerTrait;
64
65
    /**
66
     * The trait that provides header handling functionality.
67
     *
68
     * @var TechDivision\Import\HeaderTrait
69
     */
70
    use HeaderTrait;
71
72
    /**
73
     * The trait that provides row handling functionality.
74
     *
75
     * @var TechDivision\Import\RowTrait
76
     */
77
    use RowTrait;
78
79
    /**
80
     * The name of the file to be imported.
81
     *
82
     * @var string
83
     */
84
    protected $filename;
85
86
    /**
87
     * The actual line number.
88
     *
89
     * @var integer
90
     */
91
    protected $lineNumber = 0;
92
93
    /**
94
     * The actual operation name.
95
     *
96
     * @var string
97
     */
98
    protected $operationName ;
99
100
    /**
101
     * The flag that stop's overserver execution on the actual row.
102
     *
103
     * @var boolean
104
     */
105
    protected $skipRow = false;
106
107
    /**
108
     * The import adapter instance.
109
     *
110
     * @var \TechDivision\Import\Adapter\ImportAdapterInterface
111
     */
112
    protected $importAdapter;
113
114
    /**
115
     * The system configuration.
116
     *
117
     * @var \TechDivision\Import\Configuration\SubjectConfigurationInterface
118
     */
119
    protected $configuration;
120
121
    /**
122
     * The RegistryProcessor instance to handle running threads.
123
     *
124
     * @var \TechDivision\Import\Services\RegistryProcessorInterface
125
     */
126
    protected $registryProcessor;
127
128
    /**
129
     * The actions unique serial.
130
     *
131
     * @var string
132
     */
133
    protected $serial;
134
135
    /**
136
     * Array with the subject's observers.
137
     *
138
     * @var array
139
     */
140
    protected $observers = array();
141
142
    /**
143
     * Array with the subject's callbacks.
144
     *
145
     * @var array
146
     */
147
    protected $callbacks = array();
148
149
    /**
150
     * The subject's callback mappings.
151
     *
152
     * @var array
153
     */
154
    protected $callbackMappings = array();
155
156
    /**
157
     * The available root categories.
158
     *
159
     * @var array
160
     */
161
    protected $rootCategories = array();
162
163
    /**
164
     * The Magento configuration.
165
     *
166
     * @var array
167
     */
168
    protected $coreConfigData = array();
169
170
    /**
171
     * The available stores.
172
     *
173
     * @var array
174
     */
175
    protected $stores = array();
176
177
    /**
178
     * The available websites.
179
     *
180
     * @var array
181
     */
182
    protected $storeWebsites = array();
183
184
    /**
185
     * The default store.
186
     *
187
     * @var array
188
     */
189
    protected $defaultStore;
190
191
    /**
192
     * The store view code the create the product/attributes for.
193
     *
194
     * @var string
195
     */
196
    protected $storeViewCode;
197
198
    /**
199
     * The UID generator for the core config data.
200
     *
201
     * @var \TechDivision\Import\Utils\Generators\GeneratorInterface
202
     */
203
    protected $coreConfigDataUidGenerator;
204
205
    /**
206
     * Initialize the subject instance.
207
     *
208
     * @param \TechDivision\Import\Services\RegistryProcessorInterface $registryProcessor          The registry processor instance
209
     * @param \TechDivision\Import\Utils\Generators\GeneratorInterface $coreConfigDataUidGenerator The UID generator for the core config data
210
     * @param \Doctrine\Common\Collections\Collection                  $systemLoggers              The array with the system loggers instances
211
     */
212 77
    public function __construct(
213
        RegistryProcessorInterface $registryProcessor,
214
        GeneratorInterface $coreConfigDataUidGenerator,
215
        Collection $systemLoggers
216
    ) {
217 77
        $this->systemLoggers = $systemLoggers;
0 ignored issues
show
Documentation Bug introduced by
It seems like $systemLoggers of type object<Doctrine\Common\Collections\Collection> is incompatible with the declared type array of property $systemLoggers.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
218 77
        $this->registryProcessor = $registryProcessor;
219 77
        $this->coreConfigDataUidGenerator = $coreConfigDataUidGenerator;
220 77
    }
221
222
    /**
223
     * Set's the name of the file to import
224
     *
225
     * @param string $filename The filename
226
     *
227
     * @return void
228
     */
229 13
    public function setFilename($filename)
230
    {
231 13
        $this->filename = $filename;
232 13
    }
233
234
    /**
235
     * Return's the name of the file to import.
236
     *
237
     * @return string The filename
238
     */
239 13
    public function getFilename()
240
    {
241 13
        return $this->filename;
242
    }
243
244
    /**
245
     * Set's the actual operation name.
246
     *
247
     * @param string $operationName The actual operation name
248
     *
249
     * @return void
250
     */
251 1
    public function setOperationName($operationName)
252
    {
253 1
        $this->operationName = $operationName;
254 1
    }
255
256
    /**
257
     * Return's the actual operation name.
258
     *
259
     * @return string
260
     */
261 1
    public function getOperationName()
262
    {
263 1
        return $this->operationName;
264
    }
265
266
    /**
267
     * Set's the actual line number.
268
     *
269
     * @param integer $lineNumber The line number
270
     *
271
     * @return void
272
     */
273 1
    public function setLineNumber($lineNumber)
274
    {
275 1
        $this->lineNumber = $lineNumber;
276 1
    }
277
278
    /**
279
     * Return's the actual line number.
280
     *
281
     * @return integer The line number
282
     */
283 13
    public function getLineNumber()
284
    {
285 13
        return $this->lineNumber;
286
    }
287
288
    /**
289
     * Stop's observer execution on the actual row.
290
     *
291
     * @return void
292
     */
293 1
    public function skipRow()
294
    {
295 1
        $this->skipRow = true;
296 1
    }
297
298
    /**
299
     * Return's the default callback mappings.
300
     *
301
     * @return array The default callback mappings
302
     */
303 1
    public function getDefaultCallbackMappings()
304
    {
305 1
        return array();
306
    }
307
308
    /**
309
     * Tries to format the passed value to a valid date with format 'Y-m-d H:i:s'.
310
     * If the passed value is NOT a valid date, NULL will be returned.
311
     *
312
     * @param string $value The value to format
313
     *
314
     * @return string|null The formatted date or NULL if the date is not valid
315
     */
316 2
    public function formatDate($value)
317
    {
318
319
        // create a DateTime instance from the passed value
320 2
        if ($dateTime = \DateTime::createFromFormat($this->getSourceDateFormat(), $value)) {
321 1
            return $dateTime->format('Y-m-d H:i:s');
322
        }
323
324
        // return NULL, if the passed value is NOT a valid date
325 1
        return null;
326
    }
327
328
    /**
329
     * Extracts the elements of the passed value by exploding them
330
     * with the also passed delimiter.
331
     *
332
     * @param string      $value     The value to extract
333
     * @param string|null $delimiter The delimiter used to extrace the elements
334
     *
335
     * @return array The exploded values
336
     */
337 2
    public function explode($value, $delimiter = null)
338
    {
339
        // load the global configuration
340 2
        $configuration = $this->getConfiguration();
341
342
        // initializet delimiter, enclosure and escape char
343 2
        $delimiter = $delimiter ? $delimiter : $configuration->getDelimiter();
344 2
        $enclosure = $configuration->getEnclosure();
345 2
        $escape = $configuration->getEscape();
346
347
        // parse and return the found data as array
348 2
        return str_getcsv($value, $delimiter, $enclosure, $escape);
349
    }
350
351
    /**
352
     * Queries whether or not debug mode is enabled or not, default is TRUE.
353
     *
354
     * @return boolean TRUE if debug mode is enabled, else FALSE
355
     */
356 1
    public function isDebugMode()
357
    {
358 1
        return $this->getConfiguration()->isDebugMode();
359
    }
360
361
    /**
362
     * Set's the subject configuration.
363
     *
364
     * @param \TechDivision\Import\Configuration\SubjectConfigurationInterface $configuration The subject configuration
365
     *
366
     * @return void
367
     */
368 77
    public function setConfiguration(SubjectConfigurationInterface $configuration)
369
    {
370 77
        $this->configuration = $configuration;
371 77
    }
372
373
    /**
374
     * Return's the subject configuration.
375
     *
376
     * @return \TechDivision\Import\Configuration\SubjectConfigurationInterface The subject configuration
377
     */
378 77
    public function getConfiguration()
379
    {
380 77
        return $this->configuration;
381
    }
382
383
    /**
384
     * Set's the import adapter instance.
385
     *
386
     * @param \TechDivision\Import\Adapter\ImportAdapterInterface $importAdapter The import adapter instance
387
     *
388
     * @return void
389
     */
390 3
    public function setImportAdapter(ImportAdapterInterface $importAdapter)
391
    {
392 3
        $this->importAdapter = $importAdapter;
393 3
    }
394
395
    /**
396
     * Return's the import adapter instance.
397
     *
398
     * @return \TechDivision\Import\Adapter\ImportAdapterInterface The import adapter instance
399
     */
400 3
    public function getImportAdapter()
401
    {
402 3
        return $this->importAdapter;
403
    }
404
405
    /**
406
     * Return's the RegistryProcessor instance to handle the running threads.
407
     *
408
     * @return \TechDivision\Import\Services\RegistryProcessorInterface The registry processor instance
409
     */
410 77
    public function getRegistryProcessor()
411
    {
412 77
        return $this->registryProcessor;
413
    }
414
415
    /**
416
     * Set's the unique serial for this import process.
417
     *
418
     * @param string $serial The unique serial
419
     *
420
     * @return void
421
     */
422 9
    public function setSerial($serial)
423
    {
424 9
        $this->serial = $serial;
425 9
    }
426
427
    /**
428
     * Return's the unique serial for this import process.
429
     *
430
     * @return string The unique serial
431
     */
432 4
    public function getSerial()
433
    {
434 4
        return $this->serial;
435
    }
436
437
    /**
438
     * Return's the source date format to use.
439
     *
440
     * @return string The source date format
441
     */
442 4
    public function getSourceDateFormat()
443
    {
444 4
        return $this->getConfiguration()->getSourceDateFormat();
445
    }
446
447
    /**
448
     * Return's the multiple field delimiter character to use, default value is comma (,).
449
     *
450
     * @return string The multiple field delimiter character
451
     */
452 1
    public function getMultipleFieldDelimiter()
453
    {
454 1
        return $this->getConfiguration()->getMultipleFieldDelimiter();
455
    }
456
457
    /**
458
     * Return's the multiple value delimiter character to use, default value is comma (|).
459
     *
460
     * @return string The multiple value delimiter character
461
     */
462 1
    public function getMultipleValueDelimiter()
463
    {
464 1
        return $this->getConfiguration()->getMultipleValueDelimiter();
465
    }
466
467
    /**
468
     * Intializes the previously loaded global data for exactly one bunch.
469
     *
470
     * @param string $serial The serial of the actual import
471
     *
472
     * @return void
473
     */
474 77
    public function setUp($serial)
475
    {
476
477
        // load the status of the actual import
478 77
        $status = $this->getRegistryProcessor()->getAttribute($serial);
479
480
        // load the global data we've prepared initially
481 77
        $this->stores = $status[RegistryKeys::GLOBAL_DATA][RegistryKeys::STORES];
482 77
        $this->defaultStore = $status[RegistryKeys::GLOBAL_DATA][RegistryKeys::DEFAULT_STORE];
483 77
        $this->storeWebsites  = $status[RegistryKeys::GLOBAL_DATA][RegistryKeys::STORE_WEBSITES];
484 77
        $this->rootCategories = $status[RegistryKeys::GLOBAL_DATA][RegistryKeys::ROOT_CATEGORIES];
485 77
        $this->coreConfigData = $status[RegistryKeys::GLOBAL_DATA][RegistryKeys::CORE_CONFIG_DATA];
486
487
        // initialize the operation name
488 77
        $this->operationName = $this->getConfiguration()->getConfiguration()->getOperationName();
489
490
        // merge the callback mappings with the mappings from the child instance
491 77
        $this->callbackMappings = array_merge($this->callbackMappings, $this->getDefaultCallbackMappings());
492
493
        // merge the header mappings with the values found in the configuration
494 77
        $this->headerMappings = array_merge($this->headerMappings, $this->getConfiguration()->getHeaderMappings());
495
496 77
        if(isset($this->imageTypes) && count($this->imageTypes)) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after IF keyword; 0 found
Loading history...
497
            $localHeaderMappings = array();
498
            foreach ($this->imageTypes as $key => $value) {
0 ignored issues
show
Bug introduced by
The property imageTypes does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
499
                $valueImage = $value . '_image';
500
                $valueImageLabel = $value . '_image_label';
501
                $localHeaderMappings[$valueImage] = $value;
502
                $localHeaderMappings[$valueImageLabel] = $value . '_label';
503
            }
504
            $this->headerMappings = array_merge($this->headerMappings, $localHeaderMappings);
505
        }
506
507
        // merge the callback mappings the the one from the configuration file
508 77
        foreach ($this->getConfiguration()->getCallbacks() as $callbackMappings) {
509 77
            foreach ($callbackMappings as $attributeCode => $mappings) {
510
                // write a log message, that default callback configuration will
511
                // be overwritten with the one from the configuration file
512 77
                if (isset($this->callbackMappings[$attributeCode])) {
513 72
                    $this->getSystemLogger()->notice(
514 72
                        sprintf('Now override callback mappings for attribute %s with values found in configuration file', $attributeCode)
515
                    );
516
                }
517
518
                // override the attributes callbacks
519 77
                $this->callbackMappings[$attributeCode] = $mappings;
520
            }
521
        }
522 77
    }
523
524
    /**
525
     * Clean up the global data after importing the variants.
526
     *
527
     * @param string $serial The serial of the actual import
528
     *
529
     * @return void
530
     */
531 1
    public function tearDown($serial)
532
    {
533
534
        // load the registry processor
535 1
        $registryProcessor = $this->getRegistryProcessor();
536
537
        // update the source directory for the next subject
538 1
        $registryProcessor->mergeAttributesRecursive(
539 1
            $serial,
540
            array(
541 1
                RegistryKeys::SOURCE_DIRECTORY => $newSourceDir = $this->getNewSourceDir($serial),
542 1
                RegistryKeys::FILES => array($this->getFilename() => 1)
543
            )
544
        );
545
546
        // log a debug message with the new source directory
547 1
        $this->getSystemLogger()->debug(
548 1
            sprintf('Subject %s successfully updated source directory to %s', get_class($this), $newSourceDir)
549
        );
550 1
    }
551
552
    /**
553
     * Return's the target directory for the artefact export.
554
     *
555
     * @return string The target directory for the artefact export
556
     */
557 1
    public function getTargetDir()
558
    {
559 1
        return $this->getNewSourceDir($this->getSerial());
560
    }
561
562
    /**
563
     * Return's the next source directory, which will be the target directory
564
     * of this subject, in most cases.
565
     *
566
     * @param string $serial The serial of the actual import
567
     *
568
     * @return string The new source directory
569
     */
570 4
    public function getNewSourceDir($serial)
571
    {
572 4
        return sprintf('%s/%s', $this->getConfiguration()->getTargetDir(), $serial);
573
    }
574
575
    /**
576
     * Register the passed observer with the specific type.
577
     *
578
     * @param \TechDivision\Import\Observers\ObserverInterface $observer The observer to register
579
     * @param string                                           $type     The type to register the observer with
580
     *
581
     * @return void
582
     */
583 6
    public function registerObserver(ObserverInterface $observer, $type)
584
    {
585
586
        // query whether or not the array with the callbacks for the
587
        // passed type has already been initialized, or not
588 6
        if (!isset($this->observers[$type])) {
589 6
            $this->observers[$type] = array();
590
        }
591
592
        // append the callback with the instance of the passed type
593 6
        $this->observers[$type][] = $observer;
594 6
    }
595
596
    /**
597
     * Register the passed callback with the specific type.
598
     *
599
     * @param \TechDivision\Import\Callbacks\CallbackInterface $callback The subject to register the callbacks for
600
     * @param string                                           $type     The type to register the callback with
601
     *
602
     * @return void
603
     */
604 2
    public function registerCallback(CallbackInterface $callback, $type)
605
    {
606
607
        // query whether or not the array with the callbacks for the
608
        // passed type has already been initialized, or not
609 2
        if (!isset($this->callbacks[$type])) {
610 2
            $this->callbacks[$type] = array();
611
        }
612
613
        // append the callback with the instance of the passed type
614 2
        $this->callbacks[$type][] = $callback;
615 2
    }
616
617
    /**
618
     * Return's the array with callbacks for the passed type.
619
     *
620
     * @param string $type The type of the callbacks to return
621
     *
622
     * @return array The callbacks
623
     */
624 1
    public function getCallbacksByType($type)
625
    {
626
627
        // initialize the array for the callbacks
628 1
        $callbacks = array();
629
630
        // query whether or not callbacks for the type are available
631 1
        if (isset($this->callbacks[$type])) {
632 1
            $callbacks = $this->callbacks[$type];
633
        }
634
635
        // return the array with the type's callbacks
636 1
        return $callbacks;
637
    }
638
639
    /**
640
     * Return's the array with the available observers.
641
     *
642
     * @return array The observers
643
     */
644 6
    public function getObservers()
645
    {
646 6
        return $this->observers;
647
    }
648
649
    /**
650
     * Return's the array with the available callbacks.
651
     *
652
     * @return array The callbacks
653
     */
654 1
    public function getCallbacks()
655
    {
656 1
        return $this->callbacks;
657
    }
658
659
    /**
660
     * Return's the callback mappings for this subject.
661
     *
662
     * @return array The array with the subject's callback mappings
663
     */
664 2
    public function getCallbackMappings()
665
    {
666 2
        return $this->callbackMappings;
667
    }
668
669
    /**
670
     * Imports the content of the file with the passed filename.
671
     *
672
     *
673
     * @param string $serial   The serial of the actual import
674
     * @param string $filename The filename to process
675
     *
676
     * @return void
677
     * @throws \Exception Is thrown, if the import can't be processed
678
     */
679 5
    public function import($serial, $filename)
680
    {
681
682
        try {
683
            // stop processing, if the filename doesn't match
684 5
            if (!$this->match($filename)) {
685 1
                return;
686
            }
687
688
            // load the system logger instance
689 4
            $systemLogger = $this->getSystemLogger();
690
691
            // prepare the flag filenames
692 4
            $inProgressFilename = sprintf('%s.inProgress', $filename);
693 4
            $importedFilename = sprintf('%s.imported', $filename);
694 4
            $failedFilename = sprintf('%s.failed', $filename);
695
696
            // query whether or not the file has already been imported
697 4
            if ($this->isFile($failedFilename) ||
698 3
                $this->isFile($importedFilename) ||
699 4
                $this->isFile($inProgressFilename)
700
            ) {
701
                // log a debug message and exit
702 1
                $systemLogger->debug(sprintf('Import running, found inProgress file %s', $inProgressFilename));
703 1
                return;
704
            }
705
706
            // flag file as in progress
707 3
            $this->touch($inProgressFilename);
708
709
            // track the start time
710 3
            $startTime = microtime(true);
711
712
            // initialize the serial/filename
713 3
            $this->setSerial($serial);
714 3
            $this->setFilename($filename);
715
716
            // log a message that the file has to be imported
717 3
            $systemLogger->info(sprintf('Now start processing file %s', $filename));
718
719
            // let the adapter process the file
720 3
            $this->getImportAdapter()->import(array($this, 'importRow'), $filename);
721
722
            // track the time needed for the import in seconds
723 1
            $endTime = microtime(true) - $startTime;
724
725
            // log a message that the file has successfully been imported
726 1
            $systemLogger->info(sprintf('Successfully processed file %s with %d lines in %f s', $filename, $this->lineNumber, $endTime));
727
728
            // rename flag file, because import has been successfull
729 1
            $this->rename($inProgressFilename, $importedFilename);
730
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
731 2
        } catch (\Exception $e) {
732
            // rename the flag file, because import failed and write the stack trace
733 2
            $this->rename($inProgressFilename, $failedFilename);
0 ignored issues
show
Bug introduced by
The variable $inProgressFilename does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
Bug introduced by
The variable $failedFilename does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
734 2
            $this->write($failedFilename, $e->__toString());
735
736
            // do not wrap the exception if not already done
737 2
            if ($e instanceof WrappedColumnException) {
738 1
                throw $e;
739
            }
740
741
            // else wrap and throw the exception
742 1
            throw $this->wrapException(array(), $e);
743
        }
744 1
    }
745
746
    /**
747
     * This method queries whether or not the passed filename matches
748
     * the pattern, based on the subjects configured prefix.
749
     *
750
     * @param string $filename The filename to match
751
     *
752
     * @return boolean TRUE if the filename matches, else FALSE
753
     */
754 5
    protected function match($filename)
755
    {
756
757
        // prepare the pattern to query whether the file has to be processed or not
758 5
        $pattern = sprintf(
759 5
            '/^.*\/%s.*\\.%s$/',
760 5
            $this->getConfiguration()->getPrefix(),
761 5
            $this->getConfiguration()->getSuffix()
762
        );
763
764
        // stop processing, if the filename doesn't match
765 5
        return (boolean) preg_match($pattern, $filename);
766
    }
767
768
    /**
769
     * Imports the passed row into the database. If the import failed, the exception
770
     * will be catched and logged, but the import process will be continued.
771
     *
772
     * @param array $row The row with the data to be imported
773
     *
774
     * @return void
775
     */
776 7
    public function importRow(array $row)
777
    {
778
779
        // initialize the row
780 7
        $this->row = $row;
781
782
        // raise the line number and reset the skip row flag
783 7
        $this->lineNumber++;
784 7
        $this->skipRow = false;
785
786
        // initialize the headers with the columns from the first line
787 7
        if (sizeof($this->headers) === 0) {
788 1
            foreach ($this->row as $value => $key) {
789 1
                $this->headers[$this->mapAttributeCodeByHeaderMapping($key)] = $value;
790
            }
791 1
            return;
792
        }
793
794
        // process the observers
795 6
        foreach ($this->getObservers() as $observers) {
796
            // invoke the pre-import/import and post-import observers
797 6
            foreach ($observers as $observer) {
798
                // query whether or not we have to skip the row
799 6
                if ($this->skipRow) {
800
                    // log a debug message with the actual line nr/file information
801 1
                    $this->getSystemLogger()->warning(
802 1
                        $this->appendExceptionSuffix(
803 1
                            sprintf(
804 1
                                'Skip processing operation "%s" after observer "%s"',
805 1
                                $this->operationName,
806 1
                                $this->getConfiguration()->getId()
807
                            )
808
                        )
809
                    );
810
811
                    // skip the row
812 1
                    break 2;
813
                }
814
815
                // if not, set the subject and process the observer
816 6
                if ($observer instanceof ObserverInterface) {
817 6
                    $this->row = $observer->handle($this);
818
                }
819
            }
820
        }
821
822
        // log a debug message with the actual line nr/file information
823 6
        $this->getSystemLogger()->debug(
824 6
            $this->appendExceptionSuffix(
825 6
                sprintf(
826 6
                    'Successfully processed operation "%s"',
827 6
                    $this->operationName
828
                )
829
            )
830
        );
831 6
    }
832
833
    /**
834
     * Queries whether or not that the subject needs an OK file to be processed.
835
     *
836
     * @return boolean TRUE if the subject needs an OK file, else FALSE
837
     */
838 1
    public function isOkFileNeeded()
839
    {
840 1
        return $this->getConfiguration()->isOkFileNeeded();
841
    }
842
843
    /**
844
     * Return's the default store.
845
     *
846
     * @return array The default store
847
     */
848
    public function getDefaultStore()
849
    {
850
        return $this->defaultStore;
851
    }
852
853
    /**
854
     * Return's the default store view code.
855
     *
856
     * @return array The default store view code
857
     */
858 5
    public function getDefaultStoreViewCode()
859
    {
860 5
        return $this->defaultStore[MemberNames::CODE];
861
    }
862
863
    /**
864
     * Set's the store view code the create the product/attributes for.
865
     *
866
     * @param string $storeViewCode The store view code
867
     *
868
     * @return void
869
     */
870 4
    public function setStoreViewCode($storeViewCode)
871
    {
872 4
        $this->storeViewCode = $storeViewCode;
873 4
    }
874
875
    /**
876
     * Return's the store view code the create the product/attributes for.
877
     *
878
     * @param string|null $default The default value to return, if the store view code has not been set
879
     *
880
     * @return string The store view code
881
     */
882 8
    public function getStoreViewCode($default = null)
883
    {
884
885
        // return the store view code, if available
886 8
        if ($this->storeViewCode !== null) {
887 4
            return $this->storeViewCode;
888
        }
889
890
        // if NOT and a default code is available
891 4
        if ($default !== null) {
892
            // return the default value
893 3
            return $default;
894
        }
895
896
        // return the default store view code
897 1
        return $this->getDefaultStoreViewCode();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->getDefaultStoreViewCode(); (array) is incompatible with the return type declared by the interface TechDivision\Import\Subj...rface::getStoreViewCode of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
898
    }
899
900
    /**
901
     * Prepare's the store view code in the subject. If the store_view_code row doesn't contain
902
     * any value, the default code of the default store view will be set.
903
     *
904
     * @return void
905
     */
906 2
    public function prepareStoreViewCode()
907
    {
908
909
        // re-set the store view code
910 2
        $this->setStoreViewCode(null);
911
912
        // initialize the store view code
913 2
        if ($storeViewCode = $this->getValue(ColumnKeys::STORE_VIEW_CODE)) {
914 2
            $this->setStoreViewCode($storeViewCode);
915
        }
916 2
    }
917
918
    /**
919
     * Return's the store ID of the store with the passed store view code
920
     *
921
     * @param string $storeViewCode The store view code to return the store ID for
922
     *
923
     * @return integer The ID of the store with the passed ID
924
     * @throws \Exception Is thrown, if the store with the actual code is not available
925
     */
926 4
    public function getStoreId($storeViewCode)
927
    {
928
929
        // query whether or not, the requested store is available
930 4
        if (isset($this->stores[$storeViewCode])) {
931 3
            return (integer) $this->stores[$storeViewCode][MemberNames::STORE_ID];
932
        }
933
934
        // throw an exception, if not
935 1
        throw new \Exception(
936 1
            sprintf(
937 1
                'Found invalid store view code %s in file %s on line %d',
938 1
                $storeViewCode,
939 1
                $this->getFilename(),
940 1
                $this->getLineNumber()
941
            )
942
        );
943
    }
944
945
    /**
946
     * Return's the store ID of the actual row, or of the default store
947
     * if no store view code is set in the CSV file.
948
     *
949
     * @param string|null $default The default store view code to use, if no store view code is set in the CSV file
950
     *
951
     * @return integer The ID of the actual store
952
     * @throws \Exception Is thrown, if the store with the actual code is not available
953
     */
954 2
    public function getRowStoreId($default = null)
955
    {
956
957
        // initialize the default store view code, if not passed
958 2
        if ($default === null) {
959 2
            $default = $this->getDefaultStoreViewCode();
960
        }
961
962
        // load the store view code the create the product/attributes for
963 2
        return $this->getStoreId($this->getStoreViewCode($default));
0 ignored issues
show
Bug introduced by
It seems like $default defined by $this->getDefaultStoreViewCode() on line 959 can also be of type array; however, TechDivision\Import\Subj...ect::getStoreViewCode() does only seem to accept string|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
Bug introduced by
It seems like $this->getStoreViewCode($default) targeting TechDivision\Import\Subj...ect::getStoreViewCode() can also be of type array; however, TechDivision\Import\Subj...ctSubject::getStoreId() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
964
    }
965
966
    /**
967
     * Return's the root category for the actual view store.
968
     *
969
     * @return array The store's root category
970
     * @throws \Exception Is thrown if the root category for the passed store code is NOT available
971
     */
972 2
    public function getRootCategory()
973
    {
974
975
        // load the actual store view code
976 2
        $storeViewCode = $this->getStoreViewCode($this->getDefaultStoreViewCode());
0 ignored issues
show
Documentation introduced by
$this->getDefaultStoreViewCode() is of type array, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
977
978
        // query weather or not we've a root category or not
979 2
        if (isset($this->rootCategories[$storeViewCode])) {
980 1
            return $this->rootCategories[$storeViewCode];
981
        }
982
983
        // throw an exception if the root category is NOT available
984 1
        throw new \Exception(sprintf('Root category for %s is not available', $storeViewCode));
985
    }
986
987
    /**
988
     * Return's the Magento configuration value.
989
     *
990
     * @param string  $path    The Magento path of the requested configuration value
991
     * @param mixed   $default The default value that has to be returned, if the requested configuration value is not set
992
     * @param string  $scope   The scope the configuration value has been set
993
     * @param integer $scopeId The scope ID the configuration value has been set
994
     *
995
     * @return mixed The configuration value
996
     * @throws \Exception Is thrown, if nor a value can be found or a default value has been passed
997
     */
998 5
    public function getCoreConfigData($path, $default = null, $scope = ScopeKeys::SCOPE_DEFAULT, $scopeId = 0)
999
    {
1000
1001
        // initialize the core config data
1002
        $coreConfigData = array(
1003 5
            MemberNames::PATH => $path,
1004 5
            MemberNames::SCOPE => $scope,
1005 5
            MemberNames::SCOPE_ID => $scopeId
1006
        );
1007
1008
        // generate the UID from the passed data
1009 5
        $uniqueIdentifier = $this->coreConfigDataUidGenerator->generate($coreConfigData);
1010
1011
        // iterate over the core config data and try to find the requested configuration value
1012 5
        if (isset($this->coreConfigData[$uniqueIdentifier])) {
1013 1
            return $this->coreConfigData[$uniqueIdentifier][MemberNames::VALUE];
1014
        }
1015
1016
        // query whether or not we've to query for the configuration value on fallback level 'websites' also
1017 4
        if ($scope === ScopeKeys::SCOPE_STORES) {
1018
            // query whether or not the website with the passed ID is available
1019 2
            foreach ($this->storeWebsites as $storeWebsite) {
1020 2
                if ($storeWebsite[MemberNames::WEBSITE_ID] === $scopeId) {
1021
                    // replace scope with 'websites' and website ID
1022 2
                    $coreConfigData = array_merge(
1023 2
                        $coreConfigData,
1024
                        array(
1025 2
                            MemberNames::SCOPE    => ScopeKeys::SCOPE_WEBSITES,
1026
                            MemberNames::SCOPE_ID => $storeWebsite[MemberNames::WEBSITE_ID]
1027
                        )
1028
                    );
1029
1030
                    // generate the UID from the passed data, merged with the 'websites' scope and ID
1031 2
                    $uniqueIdentifier = $this->coreConfigDataUidGenerator->generate($coreConfigData);
1032
1033
                    // query whether or not, the configuration value on 'websites' level
1034 2 View Code Duplication
                    if (isset($this->coreConfigData[$uniqueIdentifier][MemberNames::VALUE])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1035 2
                        return $this->coreConfigData[$uniqueIdentifier][MemberNames::VALUE];
1036
                    }
1037
                }
1038
            }
1039
        }
1040
1041
        // replace scope with 'default' and scope ID '0'
1042 3
        $coreConfigData = array_merge(
1043 3
            $coreConfigData,
1044
            array(
1045 3
                MemberNames::SCOPE    => ScopeKeys::SCOPE_DEFAULT,
1046
                MemberNames::SCOPE_ID => 0
1047
            )
1048
        );
1049
1050
        // generate the UID from the passed data, merged with the 'default' scope and ID 0
1051 3
        $uniqueIdentifier = $this->coreConfigDataUidGenerator->generate($coreConfigData);
1052
1053
        // query whether or not, the configuration value on 'default' level
1054 3 View Code Duplication
        if (isset($this->coreConfigData[$uniqueIdentifier][MemberNames::VALUE])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1055 1
            return $this->coreConfigData[$uniqueIdentifier][MemberNames::VALUE];
1056
        }
1057
1058
        // if not, return the passed default value
1059 2
        if ($default !== null) {
1060 1
            return $default;
1061
        }
1062
1063
        // throw an exception if no value can be found
1064
        // in the Magento configuration
1065 1
        throw new \Exception(
1066 1
            sprintf(
1067 1
                'Can\'t find a value for configuration "%s-%s-%d" in "core_config_data"',
1068 1
                $path,
1069 1
                $scope,
1070 1
                $scopeId
1071
            )
1072
        );
1073
    }
1074
1075
    /**
1076
     * Resolve the original column name for the passed one.
1077
     *
1078
     * @param string $columnName The column name that has to be resolved
1079
     *
1080
     * @return string|null The original column name
1081
     */
1082 2
    public function resolveOriginalColumnName($columnName)
1083
    {
1084
1085
        // try to load the original data
1086 2
        $originalData = $this->getOriginalData();
1087
1088
        // query whether or not original data is available
1089 2
        if (isset($originalData[ColumnKeys::ORIGINAL_COLUMN_NAMES])) {
1090
            // query whether or not the original column name is available
1091 1
            if (isset($originalData[ColumnKeys::ORIGINAL_COLUMN_NAMES][$columnName])) {
1092 1
                return $originalData[ColumnKeys::ORIGINAL_COLUMN_NAMES][$columnName];
1093
            }
1094
1095
            // query whether or a wildcard column name is available
1096 1
            if (isset($originalData[ColumnKeys::ORIGINAL_COLUMN_NAMES]['*'])) {
1097 1
                return $originalData[ColumnKeys::ORIGINAL_COLUMN_NAMES]['*'];
1098
            }
1099
        }
1100
1101
        // return the original column name
1102 1
        return $columnName;
1103
    }
1104
1105
    /**
1106
     * Return's the original data if available, or an empty array.
1107
     *
1108
     * @return array The original data
1109
     */
1110 2
    public function getOriginalData()
1111
    {
1112
1113
        // initialize the array for the original data
1114 2
        $originalData = array();
1115
1116
        // query whether or not the column contains original data
1117 2
        if ($this->hasOriginalData()) {
1118
            // unerialize the original data from the column
1119 1
            $originalData = unserialize($this->row[$this->headers[ColumnKeys::ORIGINAL_DATA]]);
1120
        }
1121
1122
        // return an empty array, if not
1123 2
        return $originalData;
1124
    }
1125
1126
    /**
1127
     * Query's whether or not the actual column contains original data like
1128
     * filename, line number and column names.
1129
     *
1130
     * @return boolean TRUE if the actual column contains origin data, else FALSE
1131
     */
1132 3
    public function hasOriginalData()
1133
    {
1134 3
        return isset($this->headers[ColumnKeys::ORIGINAL_DATA]) && isset($this->row[$this->headers[ColumnKeys::ORIGINAL_DATA]]);
1135
    }
1136
1137
    /**
1138
     * Wraps the passed exeception into a new one by trying to resolve the original filname,
1139
     * line number and column names and use it for a detailed exception message.
1140
     *
1141
     * @param array      $columnNames The column names that should be resolved and wrapped
1142
     * @param \Exception $parent      The exception we want to wrap
1143
     * @param string     $className   The class name of the exception type we want to wrap the parent one
1144
     *
1145
     * @return \Exception the wrapped exception
1146
     */
1147 2
    public function wrapException(
1148
        array $columnNames = array(),
1149
        \Exception $parent = null,
1150
        $className = '\TechDivision\Import\Exceptions\WrappedColumnException'
1151
    ) {
1152
1153
        // initialize the message
1154 2
        $message = $parent->getMessage();
0 ignored issues
show
Bug introduced by
It seems like $parent is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
1155
1156
        // query whether or not has been a result of invalid data of a previous column of a CSV file
1157 2
        if ($this->hasOriginalData()) {
1158
            // load the original data
1159 1
            $originalData = $this->getOriginalData();
1160
1161
            // replace old filename and line number of the original message
1162 1
            $message = $this->appendExceptionSuffix(
1163 1
                $this->stripExceptionSuffix($message),
1164 1
                $originalData[ColumnKeys::ORIGINAL_FILENAME],
1165 1
                $originalData[ColumnKeys::ORIGINAL_LINE_NUMBER]
1166
            );
1167
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
1168
        } else {
1169
            // append filename and line number to the original message
1170 1
            $message = $this->appendExceptionSuffix(
1171 1
                $this->stripExceptionSuffix($message),
1172 1
                $this->filename,
1173 1
                $this->lineNumber
1174
            );
1175
        }
1176
1177
        // query whether or not, column names has been passed
1178 2
        if (sizeof($columnNames) > 0) {
1179
            // prepare the original column names
1180 1
            $originalColumnNames = array();
1181 1
            foreach ($columnNames as $columnName) {
1182 1
                $originalColumnNames[] = $this->resolveOriginalColumnName($columnName);
1183
            }
1184
1185
            // append the column information
1186 1
            $message = sprintf('%s in column(s) %s', $message, implode(', ', $originalColumnNames));
1187
        }
1188
1189
        // create a new exception and wrap the parent one
1190 2
        return new $className($message, null, $parent);
1191
    }
1192
1193
    /**
1194
     * Strip's the exception suffix containing filename and line number from the
1195
     * passed message.
1196
     *
1197
     * @param string $message The message to strip the exception suffix from
1198
     *
1199
     * @return mixed The message without the exception suffix
1200
     */
1201 2
    public function stripExceptionSuffix($message)
1202
    {
1203 2
        return str_replace($this->appendExceptionSuffix(), '', $message);
1204
    }
1205
1206
    /**
1207
     * Append's the exception suffix containing filename and line number to the
1208
     * passed message. If no message has been passed, only the suffix will be
1209
     * returned
1210
     *
1211
     * @param string|null $message    The message to append the exception suffix to
1212
     * @param string|null $filename   The filename used to create the suffix
1213
     * @param string|null $lineNumber The line number used to create the suffx
1214
     *
1215
     * @return string The message with the appended exception suffix
1216
     */
1217 12
    public function appendExceptionSuffix($message = null, $filename = null, $lineNumber = null)
1218
    {
1219
1220
        // query whether or not a filename has been passed
1221 12
        if ($filename === null) {
1222 12
            $filename = $this->getFilename();
1223
        }
1224
1225
        // query whether or not a line number has been passed
1226 12
        if ($lineNumber === null) {
1227 12
            $lineNumber = $this->getLineNumber();
1228
        }
1229
1230
        // if no message has been passed, only return the suffix
1231 12
        if ($message === null) {
1232 2
            return sprintf(' in file %s on line %d', $filename, $lineNumber);
1233
        }
1234
1235
        // concatenate the message with the suffix and return it
1236 12
        return sprintf('%s in file %s on line %d', $message, $filename, $lineNumber);
1237
    }
1238
1239
    /**
1240
     * Raises the value for the counter with the passed key by one.
1241
     *
1242
     * @param mixed $counterName The name of the counter to raise
1243
     *
1244
     * @return integer The counter's new value
1245
     */
1246 1
    public function raiseCounter($counterName)
1247
    {
1248
1249
        // raise the counter with the passed name
1250 1
        return $this->getRegistryProcessor()->raiseCounter(
1251 1
            $this->getSerial(),
1252 1
            $counterName
1253
        );
1254
    }
1255
1256
    /**
1257
     * Merge the passed array into the status of the actual import.
1258
     *
1259
     * @param array $status The status information to be merged
1260
     *
1261
     * @return void
1262
     */
1263 1
    public function mergeAttributesRecursive(array $status)
1264
    {
1265
1266
        // merge the passed status
1267 1
        $this->getRegistryProcessor()->mergeAttributesRecursive(
1268 1
            $this->getSerial(),
1269 1
            $status
1270
        );
1271 1
    }
1272
}
1273