Completed
Pull Request — master (#51)
by Tim
09:44
created

AbstractSubject::appendExceptionSuffix()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 21
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
dl 0
loc 21
c 0
b 0
f 0
ccs 0
cts 13
cp 0
rs 9.0534
cc 4
eloc 8
nc 8
nop 3
crap 20
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 Psr\Log\LoggerInterface;
24
use Goodby\CSV\Import\Standard\Lexer;
25
use Goodby\CSV\Import\Standard\LexerConfig;
26
use Goodby\CSV\Import\Standard\Interpreter;
27
use TechDivision\Import\Utils\ScopeKeys;
28
use TechDivision\Import\Utils\LoggerKeys;
29
use TechDivision\Import\Utils\ColumnKeys;
30
use TechDivision\Import\Utils\MemberNames;
31
use TechDivision\Import\Utils\RegistryKeys;
32
use TechDivision\Import\Utils\Generators\GeneratorInterface;
33
use TechDivision\Import\Services\RegistryProcessor;
34
use TechDivision\Import\Callbacks\CallbackVisitor;
35
use TechDivision\Import\Callbacks\CallbackInterface;
36
use TechDivision\Import\Observers\ObserverVisitor;
37
use TechDivision\Import\Observers\ObserverInterface;
38
use TechDivision\Import\Services\RegistryProcessorInterface;
39
use TechDivision\Import\Configuration\SubjectConfigurationInterface;
40
41
/**
42
 * An abstract subject implementation.
43
 *
44
 * @author    Tim Wagner <[email protected]>
45
 * @copyright 2016 TechDivision GmbH <[email protected]>
46
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
47
 * @link      https://github.com/techdivision/import
48
 * @link      http://www.techdivision.com
49
 */
50
abstract class AbstractSubject implements SubjectInterface
51
{
52
53
    /**
54
     * The trait that provides basic filesystem handling functionality.
55
     *
56
     * @var TechDivision\Import\Subjects\FilesystemTrait
57
     */
58
    use FilesystemTrait;
59
60
    /**
61
     * The system configuration.
62
     *
63
     * @var \TechDivision\Import\Configuration\SubjectConfigurationInterface
64
     */
65
    protected $configuration;
66
67
    /**
68
     * The array with the system logger instances.
69
     *
70
     * @var array
71
     */
72
    protected $systemLoggers = array();
73
74
    /**
75
     * The RegistryProcessor instance to handle running threads.
76
     *
77
     * @var \TechDivision\Import\Services\RegistryProcessorInterface
78
     */
79
    protected $registryProcessor;
80
81
    /**
82
     * The actions unique serial.
83
     *
84
     * @var string
85
     */
86
    protected $serial;
87
88
    /**
89
     * The name of the file to be imported.
90
     *
91
     * @var string
92
     */
93
    protected $filename;
94
95
    /**
96
     * Array with the subject's observers.
97
     *
98
     * @var array
99
     */
100
    protected $observers = array();
101
102
    /**
103
     * Array with the subject's callbacks.
104
     *
105
     * @var array
106
     */
107
    protected $callbacks = array();
108
109
    /**
110
     * The subject's callback mappings.
111
     *
112
     * @var array
113
     */
114
    protected $callbackMappings = array();
115
116
    /**
117
     * Contain's the column names from the header line.
118
     *
119
     * @var array
120
     */
121
    protected $headers = array();
122
123
    /**
124
     * The actual line number.
125
     *
126
     * @var integer
127
     */
128
    protected $lineNumber = 0;
129
130
    /**
131
     * The actual operation name.
132
     *
133
     * @var string
134
     */
135
    protected $operationName ;
136
137
    /**
138
     * The flag that stop's overserver execution on the actual row.
139
     *
140
     * @var boolean
141
     */
142
    protected $skipRow = false;
143
144
    /**
145
     * The available root categories.
146
     *
147
     * @var array
148
     */
149
    protected $rootCategories = array();
150
151
    /**
152
     * The Magento configuration.
153
     *
154
     * @var array
155
     */
156
    protected $coreConfigData = array();
157
158
    /**
159
     * The available stores.
160
     *
161
     * @var array
162
     */
163
    protected $stores = array();
164
165
    /**
166
     * The default store.
167
     *
168
     * @var array
169
     */
170
    protected $defaultStore;
171
172
    /**
173
     * The store view code the create the product/attributes for.
174
     *
175
     * @var string
176
     */
177
    protected $storeViewCode;
178
179
    /**
180
     * The UID generator for the core config data.
181
     *
182
     * @var \TechDivision\Import\Utils\Generators\GeneratorInterface
183
     */
184
    protected $coreConfigDataUidGenerator;
185
186
    /**
187
     * The actual row.
188
     *
189
     * @var array
190
     */
191
    protected $row = array();
192
193
    /**
194
     * Initialize the subject instance.
195
     *
196
     * @param \TechDivision\Import\Configuration\SubjectConfigurationInterface $configuration              The subject configuration instance
197
     * @param \TechDivision\Import\Services\RegistryProcessorInterface         $registryProcessor          The registry processor instance
198
     * @param \TechDivision\Import\Utils\Generators\GeneratorInterface         $coreConfigDataUidGenerator The UID generator for the core config data
199
     * @param array                                                            $systemLoggers              The array with the system loggers instances
200
     */
201
    public function __construct(
202
        SubjectConfigurationInterface $configuration,
203
        RegistryProcessorInterface $registryProcessor,
204
        GeneratorInterface $coreConfigDataUidGenerator,
205
        array $systemLoggers
206
    ) {
207
        $this->configuration = $configuration;
208
        $this->registryProcessor = $registryProcessor;
209
        $this->coreConfigDataUidGenerator = $coreConfigDataUidGenerator;
210
        $this->systemLoggers = $systemLoggers;
211
    }
212
213
    /**
214
     * Return's the default callback mappings.
215
     *
216
     * @return array The default callback mappings
217
     */
218
    public function getDefaultCallbackMappings()
219
    {
220
        return array();
221
    }
222
223
    /**
224
     * Return's the actual row.
225
     *
226
     * @return array The actual row
227
     */
228
    public function getRow()
229
    {
230
        return $this->row;
231
    }
232
233
    /**
234
     * Stop's observer execution on the actual row.
235
     *
236
     * @return void
237
     */
238
    public function skipRow()
239
    {
240
        $this->skipRow = true;
241
    }
242
243
    /**
244
     * Return's the actual line number.
245
     *
246
     * @return integer The line number
247
     */
248
    public function getLineNumber()
249
    {
250
        return $this->lineNumber;
251
    }
252
253
    /**
254
     * Return's the actual operation name.
255
     *
256
     * @return string
257
     */
258
    public function getOperationName()
259
    {
260
        return $this->operationName;
261
    }
262
263
    /**
264
     * Set's the array containing header row.
265
     *
266
     * @param array $headers The array with the header row
267
     *
268
     * @return void
269
     */
270
    public function setHeaders(array $headers)
271
    {
272
        $this->headers = $headers;
273
    }
274
275
    /**
276
     * Return's the array containing header row.
277
     *
278
     * @return array The array with the header row
279
     */
280
    public function getHeaders()
281
    {
282
        return $this->headers;
283
    }
284
285
    /**
286
     * Queries whether or not the header with the passed name is available.
287
     *
288
     * @param string $name The header name to query
289
     *
290
     * @return boolean TRUE if the header is available, else FALSE
291
     */
292
    public function hasHeader($name)
293
    {
294
        return isset($this->headers[$name]);
295
    }
296
297
    /**
298
     * Return's the header value for the passed name.
299
     *
300
     * @param string $name The name of the header to return the value for
301
     *
302
     * @return mixed The header value
303
     * \InvalidArgumentException Is thrown, if the header with the passed name is NOT available
304
     */
305
    public function getHeader($name)
306
    {
307
308
        // query whether or not, the header is available
309
        if (isset($this->headers[$name])) {
310
            return $this->headers[$name];
311
        }
312
313
        // throw an exception, if not
314
        throw new \InvalidArgumentException(sprintf('Header %s is not available', $name));
315
    }
316
317
    /**
318
     * Add's the header with the passed name and position, if not NULL.
319
     *
320
     * @param string $name The header name to add
321
     *
322
     * @return integer The new headers position
323
     */
324
    public function addHeader($name)
325
    {
326
327
        // add the header
328
        $this->headers[$name] = $position = sizeof($this->headers);
329
330
        // return the new header's position
331
        return $position;
332
    }
333
334
    /**
335
     * Queries whether or not debug mode is enabled or not, default is TRUE.
336
     *
337
     * @return boolean TRUE if debug mode is enabled, else FALSE
338
     */
339
    public function isDebugMode()
340
    {
341
        return $this->getConfiguration()->isDebugMode();
342
    }
343
344
    /**
345
     * Return's the system configuration.
346
     *
347
     * @return \TechDivision\Import\Configuration\SubjectConfigurationInterface The system configuration
348
     */
349
    public function getConfiguration()
350
    {
351
        return $this->configuration;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->configuration; (TechDivision\Import\Conf...tConfigurationInterface) is incompatible with the return type declared by the interface TechDivision\Import\Subj...rface::getConfiguration of type TechDivision\Import\Configuration\SubjectInterface.

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...
352
    }
353
354
    /**
355
     * Return's the logger with the passed name, by default the system logger.
356
     *
357
     * @param string $name The name of the requested system logger
358
     *
359
     * @return \Psr\Log\LoggerInterface The logger instance
360
     * @throws \Exception Is thrown, if the requested logger is NOT available
361
     */
362
    public function getSystemLogger($name = LoggerKeys::SYSTEM)
363
    {
364
365
        // query whether or not, the requested logger is available
366
        if (isset($this->systemLoggers[$name])) {
367
            return $this->systemLoggers[$name];
368
        }
369
370
        // throw an exception if the requested logger is NOT available
371
        throw new \Exception(sprintf('The requested logger \'%s\' is not available', $name));
372
    }
373
374
    /**
375
     * Return's the array with the system logger instances.
376
     *
377
     * @return array The logger instance
378
     */
379
    public function getSystemLoggers()
380
    {
381
        return $this->systemLoggers;
382
    }
383
384
    /**
385
     * Return's the RegistryProcessor instance to handle the running threads.
386
     *
387
     * @return \TechDivision\Import\Services\RegistryProcessorInterface The registry processor instance
388
     */
389
    public function getRegistryProcessor()
390
    {
391
        return $this->registryProcessor;
392
    }
393
394
    /**
395
     * Set's the unique serial for this import process.
396
     *
397
     * @param string $serial The unique serial
398
     *
399
     * @return void
400
     */
401
    public function setSerial($serial)
402
    {
403
        $this->serial = $serial;
404
    }
405
406
    /**
407
     * Return's the unique serial for this import process.
408
     *
409
     * @return string The unique serial
410
     */
411
    public function getSerial()
412
    {
413
        return $this->serial;
414
    }
415
416
    /**
417
     * Set's the name of the file to import
418
     *
419
     * @param string $filename The filename
420
     *
421
     * @return void
422
     */
423
    public function setFilename($filename)
424
    {
425
        $this->filename = $filename;
426
    }
427
428
    /**
429
     * Return's the name of the file to import.
430
     *
431
     * @return string The filename
432
     */
433
    public function getFilename()
434
    {
435
        return $this->filename;
436
    }
437
438
    /**
439
     * Return's the source date format to use.
440
     *
441
     * @return string The source date format
442
     */
443
    public function getSourceDateFormat()
444
    {
445
        return $this->getConfiguration()->getSourceDateFormat();
446
    }
447
448
    /**
449
     * Return's the multiple field delimiter character to use, default value is comma (,).
450
     *
451
     * @return string The multiple field delimiter character
452
     */
453
    public function getMultipleFieldDelimiter()
454
    {
455
        return $this->getConfiguration()->getMultipleFieldDelimiter();
0 ignored issues
show
Bug introduced by
The method getMultipleFieldDelimiter() does not seem to exist on object<TechDivision\Impo...ConfigurationInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
456
    }
457
458
    /**
459
     * Return's the initialized PDO connection.
460
     *
461
     * @return \PDO The initialized PDO connection
462
     */
463
    public function getConnection()
464
    {
465
        return $this->getProductProcessor()->getConnection();
0 ignored issues
show
Bug introduced by
The method getProductProcessor() does not seem to exist on object<TechDivision\Impo...bjects\AbstractSubject>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
466
    }
467
468
    /**
469
     * Intializes the previously loaded global data for exactly one bunch.
470
     *
471
     * @return void
472
     * @see \Importer\Csv\Actions\ProductImportAction::prepare()
473
     */
474
    public function setUp()
475
    {
476
477
        // load the status of the actual import
478
        $status = $this->getRegistryProcessor()->getAttribute($this->getSerial());
479
480
        // load the global data we've prepared initially
481
        $this->stores = $status[RegistryKeys::GLOBAL_DATA][RegistryKeys::STORES];
482
        $this->defaultStore = $status[RegistryKeys::GLOBAL_DATA][RegistryKeys::DEFAULT_STORE];
483
        $this->rootCategories = $status[RegistryKeys::GLOBAL_DATA][RegistryKeys::ROOT_CATEGORIES];
484
        $this->coreConfigData = $status[RegistryKeys::GLOBAL_DATA][RegistryKeys::CORE_CONFIG_DATA];
485
486
        // initialize the operation name
487
        $this->operationName = $this->getConfiguration()->getConfiguration()->getOperationName();
488
489
        // merge the callback mappings with the mappings from the child instance
490
        $this->callbackMappings = array_merge($this->callbackMappings, $this->getDefaultCallbackMappings());
491
492
        // merge the callback mappings the the one from the configuration file
493
        foreach ($this->getConfiguration()->getCallbacks() as $callbackMappings) {
494
            foreach ($callbackMappings as $attributeCode => $mappings) {
495
                // write a log message, that default callback configuration will
496
                // be overwritten with the one from the configuration file
497
                if (isset($this->callbackMappings[$attributeCode])) {
498
                    $this->getSystemLogger()->notice(
499
                        sprintf('Now override callback mappings for attribute %s with values found in configuration file', $attributeCode)
500
                    );
501
                }
502
503
                // override the attributes callbacks
504
                $this->callbackMappings[$attributeCode] = $mappings;
505
            }
506
        }
507
508
        // initialize the callbacks/observers
509
        CallbackVisitor::get()->visit($this);
510
        ObserverVisitor::get()->visit($this);
511
    }
512
513
    /**
514
     * Clean up the global data after importing the variants.
515
     *
516
     * @return void
517
     */
518
    public function tearDown()
519
    {
520
521
        // load the registry processor
522
        $registryProcessor = $this->getRegistryProcessor();
523
524
        // update the source directory for the next subject
525
        $registryProcessor->mergeAttributesRecursive(
526
            $this->getSerial(),
527
            array(RegistryKeys::SOURCE_DIRECTORY => $this->getNewSourceDir())
528
        );
529
530
        // log a debug message with the new source directory
531
        $this->getSystemLogger()->debug(
532
            sprintf('Subject %s successfully updated source directory to %s', __CLASS__, $this->getNewSourceDir())
533
        );
534
    }
535
536
    /**
537
     * Return's the next source directory, which will be the target directory
538
     * of this subject, in most cases.
539
     *
540
     * @return string The new source directory
541
     */
542
    protected function getNewSourceDir()
543
    {
544
        return sprintf('%s/%s', $this->getConfiguration()->getTargetDir(), $this->getSerial());
545
    }
546
547
    /**
548
     * Register the passed observer with the specific type.
549
     *
550
     * @param \TechDivision\Import\Observers\ObserverInterface $observer The observer to register
551
     * @param string                                           $type     The type to register the observer with
552
     *
553
     * @return void
554
     */
555
    public function registerObserver(ObserverInterface $observer, $type)
556
    {
557
558
        // query whether or not the array with the callbacks for the
559
        // passed type has already been initialized, or not
560
        if (!isset($this->observers[$type])) {
561
            $this->observers[$type] = array();
562
        }
563
564
        // append the callback with the instance of the passed type
565
        $this->observers[$type][] = $observer;
566
    }
567
568
    /**
569
     * Register the passed callback with the specific type.
570
     *
571
     * @param \TechDivision\Import\Callbacks\CallbackInterface $callback The subject to register the callbacks for
572
     * @param string                                           $type     The type to register the callback with
573
     *
574
     * @return void
575
     */
576
    public function registerCallback(CallbackInterface $callback, $type)
577
    {
578
579
        // query whether or not the array with the callbacks for the
580
        // passed type has already been initialized, or not
581
        if (!isset($this->callbacks[$type])) {
582
            $this->callbacks[$type] = array();
583
        }
584
585
        // append the callback with the instance of the passed type
586
        $this->callbacks[$type][] = $callback;
587
    }
588
589
    /**
590
     * Return's the array with callbacks for the passed type.
591
     *
592
     * @param string $type The type of the callbacks to return
593
     *
594
     * @return array The callbacks
595
     */
596
    public function getCallbacksByType($type)
597
    {
598
599
        // initialize the array for the callbacks
600
        $callbacks = array();
601
602
        // query whether or not callbacks for the type are available
603
        if (isset($this->callbacks[$type])) {
604
            $callbacks = $this->callbacks[$type];
605
        }
606
607
        // return the array with the type's callbacks
608
        return $callbacks;
609
    }
610
611
    /**
612
     * Return's the array with the available observers.
613
     *
614
     * @return array The observers
615
     */
616
    public function getObservers()
617
    {
618
        return $this->observers;
619
    }
620
621
    /**
622
     * Return's the array with the available callbacks.
623
     *
624
     * @return array The callbacks
625
     */
626
    public function getCallbacks()
627
    {
628
        return $this->callbacks;
629
    }
630
631
    /**
632
     * Return's the callback mappings for this subject.
633
     *
634
     * @return array The array with the subject's callback mappings
635
     */
636
    public function getCallbackMappings()
637
    {
638
        return $this->callbackMappings;
639
    }
640
641
    /**
642
     * Imports the content of the file with the passed filename.
643
     *
644
     * @param string $serial   The unique process serial
645
     * @param string $filename The filename to process
646
     *
647
     * @return void
648
     * @throws \Exception Is thrown, if the import can't be processed
649
     */
650
    public function import($serial, $filename)
651
    {
652
653
        try {
654
            // stop processing, if the filename doesn't match
655
            if (!$this->match($filename)) {
656
                return;
657
            }
658
659
            // load the system logger instance
660
            $systemLogger = $this->getSystemLogger();
661
662
            // prepare the flag filenames
663
            $inProgressFilename = sprintf('%s.inProgress', $filename);
664
            $importedFilename = sprintf('%s.imported', $filename);
665
            $failedFilename = sprintf('%s.failed', $filename);
666
667
            // query whether or not the file has already been imported
668
            if (is_file($failedFilename) ||
669
                is_file($importedFilename) ||
670
                is_file($inProgressFilename)
671
            ) {
672
                // log a debug message and exit
673
                $systemLogger->debug(sprintf('Import running, found inProgress file %s', $inProgressFilename));
674
                return;
675
            }
676
677
            // flag file as in progress
678
            touch($inProgressFilename);
679
680
            // track the start time
681
            $startTime = microtime(true);
682
683
            // initialize serial and filename
684
            $this->setSerial($serial);
685
            $this->setFilename($filename);
686
687
            // load the system logger
688
            $systemLogger = $this->getSystemLogger();
689
690
            // initialize the global global data to import a bunch
691
            $this->setUp();
692
693
            // initialize the lexer instance itself
694
            $lexer = new Lexer($this->getLexerConfig());
695
696
            // initialize the interpreter
697
            $interpreter = new Interpreter();
698
            $interpreter->addObserver(array($this, 'importRow'));
699
700
            // query whether or not we want to use the strict mode
701
            if (!$this->getConfiguration()->isStrictMode()) {
702
                $interpreter->unstrict();
703
            }
704
705
            // log a message that the file has to be imported
706
            $systemLogger->debug(sprintf('Now start importing file %s', $filename));
707
708
            // parse the CSV file to be imported
709
            $lexer->parse($filename, $interpreter);
710
711
            // track the time needed for the import in seconds
712
            $endTime = microtime(true) - $startTime;
713
714
            // clean up the data after importing the bunch
715
            $this->tearDown();
716
717
            // log a message that the file has successfully been imported
718
            $systemLogger->debug(sprintf('Succesfully imported file %s in %f s', $filename, $endTime));
719
720
            // rename flag file, because import has been successfull
721
            rename($inProgressFilename, $importedFilename);
722
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
723
        } catch (\Exception $e) {
724
            // rename the flag file, because import failed and write the stack trace
725
            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...
726
            file_put_contents($failedFilename, $e->__toString());
727
728
            // clean up the data after importing the bunch
729
            $this->tearDown();
730
731
            // re-throw the exception
732
            throw $e;
733
        }
734
    }
735
736
    /**
737
     * This method queries whether or not the passed filename matches
738
     * the pattern, based on the subjects configured prefix.
739
     *
740
     * @param string $filename The filename to match
741
     *
742
     * @return boolean TRUE if the filename matches, else FALSE
743
     */
744
    protected function match($filename)
745
    {
746
747
        // prepare the pattern to query whether the file has to be processed or not
748
        $pattern = sprintf('/^.*\/%s.*\\.csv$/', $this->getConfiguration()->getPrefix());
749
750
        // stop processing, if the filename doesn't match
751
        return (boolean) preg_match($pattern, $filename);
752
    }
753
754
    /**
755
     * Initialize and return the lexer configuration.
756
     *
757
     * @return \Goodby\CSV\Import\Standard\LexerConfig The lexer configuration
758
     */
759
    protected function getLexerConfig()
760
    {
761
762
        // initialize the lexer configuration
763
        $config = new LexerConfig();
764
765
        // query whether or not a delimiter character has been configured
766
        if ($delimiter = $this->getConfiguration()->getDelimiter()) {
767
            $config->setDelimiter($delimiter);
768
        }
769
770
        // query whether or not a custom escape character has been configured
771
        if ($escape = $this->getConfiguration()->getEscape()) {
772
            $config->setEscape($escape);
773
        }
774
775
        // query whether or not a custom enclosure character has been configured
776
        if ($enclosure = $this->getConfiguration()->getEnclosure()) {
777
            $config->setEnclosure($enclosure);
778
        }
779
780
        // query whether or not a custom source charset has been configured
781
        if ($fromCharset = $this->getConfiguration()->getFromCharset()) {
782
            $config->setFromCharset($fromCharset);
783
        }
784
785
        // query whether or not a custom target charset has been configured
786
        if ($toCharset = $this->getConfiguration()->getToCharset()) {
787
            $config->setToCharset($toCharset);
788
        }
789
790
        // return the lexer configuratio
791
        return $config;
792
    }
793
794
    /**
795
     * Imports the passed row into the database. If the import failed, the exception
796
     * will be catched and logged, but the import process will be continued.
797
     *
798
     * @param array $row The row with the data to be imported
799
     *
800
     * @return void
801
     */
802
    public function importRow(array $row)
803
    {
804
805
        // initialize the row
806
        $this->row = $row;
807
808
        // raise the line number and reset the skip row flag
809
        $this->lineNumber++;
810
        $this->skipRow = false;
811
812
        // initialize the headers with the columns from the first line
813
        if (sizeof($this->headers) === 0) {
814
            foreach ($this->row as $value => $key) {
815
                $this->headers[$this->mapAttributeCodeByHeaderMapping($key)] = $value;
816
            }
817
            return;
818
        }
819
820
        // process the observers
821
        foreach ($this->getObservers() as $observers) {
822
            // invoke the pre-import/import and post-import observers
823
            foreach ($observers as $observer) {
824
                // query whether or not we have to skip the row
825
                if ($this->skipRow) {
826
                    break;
827
                }
828
829
                // if not, process the next observer
830
                if ($observer instanceof ObserverInterface) {
831
                    $this->row = $observer->handle($this->row);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TechDivision\Import\Observers\ObserverInterface as the method handle() does only exist in the following implementations of said interface: TechDivision\Import\Obse...stractAttributeObserver, TechDivision\Import\Obse...tractFileUploadObserver, TechDivision\Import\Obse...tionalAttributeObserver, TechDivision\Import\Observers\AttributeSetObserver.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
832
                }
833
            }
834
        }
835
836
        // log a debug message with the actual line nr/file information
837
        $this->getSystemLogger()->debug(
838
            sprintf(
839
                'Successfully processed row (operation: %s) in file %s on line %d',
840
                $this->operationName,
841
                $this->filename,
842
                $this->lineNumber
843
            )
844
        );
845
    }
846
847
    /**
848
     * Map the passed attribute code, if a header mapping exists and return the
849
     * mapped mapping.
850
     *
851
     * @param string $attributeCode The attribute code to map
852
     *
853
     * @return string The mapped attribute code, or the original one
854
     */
855
    public function mapAttributeCodeByHeaderMapping($attributeCode)
856
    {
857
858
        // load the header mappings
859
        $headerMappings = $this->getHeaderMappings();
860
861
        // query weather or not we've a mapping, if yes, map the attribute code
862
        if (isset($headerMappings[$attributeCode])) {
863
            $attributeCode = $headerMappings[$attributeCode];
864
        }
865
866
        // return the (mapped) attribute code
867
        return $attributeCode;
868
    }
869
870
    /**
871
     * Queries whether or not that the subject needs an OK file to be processed.
872
     *
873
     * @return boolean TRUE if the subject needs an OK file, else FALSE
874
     */
875
    public function isOkFileNeeded()
876
    {
877
        return $this->getConfiguration()->isOkFileNeeded();
878
    }
879
880
    /**
881
     * Return's the default store.
882
     *
883
     * @return array The default store
884
     */
885
    public function getDefaultStore()
886
    {
887
        return $this->defaultStore;
888
    }
889
890
    /**
891
     * Set's the store view code the create the product/attributes for.
892
     *
893
     * @param string $storeViewCode The store view code
894
     *
895
     * @return void
896
     */
897
    public function setStoreViewCode($storeViewCode)
898
    {
899
        $this->storeViewCode = $storeViewCode;
900
    }
901
902
    /**
903
     * Return's the store view code the create the product/attributes for.
904
     *
905
     * @param string|null $default The default value to return, if the store view code has not been set
906
     *
907
     * @return string The store view code
908
     */
909
    public function getStoreViewCode($default = null)
910
    {
911
912
        // return the store view code, if available
913
        if ($this->storeViewCode != null) {
914
            return $this->storeViewCode;
915
        }
916
917
        // if NOT and a default code is available
918
        if ($default != null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $default of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
919
            // return the default value
920
            return $default;
921
        }
922
    }
923
924
    /**
925
     * Return's the root category for the actual view store.
926
     *
927
     * @return array The store's root category
928
     * @throws \Exception Is thrown if the root category for the passed store code is NOT available
929
     */
930
    public function getRootCategory()
931
    {
932
933
        // load the default store
934
        $defaultStore = $this->getDefaultStore();
935
936
        // load the actual store view code
937
        $storeViewCode = $this->getStoreViewCode($defaultStore[MemberNames::CODE]);
938
939
        // query weather or not we've a root category or not
940
        if (isset($this->rootCategories[$storeViewCode])) {
941
            return $this->rootCategories[$storeViewCode];
942
        }
943
944
        // throw an exception if the root category is NOT available
945
        throw new \Exception(sprintf('Root category for %s is not available', $storeViewCode));
946
    }
947
948
    /**
949
     * Return's the Magento configuration value.
950
     *
951
     * @param string  $path    The Magento path of the requested configuration value
952
     * @param mixed   $default The default value that has to be returned, if the requested configuration value is not set
953
     * @param string  $scope   The scope the configuration value has been set
954
     * @param integer $scopeId The scope ID the configuration value has been set
955
     *
956
     * @return mixed The configuration value
957
     * @throws \Exception Is thrown, if nor a value can be found or a default value has been passed
958
     */
959
    public function getCoreConfigData($path, $default = null, $scope = ScopeKeys::SCOPE_DEFAULT, $scopeId = 0)
960
    {
961
962
        // initialize the core config data
963
        $coreConfigData = array(
964
            MemberNames::PATH => $path,
965
            MemberNames::SCOPE => $scope,
966
            MemberNames::SCOPE_ID => $scopeId
967
        );
968
969
        // generate the UID from the passed data
970
        $uniqueIdentifier = $this->coreConfigDataUidGenerator->generate($coreConfigData);
971
972
        // iterate over the core config data and try to find the requested configuration value
973
        if (isset($this->coreConfigData[$uniqueIdentifier])) {
974
            return $this->coreConfigData[$uniqueIdentifier][MemberNames::VALUE];
975
        }
976
977
        // query whether or not we've to query for the configuration value on fallback level 'websites' also
978
        if ($scope === ScopeKeys::SCOPE_STORES && isset($this->stores[$scopeId])) {
979
            // replace scope with 'websites' and website ID
980
            $coreConfigData = array_merge(
981
                $coreConfigData,
982
                array(
983
                    MemberNames::SCOPE    => ScopeKeys::SCOPE_WEBSITES,
984
                    MemberNames::SCOPE_ID => $this->stores[$scopeId][MemberNames::WEBSITE_ID]
985
                )
986
            );
987
988
            // generate the UID from the passed data, merged with the 'websites' scope and ID
989
            $uniqueIdentifier = $this->coreConfigDataUidGenerator->generate($coreConfigData);
990
991
            // query whether or not, the configuration value on 'websites' level
992 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...
993
                return $this->coreConfigData[$uniqueIdentifier][MemberNames::VALUE];
994
            }
995
        }
996
997
        // replace scope with 'default' and scope ID '0'
998
        $coreConfigData = array_merge(
999
            $coreConfigData,
1000
            array(
1001
                MemberNames::SCOPE    => ScopeKeys::SCOPE_DEFAULT,
1002
                MemberNames::SCOPE_ID => 0
1003
            )
1004
        );
1005
1006
        // generate the UID from the passed data, merged with the 'default' scope and ID 0
1007
        $uniqueIdentifier = $this->coreConfigDataUidGenerator->generate($coreConfigData);
1008
1009
        // query whether or not, the configuration value on 'default' level
1010 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...
1011
            return $this->coreConfigData[$uniqueIdentifier][MemberNames::VALUE];
1012
        }
1013
1014
        // if not, return the passed default value
1015
        if ($default !== null) {
1016
            return $default;
1017
        }
1018
1019
        // throw an exception if no value can be found
1020
        // in the Magento configuration
1021
        throw new \Exception(
1022
            sprintf(
1023
                'Can\'t find a value for configuration "%s-%s-%d" in "core_config_data"',
1024
                $path,
1025
                $scope,
1026
                $scopeId
1027
            )
1028
        );
1029
    }
1030
1031
    /**
1032
     * Resolve the original column name for the passed one.
1033
     *
1034
     * @param string $columnName The column name that has to be resolved
1035
     *
1036
     * @return string|null The original column name
1037
     */
1038
    public function resolveOriginalColumnName($columnName)
1039
    {
1040
1041
        // try to load the original data
1042
        $originalData = $this->getOriginalData();
1043
1044
        // query whether or not original data is available
1045
        if (isset($originalData[ColumnKeys::ORIGINAL_COLUMN_NAMES])) {
1046
            // query whether or not the original column name is available
1047
            if (isset($originalData[ColumnKeys::ORIGINAL_COLUMN_NAMES][$columnName])) {
1048
                return $originalData[ColumnKeys::ORIGINAL_COLUMN_NAMES][$columnName];
1049
            }
1050
1051
            // query whether or a wildcard column name is available
1052
            if (isset($originalData[ColumnKeys::ORIGINAL_COLUMN_NAMES]['*'])) {
1053
                return $originalData[ColumnKeys::ORIGINAL_COLUMN_NAMES]['*'];
1054
            }
1055
        }
1056
    }
1057
1058
    /**
1059
     * Return's the original data if available, or an empty array.
1060
     *
1061
     * @return array The original data
1062
     */
1063
    public function getOriginalData()
1064
    {
1065
1066
        // initialize the array for the original data
1067
        $originalData = array();
1068
1069
        // query whether or not the column contains original data
1070
        if ($this->hasOriginalData()) {
1071
            // unerialize the original data from the column
1072
            $originalData = unserialize($this->row[$this->headers[ColumnKeys::ORIGINAL_DATA]]);
1073
        }
1074
1075
        // return an empty array, if not
1076
        return $originalData;
1077
    }
1078
1079
    /**
1080
     * Query's whether or not the actual column contains original data like
1081
     * filename, line number and column names.
1082
     *
1083
     * @return boolean TRUE if the actual column contains origin data, else FALSE
1084
     */
1085
    public function hasOriginalData()
1086
    {
1087
        return isset($this->headers[ColumnKeys::ORIGINAL_DATA]) && isset($this->row[$this->headers[ColumnKeys::ORIGINAL_DATA]]);
1088
    }
1089
1090
    /**
1091
     * Wraps the passed exeception into a new one by trying to resolve the original filname,
1092
     * line number and column names and use it for a detailed exception message.
1093
     *
1094
     * @param array      $columnNames The column names that should be resolved and wrapped
1095
     * @param \Exception $parent      The exception we want to wrap
1096
     * @param string     $className   The class name of the exception type we want to wrap the parent one
1097
     *
1098
     * @return \Exception the wrapped exception
1099
     */
1100
    public function wrapException(
1101
        array $columnNames,
1102
        \Exception $parent = null,
1103
        $className = '\TechDivision\Import\Exceptions\WrappedColumnException'
1104
    ) {
1105
1106
        // query whether or not has been a result of invalid data of a previous column of a CSV file
1107
        if ($this->hasOriginalData()) {
1108
            // load the original data
1109
            $originalData = $this->getOriginalData();
1110
1111
            // replace old filename and line number with original data
1112
            $message = $this->appendExceptionSuffix(
1113
                $this->stripExceptionSuffix($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...
1114
                $originalData[ColumnKeys::ORIGINAL_FILENAME],
1115
                $originalData[ColumnKeys::ORIGINAL_LINE_NUMBER]
1116
            );
1117
1118
            // prepare the original column names
1119
            $originalColumnNames = array();
1120
            foreach ($columnNames as $columnName) {
1121
                $originalColumnNames = $this->resolveOriginalColumnName($columnName);
1122
            }
1123
1124
            // append the column information
1125
            $message = sprintf('%s in column(s) %s', $message, implode(', ', $originalColumnNames));
1126
1127
            // create a new exception and wrap the parent one
1128
            return new $className($message, null, $parent);
1129
        }
1130
1131
        // simply return the parent exception, because
1132
        // we can't find any original information
1133
        return $parent;
1134
    }
1135
1136
    /**
1137
     * Strip's the exception suffix containing filename and line number from the
1138
     * passed message.
1139
     *
1140
     * @param string $message The message to strip the exception suffix from
1141
     *
1142
     * @return mixed The message without the exception suffix
1143
     */
1144
    public function stripExceptionSuffix($message)
1145
    {
1146
        return str_replace($this->appendExceptionSuffix(), '', $message);
1147
    }
1148
1149
    /**
1150
     * Append's the exception suffix containing filename and line number to the
1151
     * passed message. If no message has been passed, only the suffix will be
1152
     * returned
1153
     *
1154
     * @param string|null $message    The message to append the exception suffix to
1155
     * @param string|null $filename   The filename used to create the suffix
1156
     * @param string|null $lineNumber The line number used to create the suffx
1157
     *
1158
     * @return string The message with the appended exception suffix
1159
     */
1160
    public function appendExceptionSuffix($message = null, $filename = null, $lineNumber = null)
1161
    {
1162
1163
        // query whether or not a filename has been passed
1164
        if ($filename === null) {
1165
            $filename = $this->getFilename();
1166
        }
1167
1168
        // query whether or not a line number has been passed
1169
        if ($lineNumber === null) {
1170
            $lineNumber = $this->getLineNumber();
1171
        }
1172
1173
        // if no message has been passed, only return the suffix
1174
        if ($message === null) {
1175
            return sprintf(' in file %s on line %d', $filename, $lineNumber);
1176
        }
1177
1178
        // concatenate the message with the suffix and return it
1179
        return sprintf('%s in file %s on line %d', $message, $filename, $lineNumber);
1180
    }
1181
}
1182