Completed
Push — master ( f53ff2...c7d703 )
by Tim
09:19 queued 04:55
created

AbstractSubject::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 5
cts 5
cp 1
rs 9.6666
c 0
b 0
f 0
cc 1
eloc 7
nc 1
nop 3
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
     * UNIX timestamp with the date the last row counter has been logged.
207
     *
208
     * @var integer
209
     */
210
    protected $lastLog = 0;
211
212
    /**
213
     * The number of the last line that has been logged with the row counter
214
     * @var integer
215
     */
216
    protected $lastLineNumber = 0;
217
218
    /**
219
     * Initialize the subject instance.
220
     *
221
     * @param \TechDivision\Import\Services\RegistryProcessorInterface $registryProcessor          The registry processor instance
222
     * @param \TechDivision\Import\Utils\Generators\GeneratorInterface $coreConfigDataUidGenerator The UID generator for the core config data
223
     * @param \Doctrine\Common\Collections\Collection                  $systemLoggers              The array with the system loggers instances
224
     */
225 77
    public function __construct(
226
        RegistryProcessorInterface $registryProcessor,
227
        GeneratorInterface $coreConfigDataUidGenerator,
228
        Collection $systemLoggers
229
    ) {
230 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...
231 77
        $this->registryProcessor = $registryProcessor;
232 77
        $this->coreConfigDataUidGenerator = $coreConfigDataUidGenerator;
233 77
    }
234
235
    /**
236
     * Set's the name of the file to import
237
     *
238
     * @param string $filename The filename
239
     *
240
     * @return void
241
     */
242 13
    public function setFilename($filename)
243
    {
244 13
        $this->filename = $filename;
245 13
    }
246
247
    /**
248
     * Return's the name of the file to import.
249
     *
250
     * @return string The filename
251
     */
252 13
    public function getFilename()
253
    {
254 13
        return $this->filename;
255
    }
256
257
    /**
258
     * Set's the actual operation name.
259
     *
260
     * @param string $operationName The actual operation name
261
     *
262
     * @return void
263
     */
264 1
    public function setOperationName($operationName)
265
    {
266 1
        $this->operationName = $operationName;
267 1
    }
268
269
    /**
270
     * Return's the actual operation name.
271
     *
272
     * @return string
273
     */
274 1
    public function getOperationName()
275
    {
276 1
        return $this->operationName;
277
    }
278
279
    /**
280
     * Set's the actual line number.
281
     *
282
     * @param integer $lineNumber The line number
283
     *
284
     * @return void
285
     */
286 1
    public function setLineNumber($lineNumber)
287
    {
288 1
        $this->lineNumber = $lineNumber;
289 1
    }
290
291
    /**
292
     * Return's the actual line number.
293
     *
294
     * @return integer The line number
295
     */
296 13
    public function getLineNumber()
297
    {
298 13
        return $this->lineNumber;
299
    }
300
301
    /**
302
     * Stop's observer execution on the actual row.
303
     *
304
     * @return void
305
     */
306 1
    public function skipRow()
307
    {
308 1
        $this->skipRow = true;
309 1
    }
310
311
    /**
312
     * Return's the default callback mappings.
313
     *
314
     * @return array The default callback mappings
315
     */
316 1
    public function getDefaultCallbackMappings()
317
    {
318 1
        return array();
319
    }
320
321
    /**
322
     * Tries to format the passed value to a valid date with format 'Y-m-d H:i:s'.
323
     * If the passed value is NOT a valid date, NULL will be returned.
324
     *
325
     * @param string $value The value to format
326
     *
327
     * @return string|null The formatted date or NULL if the date is not valid
328
     */
329 2
    public function formatDate($value)
330
    {
331
332
        // create a DateTime instance from the passed value
333 2
        if ($dateTime = \DateTime::createFromFormat($this->getSourceDateFormat(), $value)) {
334 1
            return $dateTime->format('Y-m-d H:i:s');
335
        }
336
337
        // return NULL, if the passed value is NOT a valid date
338 1
        return null;
339
    }
340
341
    /**
342
     * Extracts the elements of the passed value by exploding them
343
     * with the also passed delimiter.
344
     *
345
     * @param string      $value     The value to extract
346
     * @param string|null $delimiter The delimiter used to extrace the elements
347
     *
348
     * @return array The exploded values
349
     */
350 2
    public function explode($value, $delimiter = null)
351
    {
352
        // load the global configuration
353 2
        $configuration = $this->getConfiguration();
354
355
        // initializet delimiter, enclosure and escape char
356 2
        $delimiter = $delimiter ? $delimiter : $configuration->getDelimiter();
357 2
        $enclosure = $configuration->getEnclosure();
358 2
        $escape = $configuration->getEscape();
359
360
        // parse and return the found data as array
361 2
        return str_getcsv($value, $delimiter, $enclosure, $escape);
362
    }
363
364
    /**
365
     * Queries whether or not debug mode is enabled or not, default is TRUE.
366
     *
367
     * @return boolean TRUE if debug mode is enabled, else FALSE
368
     */
369 1
    public function isDebugMode()
370
    {
371 1
        return $this->getConfiguration()->isDebugMode();
372
    }
373
374
    /**
375
     * Set's the subject configuration.
376
     *
377
     * @param \TechDivision\Import\Configuration\SubjectConfigurationInterface $configuration The subject configuration
378
     *
379
     * @return void
380
     */
381 77
    public function setConfiguration(SubjectConfigurationInterface $configuration)
382
    {
383 77
        $this->configuration = $configuration;
384 77
    }
385
386
    /**
387
     * Return's the subject configuration.
388
     *
389
     * @return \TechDivision\Import\Configuration\SubjectConfigurationInterface The subject configuration
390
     */
391 77
    public function getConfiguration()
392
    {
393 77
        return $this->configuration;
394
    }
395
396
    /**
397
     * Set's the import adapter instance.
398
     *
399
     * @param \TechDivision\Import\Adapter\ImportAdapterInterface $importAdapter The import adapter instance
400
     *
401
     * @return void
402
     */
403 3
    public function setImportAdapter(ImportAdapterInterface $importAdapter)
404
    {
405 3
        $this->importAdapter = $importAdapter;
406 3
    }
407
408
    /**
409
     * Return's the import adapter instance.
410
     *
411
     * @return \TechDivision\Import\Adapter\ImportAdapterInterface The import adapter instance
412
     */
413 3
    public function getImportAdapter()
414
    {
415 3
        return $this->importAdapter;
416
    }
417
418
    /**
419
     * Return's the RegistryProcessor instance to handle the running threads.
420
     *
421
     * @return \TechDivision\Import\Services\RegistryProcessorInterface The registry processor instance
422
     */
423 77
    public function getRegistryProcessor()
424
    {
425 77
        return $this->registryProcessor;
426
    }
427
428
    /**
429
     * Set's the unique serial for this import process.
430
     *
431
     * @param string $serial The unique serial
432
     *
433
     * @return void
434
     */
435 9
    public function setSerial($serial)
436
    {
437 9
        $this->serial = $serial;
438 9
    }
439
440
    /**
441
     * Return's the unique serial for this import process.
442
     *
443
     * @return string The unique serial
444
     */
445 4
    public function getSerial()
446
    {
447 4
        return $this->serial;
448
    }
449
450
    /**
451
     * Return's the source date format to use.
452
     *
453
     * @return string The source date format
454
     */
455 4
    public function getSourceDateFormat()
456
    {
457 4
        return $this->getConfiguration()->getSourceDateFormat();
458
    }
459
460
    /**
461
     * Return's the multiple field delimiter character to use, default value is comma (,).
462
     *
463
     * @return string The multiple field delimiter character
464
     */
465 1
    public function getMultipleFieldDelimiter()
466
    {
467 1
        return $this->getConfiguration()->getMultipleFieldDelimiter();
468
    }
469
470
    /**
471
     * Return's the multiple value delimiter character to use, default value is comma (|).
472
     *
473
     * @return string The multiple value delimiter character
474
     */
475 1
    public function getMultipleValueDelimiter()
476
    {
477 1
        return $this->getConfiguration()->getMultipleValueDelimiter();
478
    }
479
480
    /**
481
     * Intializes the previously loaded global data for exactly one bunch.
482
     *
483
     * @param string $serial The serial of the actual import
484
     *
485
     * @return void
486
     */
487 77
    public function setUp($serial)
488
    {
489
490
        // load the status of the actual import
491 77
        $status = $this->getRegistryProcessor()->getAttribute($serial);
492
493
        // load the global data we've prepared initially
494 77
        $this->stores = $status[RegistryKeys::GLOBAL_DATA][RegistryKeys::STORES];
495 77
        $this->defaultStore = $status[RegistryKeys::GLOBAL_DATA][RegistryKeys::DEFAULT_STORE];
496 77
        $this->storeWebsites  = $status[RegistryKeys::GLOBAL_DATA][RegistryKeys::STORE_WEBSITES];
497 77
        $this->rootCategories = $status[RegistryKeys::GLOBAL_DATA][RegistryKeys::ROOT_CATEGORIES];
498 77
        $this->coreConfigData = $status[RegistryKeys::GLOBAL_DATA][RegistryKeys::CORE_CONFIG_DATA];
499
500
        // initialize the operation name
501 77
        $this->operationName = $this->getConfiguration()->getConfiguration()->getOperationName();
502
503
        // merge the callback mappings with the mappings from the child instance
504 77
        $this->callbackMappings = array_merge($this->callbackMappings, $this->getDefaultCallbackMappings());
505
506
        // merge the header mappings with the values found in the configuration
507 77
        $this->headerMappings = array_merge($this->headerMappings, $this->getConfiguration()->getHeaderMappings());
508
509
        // merge the callback mappings the the one from the configuration file
510 77
        foreach ($this->getConfiguration()->getCallbacks() as $callbackMappings) {
511 77
            foreach ($callbackMappings as $attributeCode => $mappings) {
512
                // write a log message, that default callback configuration will
513
                // be overwritten with the one from the configuration file
514 77
                if (isset($this->callbackMappings[$attributeCode])) {
515 72
                    $this->getSystemLogger()->notice(
516 72
                        sprintf('Now override callback mappings for attribute %s with values found in configuration file', $attributeCode)
517
                    );
518
                }
519
520
                // override the attributes callbacks
521 77
                $this->callbackMappings[$attributeCode] = $mappings;
522
            }
523
        }
524 77
    }
525
526
    /**
527
     * Clean up the global data after importing the variants.
528
     *
529
     * @param string $serial The serial of the actual import
530
     *
531
     * @return void
532
     */
533 1
    public function tearDown($serial)
534
    {
535
536
        // load the registry processor
537 1
        $registryProcessor = $this->getRegistryProcessor();
538
539
        // update the source directory for the next subject
540 1
        $registryProcessor->mergeAttributesRecursive(
541 1
            $serial,
542
            array(
543 1
                RegistryKeys::SOURCE_DIRECTORY => $newSourceDir = $this->getNewSourceDir($serial),
544 1
                RegistryKeys::FILES => array($this->getFilename() => array(RegistryKeys::STATUS => 1))
545
            )
546
        );
547
548
        // log a debug message with the new source directory
549 1
        $this->getSystemLogger()->debug(
550 1
            sprintf('Subject %s successfully updated source directory to %s', get_class($this), $newSourceDir)
551
        );
552 1
    }
553
554
    /**
555
     * Return's the target directory for the artefact export.
556
     *
557
     * @return string The target directory for the artefact export
558
     */
559 1
    public function getTargetDir()
560
    {
561 1
        return $this->getNewSourceDir($this->getSerial());
562
    }
563
564
    /**
565
     * Return's the next source directory, which will be the target directory
566
     * of this subject, in most cases.
567
     *
568
     * @param string $serial The serial of the actual import
569
     *
570
     * @return string The new source directory
571
     */
572 4
    public function getNewSourceDir($serial)
573
    {
574 4
        return sprintf('%s/%s', $this->getConfiguration()->getTargetDir(), $serial);
575
    }
576
577
    /**
578
     * Register the passed observer with the specific type.
579
     *
580
     * @param \TechDivision\Import\Observers\ObserverInterface $observer The observer to register
581
     * @param string                                           $type     The type to register the observer with
582
     *
583
     * @return void
584
     */
585 6
    public function registerObserver(ObserverInterface $observer, $type)
586
    {
587
588
        // query whether or not the array with the callbacks for the
589
        // passed type has already been initialized, or not
590 6
        if (!isset($this->observers[$type])) {
591 6
            $this->observers[$type] = array();
592
        }
593
594
        // append the callback with the instance of the passed type
595 6
        $this->observers[$type][] = $observer;
596 6
    }
597
598
    /**
599
     * Register the passed callback with the specific type.
600
     *
601
     * @param \TechDivision\Import\Callbacks\CallbackInterface $callback The subject to register the callbacks for
602
     * @param string                                           $type     The type to register the callback with
603
     *
604
     * @return void
605
     */
606 2
    public function registerCallback(CallbackInterface $callback, $type)
607
    {
608
609
        // query whether or not the array with the callbacks for the
610
        // passed type has already been initialized, or not
611 2
        if (!isset($this->callbacks[$type])) {
612 2
            $this->callbacks[$type] = array();
613
        }
614
615
        // append the callback with the instance of the passed type
616 2
        $this->callbacks[$type][] = $callback;
617 2
    }
618
619
    /**
620
     * Return's the array with callbacks for the passed type.
621
     *
622
     * @param string $type The type of the callbacks to return
623
     *
624
     * @return array The callbacks
625
     */
626 1
    public function getCallbacksByType($type)
627
    {
628
629
        // initialize the array for the callbacks
630 1
        $callbacks = array();
631
632
        // query whether or not callbacks for the type are available
633 1
        if (isset($this->callbacks[$type])) {
634 1
            $callbacks = $this->callbacks[$type];
635
        }
636
637
        // return the array with the type's callbacks
638 1
        return $callbacks;
639
    }
640
641
    /**
642
     * Return's the array with the available observers.
643
     *
644
     * @return array The observers
645
     */
646 6
    public function getObservers()
647
    {
648 6
        return $this->observers;
649
    }
650
651
    /**
652
     * Return's the array with the available callbacks.
653
     *
654
     * @return array The callbacks
655
     */
656 1
    public function getCallbacks()
657
    {
658 1
        return $this->callbacks;
659
    }
660
661
    /**
662
     * Return's the callback mappings for this subject.
663
     *
664
     * @return array The array with the subject's callback mappings
665
     */
666 2
    public function getCallbackMappings()
667
    {
668 2
        return $this->callbackMappings;
669
    }
670
671
    /**
672
     * Imports the content of the file with the passed filename.
673
     *
674
     *
675
     * @param string $serial   The serial of the actual import
676
     * @param string $filename The filename to process
677
     *
678
     * @return void
679
     * @throws \Exception Is thrown, if the import can't be processed
680
     */
681 5
    public function import($serial, $filename)
682
    {
683
684
        try {
685
            // stop processing, if the filename doesn't match
686 5
            if (!$this->match($filename)) {
687 1
                return;
688
            }
689
690
            // load the system logger instance
691 4
            $systemLogger = $this->getSystemLogger();
692
693
            // prepare the flag filenames
694 4
            $inProgressFilename = sprintf('%s.inProgress', $filename);
695 4
            $importedFilename = sprintf('%s.imported', $filename);
696 4
            $failedFilename = sprintf('%s.failed', $filename);
697
698
            // query whether or not the file has already been imported
699 4
            if ($this->isFile($failedFilename) ||
700 3
                $this->isFile($importedFilename) ||
701 4
                $this->isFile($inProgressFilename)
702
            ) {
703
                // log a debug message and exit
704 1
                $systemLogger->debug(sprintf('Import running, found inProgress file %s', $inProgressFilename));
705 1
                return;
706
            }
707
708
            // flag file as in progress
709 3
            $this->touch($inProgressFilename);
710
711
            // track the start time
712 3
            $startTime = microtime(true);
713
714
            // initialize the serial/filename
715 3
            $this->setSerial($serial);
716 3
            $this->setFilename($filename);
717
718
            // initialize the last time we've logged the counter with the processed rows per minute
719 3
            $this->lastLog = time();
720
721
            // log a message that the file has to be imported
722 3
            $systemLogger->info(sprintf('Now start processing file %s', $filename));
723
724
            // let the adapter process the file
725 3
            $this->getImportAdapter()->import(array($this, 'importRow'), $filename);
726
727
            // track the time needed for the import in seconds
728 1
            $endTime = microtime(true) - $startTime;
729
730
            // log a message that the file has successfully been imported
731 1
            $systemLogger->info(sprintf('Successfully processed file %s with %d lines in %f s', $filename, $this->lineNumber, $endTime));
732
733
            // rename flag file, because import has been successfull
734 1
            $this->rename($inProgressFilename, $importedFilename);
735
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
736 2
        } catch (\Exception $e) {
737
            // rename the flag file, because import failed and write the stack trace
738 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...
739 2
            $this->write($failedFilename, $e->__toString());
740
741
            // do not wrap the exception if not already done
742 2
            if ($e instanceof WrappedColumnException) {
743 1
                throw $e;
744
            }
745
746
            // else wrap and throw the exception
747 1
            throw $this->wrapException(array(), $e);
748
        }
749 1
    }
750
751
    /**
752
     * This method queries whether or not the passed filename matches
753
     * the pattern, based on the subjects configured prefix.
754
     *
755
     * @param string $filename The filename to match
756
     *
757
     * @return boolean TRUE if the filename matches, else FALSE
758
     */
759 5
    protected function match($filename)
760
    {
761
762
        // prepare the pattern to query whether the file has to be processed or not
763 5
        $pattern = sprintf(
764 5
            '/^.*\/%s.*\\.%s$/',
765 5
            $this->getConfiguration()->getPrefix(),
766 5
            $this->getConfiguration()->getSuffix()
767
        );
768
769
        // stop processing, if the filename doesn't match
770 5
        return (boolean) preg_match($pattern, $filename);
771
    }
772
773
    /**
774
     * Imports the passed row into the database. If the import failed, the exception
775
     * will be catched and logged, but the import process will be continued.
776
     *
777
     * @param array $row The row with the data to be imported
778
     *
779
     * @return void
780
     */
781 7
    public function importRow(array $row)
782
    {
783
784
        // initialize the row
785 7
        $this->row = $row;
786
787
        // raise the line number and reset the skip row flag
788 7
        $this->lineNumber++;
789 7
        $this->skipRow = false;
790
791
        // initialize the headers with the columns from the first line
792 7
        if (sizeof($this->headers) === 0) {
793 1
            foreach ($this->row as $value => $key) {
794 1
                $this->headers[$this->mapAttributeCodeByHeaderMapping($key)] = $value;
795
            }
796 1
            return;
797
        }
798
799
        // process the observers
800 6
        foreach ($this->getObservers() as $observers) {
801
            // invoke the pre-import/import and post-import observers
802 6
            foreach ($observers as $observer) {
803
                // query whether or not we have to skip the row
804 6
                if ($this->skipRow) {
805
                    // log a debug message with the actual line nr/file information
806 1
                    $this->getSystemLogger()->warning(
807 1
                        $this->appendExceptionSuffix(
808 1
                            sprintf(
809 1
                                'Skip processing operation "%s" after observer "%s"',
810 1
                                $this->operationName,
811 1
                                $this->getConfiguration()->getId()
812
                            )
813
                        )
814
                    );
815
816
                    // skip the row
817 1
                    break 2;
818
                }
819
820
                // if not, set the subject and process the observer
821 6
                if ($observer instanceof ObserverInterface) {
822 6
                    $this->row = $observer->handle($this);
823
                }
824
            }
825
        }
826
827
        // query whether or not a minute has been passed
828 6
        if ($this->lastLog < time() - 60) {
829
            // log the number processed rows per minute
830 6
            $this->getSystemLogger()->info(
831 6
                sprintf(
832 6
                    'Successfully processed %d rows per minute of file %s',
833 6
                    $this->lineNumber - $this->lastLineNumber,
834 6
                    $this->getFilename()
835
                )
836
            );
837
838
            // reset the last log time and the line number
839 6
            $this->lastLog = time();
840 6
            $this->lastLineNumber = $this->lineNumber;
841
        }
842
843
        // log a debug message with the actual line nr/file information
844 6
        $this->getSystemLogger()->debug(
845 6
            $this->appendExceptionSuffix(
846 6
                sprintf(
847 6
                    'Successfully processed operation "%s"',
848 6
                    $this->operationName
849
                )
850
            )
851
        );
852 6
    }
853
854
    /**
855
     * Queries whether or not that the subject needs an OK file to be processed.
856
     *
857
     * @return boolean TRUE if the subject needs an OK file, else FALSE
858
     */
859 1
    public function isOkFileNeeded()
860
    {
861 1
        return $this->getConfiguration()->isOkFileNeeded();
862
    }
863
864
    /**
865
     * Return's the default store.
866
     *
867
     * @return array The default store
868
     */
869
    public function getDefaultStore()
870
    {
871
        return $this->defaultStore;
872
    }
873
874
    /**
875
     * Return's the default store view code.
876
     *
877
     * @return array The default store view code
878
     */
879 5
    public function getDefaultStoreViewCode()
880
    {
881 5
        return $this->defaultStore[MemberNames::CODE];
882
    }
883
884
    /**
885
     * Set's the store view code the create the product/attributes for.
886
     *
887
     * @param string $storeViewCode The store view code
888
     *
889
     * @return void
890
     */
891 4
    public function setStoreViewCode($storeViewCode)
892
    {
893 4
        $this->storeViewCode = $storeViewCode;
894 4
    }
895
896
    /**
897
     * Return's the store view code the create the product/attributes for.
898
     *
899
     * @param string|null $default The default value to return, if the store view code has not been set
900
     *
901
     * @return string The store view code
902
     */
903 8
    public function getStoreViewCode($default = null)
904
    {
905
906
        // return the store view code, if available
907 8
        if ($this->storeViewCode !== null) {
908 4
            return $this->storeViewCode;
909
        }
910
911
        // if NOT and a default code is available
912 4
        if ($default !== null) {
913
            // return the default value
914 3
            return $default;
915
        }
916
917
        // return the default store view code
918 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...
919
    }
920
921
    /**
922
     * Prepare's the store view code in the subject. If the store_view_code row doesn't contain
923
     * any value, the default code of the default store view will be set.
924
     *
925
     * @return void
926
     */
927 2
    public function prepareStoreViewCode()
928
    {
929
930
        // re-set the store view code
931 2
        $this->setStoreViewCode(null);
932
933
        // initialize the store view code
934 2
        if ($storeViewCode = $this->getValue(ColumnKeys::STORE_VIEW_CODE)) {
935 2
            $this->setStoreViewCode($storeViewCode);
936
        }
937 2
    }
938
939
    /**
940
     * Return's the store ID of the store with the passed store view code
941
     *
942
     * @param string $storeViewCode The store view code to return the store ID for
943
     *
944
     * @return integer The ID of the store with the passed ID
945
     * @throws \Exception Is thrown, if the store with the actual code is not available
946
     */
947 4
    public function getStoreId($storeViewCode)
948
    {
949
950
        // query whether or not, the requested store is available
951 4
        if (isset($this->stores[$storeViewCode])) {
952 3
            return (integer) $this->stores[$storeViewCode][MemberNames::STORE_ID];
953
        }
954
955
        // throw an exception, if not
956 1
        throw new \Exception(
957 1
            sprintf(
958 1
                'Found invalid store view code %s in file %s on line %d',
959 1
                $storeViewCode,
960 1
                $this->getFilename(),
961 1
                $this->getLineNumber()
962
            )
963
        );
964
    }
965
966
    /**
967
     * Return's the store ID of the actual row, or of the default store
968
     * if no store view code is set in the CSV file.
969
     *
970
     * @param string|null $default The default store view code to use, if no store view code is set in the CSV file
971
     *
972
     * @return integer The ID of the actual store
973
     * @throws \Exception Is thrown, if the store with the actual code is not available
974
     */
975 2
    public function getRowStoreId($default = null)
976
    {
977
978
        // initialize the default store view code, if not passed
979 2
        if ($default === null) {
980 2
            $default = $this->getDefaultStoreViewCode();
981
        }
982
983
        // load the store view code the create the product/attributes for
984 2
        return $this->getStoreId($this->getStoreViewCode($default));
0 ignored issues
show
Bug introduced by
It seems like $default defined by $this->getDefaultStoreViewCode() on line 980 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...
985
    }
986
987
    /**
988
     * Return's the root category for the actual view store.
989
     *
990
     * @return array The store's root category
991
     * @throws \Exception Is thrown if the root category for the passed store code is NOT available
992
     */
993 2
    public function getRootCategory()
994
    {
995
996
        // load the actual store view code
997 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...
998
999
        // query weather or not we've a root category or not
1000 2
        if (isset($this->rootCategories[$storeViewCode])) {
1001 1
            return $this->rootCategories[$storeViewCode];
1002
        }
1003
1004
        // throw an exception if the root category is NOT available
1005 1
        throw new \Exception(sprintf('Root category for %s is not available', $storeViewCode));
1006
    }
1007
1008
    /**
1009
     * Return's the Magento configuration value.
1010
     *
1011
     * @param string  $path    The Magento path of the requested configuration value
1012
     * @param mixed   $default The default value that has to be returned, if the requested configuration value is not set
1013
     * @param string  $scope   The scope the configuration value has been set
1014
     * @param integer $scopeId The scope ID the configuration value has been set
1015
     *
1016
     * @return mixed The configuration value
1017
     * @throws \Exception Is thrown, if nor a value can be found or a default value has been passed
1018
     */
1019 5
    public function getCoreConfigData($path, $default = null, $scope = ScopeKeys::SCOPE_DEFAULT, $scopeId = 0)
1020
    {
1021
1022
        // initialize the core config data
1023
        $coreConfigData = array(
1024 5
            MemberNames::PATH => $path,
1025 5
            MemberNames::SCOPE => $scope,
1026 5
            MemberNames::SCOPE_ID => $scopeId
1027
        );
1028
1029
        // generate the UID from the passed data
1030 5
        $uniqueIdentifier = $this->coreConfigDataUidGenerator->generate($coreConfigData);
1031
1032
        // iterate over the core config data and try to find the requested configuration value
1033 5
        if (isset($this->coreConfigData[$uniqueIdentifier])) {
1034 1
            return $this->coreConfigData[$uniqueIdentifier][MemberNames::VALUE];
1035
        }
1036
1037
        // query whether or not we've to query for the configuration value on fallback level 'websites' also
1038 4
        if ($scope === ScopeKeys::SCOPE_STORES) {
1039
            // query whether or not the website with the passed ID is available
1040 2
            foreach ($this->storeWebsites as $storeWebsite) {
1041 2
                if ($storeWebsite[MemberNames::WEBSITE_ID] === $scopeId) {
1042
                    // replace scope with 'websites' and website ID
1043 2
                    $coreConfigData = array_merge(
1044 2
                        $coreConfigData,
1045
                        array(
1046 2
                            MemberNames::SCOPE    => ScopeKeys::SCOPE_WEBSITES,
1047
                            MemberNames::SCOPE_ID => $storeWebsite[MemberNames::WEBSITE_ID]
1048
                        )
1049
                    );
1050
1051
                    // generate the UID from the passed data, merged with the 'websites' scope and ID
1052 2
                    $uniqueIdentifier = $this->coreConfigDataUidGenerator->generate($coreConfigData);
1053
1054
                    // query whether or not, the configuration value on 'websites' level
1055 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...
1056 2
                        return $this->coreConfigData[$uniqueIdentifier][MemberNames::VALUE];
1057
                    }
1058
                }
1059
            }
1060
        }
1061
1062
        // replace scope with 'default' and scope ID '0'
1063 3
        $coreConfigData = array_merge(
1064 3
            $coreConfigData,
1065
            array(
1066 3
                MemberNames::SCOPE    => ScopeKeys::SCOPE_DEFAULT,
1067
                MemberNames::SCOPE_ID => 0
1068
            )
1069
        );
1070
1071
        // generate the UID from the passed data, merged with the 'default' scope and ID 0
1072 3
        $uniqueIdentifier = $this->coreConfigDataUidGenerator->generate($coreConfigData);
1073
1074
        // query whether or not, the configuration value on 'default' level
1075 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...
1076 1
            return $this->coreConfigData[$uniqueIdentifier][MemberNames::VALUE];
1077
        }
1078
1079
        // if not, return the passed default value
1080 2
        if ($default !== null) {
1081 1
            return $default;
1082
        }
1083
1084
        // throw an exception if no value can be found
1085
        // in the Magento configuration
1086 1
        throw new \Exception(
1087 1
            sprintf(
1088 1
                'Can\'t find a value for configuration "%s-%s-%d" in "core_config_data"',
1089 1
                $path,
1090 1
                $scope,
1091 1
                $scopeId
1092
            )
1093
        );
1094
    }
1095
1096
    /**
1097
     * Resolve the original column name for the passed one.
1098
     *
1099
     * @param string $columnName The column name that has to be resolved
1100
     *
1101
     * @return string|null The original column name
1102
     */
1103 2
    public function resolveOriginalColumnName($columnName)
1104
    {
1105
1106
        // try to load the original data
1107 2
        $originalData = $this->getOriginalData();
1108
1109
        // query whether or not original data is available
1110 2
        if (isset($originalData[ColumnKeys::ORIGINAL_COLUMN_NAMES])) {
1111
            // query whether or not the original column name is available
1112 1
            if (isset($originalData[ColumnKeys::ORIGINAL_COLUMN_NAMES][$columnName])) {
1113 1
                return $originalData[ColumnKeys::ORIGINAL_COLUMN_NAMES][$columnName];
1114
            }
1115
1116
            // query whether or a wildcard column name is available
1117 1
            if (isset($originalData[ColumnKeys::ORIGINAL_COLUMN_NAMES]['*'])) {
1118 1
                return $originalData[ColumnKeys::ORIGINAL_COLUMN_NAMES]['*'];
1119
            }
1120
        }
1121
1122
        // return the original column name
1123 1
        return $columnName;
1124
    }
1125
1126
    /**
1127
     * Return's the original data if available, or an empty array.
1128
     *
1129
     * @return array The original data
1130
     */
1131 2
    public function getOriginalData()
1132
    {
1133
1134
        // initialize the array for the original data
1135 2
        $originalData = array();
1136
1137
        // query whether or not the column contains original data
1138 2
        if ($this->hasOriginalData()) {
1139
            // unerialize the original data from the column
1140 1
            $originalData = unserialize($this->row[$this->headers[ColumnKeys::ORIGINAL_DATA]]);
1141
        }
1142
1143
        // return an empty array, if not
1144 2
        return $originalData;
1145
    }
1146
1147
    /**
1148
     * Query's whether or not the actual column contains original data like
1149
     * filename, line number and column names.
1150
     *
1151
     * @return boolean TRUE if the actual column contains origin data, else FALSE
1152
     */
1153 3
    public function hasOriginalData()
1154
    {
1155 3
        return isset($this->headers[ColumnKeys::ORIGINAL_DATA]) && isset($this->row[$this->headers[ColumnKeys::ORIGINAL_DATA]]);
1156
    }
1157
1158
    /**
1159
     * Wraps the passed exeception into a new one by trying to resolve the original filname,
1160
     * line number and column names and use it for a detailed exception message.
1161
     *
1162
     * @param array      $columnNames The column names that should be resolved and wrapped
1163
     * @param \Exception $parent      The exception we want to wrap
1164
     * @param string     $className   The class name of the exception type we want to wrap the parent one
1165
     *
1166
     * @return \Exception the wrapped exception
1167
     */
1168 2
    public function wrapException(
1169
        array $columnNames = array(),
1170
        \Exception $parent = null,
1171
        $className = '\TechDivision\Import\Exceptions\WrappedColumnException'
1172
    ) {
1173
1174
        // initialize the message
1175 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...
1176
1177
        // query whether or not has been a result of invalid data of a previous column of a CSV file
1178 2
        if ($this->hasOriginalData()) {
1179
            // load the original data
1180 1
            $originalData = $this->getOriginalData();
1181
1182
            // replace old filename and line number of the original message
1183 1
            $message = $this->appendExceptionSuffix(
1184 1
                $this->stripExceptionSuffix($message),
1185 1
                $originalData[ColumnKeys::ORIGINAL_FILENAME],
1186 1
                $originalData[ColumnKeys::ORIGINAL_LINE_NUMBER]
1187
            );
1188
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
1189
        } else {
1190
            // append filename and line number to the original message
1191 1
            $message = $this->appendExceptionSuffix(
1192 1
                $this->stripExceptionSuffix($message),
1193 1
                $this->filename,
1194 1
                $this->lineNumber
1195
            );
1196
        }
1197
1198
        // query whether or not, column names has been passed
1199 2
        if (sizeof($columnNames) > 0) {
1200
            // prepare the original column names
1201 1
            $originalColumnNames = array();
1202 1
            foreach ($columnNames as $columnName) {
1203 1
                $originalColumnNames[] = $this->resolveOriginalColumnName($columnName);
1204
            }
1205
1206
            // append the column information
1207 1
            $message = sprintf('%s in column(s) %s', $message, implode(', ', $originalColumnNames));
1208
        }
1209
1210
        // create a new exception and wrap the parent one
1211 2
        return new $className($message, null, $parent);
1212
    }
1213
1214
    /**
1215
     * Strip's the exception suffix containing filename and line number from the
1216
     * passed message.
1217
     *
1218
     * @param string $message The message to strip the exception suffix from
1219
     *
1220
     * @return mixed The message without the exception suffix
1221
     */
1222 2
    public function stripExceptionSuffix($message)
1223
    {
1224 2
        return str_replace($this->appendExceptionSuffix(), '', $message);
1225
    }
1226
1227
    /**
1228
     * Append's the exception suffix containing filename and line number to the
1229
     * passed message. If no message has been passed, only the suffix will be
1230
     * returned
1231
     *
1232
     * @param string|null $message    The message to append the exception suffix to
1233
     * @param string|null $filename   The filename used to create the suffix
1234
     * @param string|null $lineNumber The line number used to create the suffx
1235
     *
1236
     * @return string The message with the appended exception suffix
1237
     */
1238 12
    public function appendExceptionSuffix($message = null, $filename = null, $lineNumber = null)
1239
    {
1240
1241
        // query whether or not a filename has been passed
1242 12
        if ($filename === null) {
1243 12
            $filename = $this->getFilename();
1244
        }
1245
1246
        // query whether or not a line number has been passed
1247 12
        if ($lineNumber === null) {
1248 12
            $lineNumber = $this->getLineNumber();
1249
        }
1250
1251
        // if no message has been passed, only return the suffix
1252 12
        if ($message === null) {
1253 2
            return sprintf(' in file %s on line %d', $filename, $lineNumber);
1254
        }
1255
1256
        // concatenate the message with the suffix and return it
1257 12
        return sprintf('%s in file %s on line %d', $message, $filename, $lineNumber);
1258
    }
1259
1260
    /**
1261
     * Raises the value for the counter with the passed key by one.
1262
     *
1263
     * @param mixed $counterName The name of the counter to raise
1264
     *
1265
     * @return integer The counter's new value
1266
     */
1267 1
    public function raiseCounter($counterName)
1268
    {
1269
1270
        // raise the counter with the passed name
1271 1
        return $this->getRegistryProcessor()->raiseCounter(
1272 1
            $this->getSerial(),
1273 1
            $counterName
1274
        );
1275
    }
1276
1277
    /**
1278
     * Merge the passed array into the status of the actual import.
1279
     *
1280
     * @param array $status The status information to be merged
1281
     *
1282
     * @return void
1283
     */
1284 1
    public function mergeAttributesRecursive(array $status)
1285
    {
1286
1287
        // merge the passed status
1288 1
        $this->getRegistryProcessor()->mergeAttributesRecursive(
1289 1
            $this->getSerial(),
1290 1
            $status
1291
        );
1292 1
    }
1293
}
1294