Completed
Pull Request — master (#45)
by Tim
03:20
created

AbstractSubject::getObservers()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 4
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 2
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\MemberNames;
28
use TechDivision\Import\Utils\RegistryKeys;
29
use TechDivision\Import\Services\RegistryProcessor;
30
use TechDivision\Import\Callbacks\CallbackVisitor;
31
use TechDivision\Import\Callbacks\CallbackInterface;
32
use TechDivision\Import\Observers\ObserverVisitor;
33
use TechDivision\Import\Observers\ObserverInterface;
34
use TechDivision\Import\Services\RegistryProcessorInterface;
35
use TechDivision\Import\Configuration\SubjectConfigurationInterface;
36
use TechDivision\Import\Utils\ScopeKeys;
37
use TechDivision\Import\Utils\Generators\GeneratorInterface;
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 system configuration.
60
     *
61
     * @var \TechDivision\Import\Configuration\SubjectConfigurationInterface
62
     */
63
    protected $configuration;
64
65
    /**
66
     * The system logger implementation.
67
     *
68
     * @var \Psr\Log\LoggerInterface
69
     */
70
    protected $systemLogger;
71
72
    /**
73
     * The RegistryProcessor instance to handle running threads.
74
     *
75
     * @var \TechDivision\Import\Services\RegistryProcessorInterface
76
     */
77
    protected $registryProcessor;
78
79
    /**
80
     * The actions unique serial.
81
     *
82
     * @var string
83
     */
84
    protected $serial;
85
86
    /**
87
     * The name of the file to be imported.
88
     *
89
     * @var string
90
     */
91
    protected $filename;
92
93
    /**
94
     * Array with the subject's observers.
95
     *
96
     * @var array
97
     */
98
    protected $observers = array();
99
100
    /**
101
     * Array with the subject's callbacks.
102
     *
103
     * @var array
104
     */
105
    protected $callbacks = array();
106
107
    /**
108
     * The subject's callback mappings.
109
     *
110
     * @var array
111
     */
112
    protected $callbackMappings = array();
113
114
    /**
115
     * Contain's the column names from the header line.
116
     *
117
     * @var array
118
     */
119
    protected $headers = array();
120
121
    /**
122
     * The actual line number.
123
     *
124
     * @var integer
125
     */
126
    protected $lineNumber = 0;
127
128
    /**
129
     * The actual operation name.
130
     *
131
     * @var string
132
     */
133
    protected $operationName ;
134
135
    /**
136
     * The flag that stop's overserver execution on the actual row.
137
     *
138
     * @var boolean
139
     */
140
    protected $skipRow = false;
141
142
    /**
143
     * The available root categories.
144
     *
145
     * @var array
146
     */
147
    protected $rootCategories = array();
148
149
    /**
150
     * The Magento configuration.
151
     *
152
     * @var array
153
     */
154
    protected $coreConfigData = array();
155
156
    /**
157
     * The available stores.
158
     *
159
     * @var array
160
     */
161
    protected $stores = array();
162
163
    /**
164
     * The default store.
165
     *
166
     * @var array
167
     */
168
    protected $defaultStore;
169
170
    /**
171
     * The store view code the create the product/attributes for.
172
     *
173
     * @var string
174
     */
175
    protected $storeViewCode;
176
177
    /**
178
     * The UID generator for the core config data.
179
     *
180
     * @var \TechDivision\Import\Utils\Generators\GeneratorInterface
181
     */
182
    protected $coreConfigDataUidGenerator;
183
184
    /**
185
     * Initialize the subject instance.
186
     *
187
     * @param \Psr\Log\LoggerInterface                                         $systemLogger               The system logger instance
188
     * @param \TechDivision\Import\Configuration\SubjectConfigurationInterface $configuration              The subject configuration instance
189
     * @param \TechDivision\Import\Services\RegistryProcessorInterface         $registryProcessor          The registry processor instance
190
     * @param \TechDivision\Import\Utils\Generators\GeneratorInterface         $coreConfigDataUidGenerator The UID generator for the core config data
191
     */
192
    public function __construct(
193
        LoggerInterface $systemLogger,
194
        SubjectConfigurationInterface $configuration,
195
        RegistryProcessorInterface $registryProcessor,
196
        GeneratorInterface $coreConfigDataUidGenerator
197
    ) {
198
        $this->systemLogger = $systemLogger;
199
        $this->configuration = $configuration;
200
        $this->registryProcessor = $registryProcessor;
201
        $this->coreConfigDataUidGenerator = $coreConfigDataUidGenerator;
202
    }
203
204
    /**
205
     * Return's the default callback mappings.
206
     *
207
     * @return array The default callback mappings
208
     */
209
    public function getDefaultCallbackMappings()
210
    {
211
        return array();
212
    }
213
214
    /**
215
     * Stop's observer execution on the actual row.
216
     *
217
     * @return void
218
     */
219
    public function skipRow()
220
    {
221
        $this->skipRow = true;
222
    }
223
224
    /**
225
     * Return's the actual line number.
226
     *
227
     * @return integer The line number
228
     */
229
    public function getLineNumber()
230
    {
231
        return $this->lineNumber;
232
    }
233
234
    /**
235
     * Return's the actual operation name.
236
     *
237
     * @return string
238
     */
239
    public function getOperationName()
240
    {
241
        return $this->operationName;
242
    }
243
244
    /**
245
     * Set's the array containing header row.
246
     *
247
     * @param array $headers The array with the header row
248
     *
249
     * @return void
250
     */
251
    public function setHeaders(array $headers)
252
    {
253
        $this->headers = $headers;
254
    }
255
256
    /**
257
     * Return's the array containing header row.
258
     *
259
     * @return array The array with the header row
260
     */
261
    public function getHeaders()
262
    {
263
        return $this->headers;
264
    }
265
266
    /**
267
     * Queries whether or not the header with the passed name is available.
268
     *
269
     * @param string $name The header name to query
270
     *
271
     * @return boolean TRUE if the header is available, else FALSE
272
     */
273
    public function hasHeader($name)
274
    {
275
        return isset($this->headers[$name]);
276
    }
277
278
    /**
279
     * Return's the header value for the passed name.
280
     *
281
     * @param string $name The name of the header to return the value for
282
     *
283
     * @return mixed The header value
284
     * \InvalidArgumentException Is thrown, if the header with the passed name is NOT available
285
     */
286
    public function getHeader($name)
287
    {
288
289
        // query whether or not, the header is available
290
        if (isset($this->headers[$name])) {
291
            return $this->headers[$name];
292
        }
293
294
        // throw an exception, if not
295
        throw new \InvalidArgumentException(sprintf('Header %s is not available', $name));
296
    }
297
298
    /**
299
     * Add's the header with the passed name and position, if not NULL.
300
     *
301
     * @param string $name The header name to add
302
     *
303
     * @return integer The new headers position
304
     */
305
    public function addHeader($name)
306
    {
307
308
        // add the header
309
        $this->headers[$name] = $position = sizeof($this->headers);
310
311
        // return the new header's position
312
        return $position;
313
    }
314
315
    /**
316
     * Queries whether or not debug mode is enabled or not, default is TRUE.
317
     *
318
     * @return boolean TRUE if debug mode is enabled, else FALSE
319
     */
320
    public function isDebugMode()
321
    {
322
        return $this->getConfiguration()->isDebugMode();
323
    }
324
325
    /**
326
     * Return's the system configuration.
327
     *
328
     * @return \TechDivision\Import\Configuration\SubjectConfigurationInterface The system configuration
329
     */
330
    public function getConfiguration()
331
    {
332
        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...
333
    }
334
335
    /**
336
     * Return's the system logger.
337
     *
338
     * @return \Psr\Log\LoggerInterface The system logger instance
339
     */
340
    public function getSystemLogger()
341
    {
342
        return $this->systemLogger;
343
    }
344
345
    /**
346
     * Return's the RegistryProcessor instance to handle the running threads.
347
     *
348
     * @return \TechDivision\Import\Services\RegistryProcessorInterface The registry processor instance
349
     */
350
    public function getRegistryProcessor()
351
    {
352
        return $this->registryProcessor;
353
    }
354
355
    /**
356
     * Set's the unique serial for this import process.
357
     *
358
     * @param string $serial The unique serial
359
     *
360
     * @return void
361
     */
362
    public function setSerial($serial)
363
    {
364
        $this->serial = $serial;
365
    }
366
367
    /**
368
     * Return's the unique serial for this import process.
369
     *
370
     * @return string The unique serial
371
     */
372
    public function getSerial()
373
    {
374
        return $this->serial;
375
    }
376
377
    /**
378
     * Set's the name of the file to import
379
     *
380
     * @param string $filename The filename
381
     *
382
     * @return void
383
     */
384
    public function setFilename($filename)
385
    {
386
        $this->filename = $filename;
387
    }
388
389
    /**
390
     * Return's the name of the file to import.
391
     *
392
     * @return string The filename
393
     */
394
    public function getFilename()
395
    {
396
        return $this->filename;
397
    }
398
399
    /**
400
     * Return's the source date format to use.
401
     *
402
     * @return string The source date format
403
     */
404
    public function getSourceDateFormat()
405
    {
406
        return $this->getConfiguration()->getSourceDateFormat();
407
    }
408
409
    /**
410
     * Return's the multiple field delimiter character to use, default value is comma (,).
411
     *
412
     * @return string The multiple field delimiter character
413
     */
414
    public function getMultipleFieldDelimiter()
415
    {
416
        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...
417
    }
418
419
    /**
420
     * Return's the initialized PDO connection.
421
     *
422
     * @return \PDO The initialized PDO connection
423
     */
424
    public function getConnection()
425
    {
426
        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...
427
    }
428
429
    /**
430
     * Intializes the previously loaded global data for exactly one bunch.
431
     *
432
     * @return void
433
     * @see \Importer\Csv\Actions\ProductImportAction::prepare()
434
     */
435
    public function setUp()
436
    {
437
438
        // load the status of the actual import
439
        $status = $this->getRegistryProcessor()->getAttribute($this->getSerial());
440
441
        // load the global data we've prepared initially
442
        $this->stores = $status[RegistryKeys::GLOBAL_DATA][RegistryKeys::STORES];
443
        $this->defaultStore = $status[RegistryKeys::GLOBAL_DATA][RegistryKeys::DEFAULT_STORE];
444
        $this->rootCategories = $status[RegistryKeys::GLOBAL_DATA][RegistryKeys::ROOT_CATEGORIES];
445
        $this->coreConfigData = $status[RegistryKeys::GLOBAL_DATA][RegistryKeys::CORE_CONFIG_DATA];
446
447
        // initialize the operation name
448
        $this->operationName = $this->getConfiguration()->getConfiguration()->getOperationName();
449
450
        // merge the callback mappings with the mappings from the child instance
451
        $this->callbackMappings = array_merge($this->callbackMappings, $this->getDefaultCallbackMappings());
452
453
        // merge the callback mappings the the one from the configuration file
454
        foreach ($this->getConfiguration()->getCallbacks() as $callbackMappings) {
455
            foreach ($callbackMappings as $attributeCode => $mappings) {
456
                // write a log message, that default callback configuration will
457
                // be overwritten with the one from the configuration file
458
                if (isset($this->callbackMappings[$attributeCode])) {
459
                    $this->getSystemLogger()->notice(
460
                        sprintf('Now override callback mappings for attribute %s with values found in configuration file', $attributeCode)
461
                    );
462
                }
463
464
                // override the attributes callbacks
465
                $this->callbackMappings[$attributeCode] = $mappings;
466
            }
467
        }
468
469
        // initialize the callbacks/observers
470
        CallbackVisitor::get()->visit($this);
471
        ObserverVisitor::get()->visit($this);
472
    }
473
474
    /**
475
     * Clean up the global data after importing the variants.
476
     *
477
     * @return void
478
     */
479
    public function tearDown()
480
    {
481
482
        // load the registry processor
483
        $registryProcessor = $this->getRegistryProcessor();
484
485
        // update the source directory for the next subject
486
        $registryProcessor->mergeAttributesRecursive(
487
            $this->getSerial(),
488
            array(RegistryKeys::SOURCE_DIRECTORY => $this->getNewSourceDir())
489
        );
490
491
        // log a debug message with the new source directory
492
        $this->getSystemLogger()->debug(
493
            sprintf('Subject %s successfully updated source directory to %s', __CLASS__, $this->getNewSourceDir())
494
        );
495
    }
496
497
    /**
498
     * Return's the next source directory, which will be the target directory
499
     * of this subject, in most cases.
500
     *
501
     * @return string The new source directory
502
     */
503
    protected function getNewSourceDir()
504
    {
505
        return sprintf('%s/%s', $this->getConfiguration()->getTargetDir(), $this->getSerial());
506
    }
507
508
    /**
509
     * Register the passed observer with the specific type.
510
     *
511
     * @param \TechDivision\Import\Observers\ObserverInterface $observer The observer to register
512
     * @param string                                           $type     The type to register the observer with
513
     *
514
     * @return void
515
     */
516
    public function registerObserver(ObserverInterface $observer, $type)
517
    {
518
519
        // query whether or not the array with the callbacks for the
520
        // passed type has already been initialized, or not
521
        if (!isset($this->observers[$type])) {
522
            $this->observers[$type] = array();
523
        }
524
525
        // append the callback with the instance of the passed type
526
        $this->observers[$type][] = $observer;
527
    }
528
529
    /**
530
     * Register the passed callback with the specific type.
531
     *
532
     * @param \TechDivision\Import\Callbacks\CallbackInterface $callback The subject to register the callbacks for
533
     * @param string                                           $type     The type to register the callback with
534
     *
535
     * @return void
536
     */
537
    public function registerCallback(CallbackInterface $callback, $type)
538
    {
539
540
        // query whether or not the array with the callbacks for the
541
        // passed type has already been initialized, or not
542
        if (!isset($this->callbacks[$type])) {
543
            $this->callbacks[$type] = array();
544
        }
545
546
        // append the callback with the instance of the passed type
547
        $this->callbacks[$type][] = $callback;
548
    }
549
550
    /**
551
     * Return's the array with callbacks for the passed type.
552
     *
553
     * @param string $type The type of the callbacks to return
554
     *
555
     * @return array The callbacks
556
     */
557
    public function getCallbacksByType($type)
558
    {
559
560
        // initialize the array for the callbacks
561
        $callbacks = array();
562
563
        // query whether or not callbacks for the type are available
564
        if (isset($this->callbacks[$type])) {
565
            $callbacks = $this->callbacks[$type];
566
        }
567
568
        // return the array with the type's callbacks
569
        return $callbacks;
570
    }
571
572
    /**
573
     * Return's the array with the available observers.
574
     *
575
     * @return array The observers
576
     */
577
    public function getObservers()
578
    {
579
        return $this->observers;
580
    }
581
582
    /**
583
     * Return's the array with the available callbacks.
584
     *
585
     * @return array The callbacks
586
     */
587
    public function getCallbacks()
588
    {
589
        return $this->callbacks;
590
    }
591
592
    /**
593
     * Return's the callback mappings for this subject.
594
     *
595
     * @return array The array with the subject's callback mappings
596
     */
597
    public function getCallbackMappings()
598
    {
599
        return $this->callbackMappings;
600
    }
601
602
    /**
603
     * Imports the content of the file with the passed filename.
604
     *
605
     * @param string $serial   The unique process serial
606
     * @param string $filename The filename to process
607
     *
608
     * @return void
609
     * @throws \Exception Is thrown, if the import can't be processed
610
     */
611
    public function import($serial, $filename)
612
    {
613
614
        try {
615
            // stop processing, if the filename doesn't match
616
            if (!$this->match($filename)) {
617
                return;
618
            }
619
620
            // load the system logger instance
621
            $systemLogger = $this->getSystemLogger();
622
623
            // prepare the flag filenames
624
            $inProgressFilename = sprintf('%s.inProgress', $filename);
625
            $importedFilename = sprintf('%s.imported', $filename);
626
            $failedFilename = sprintf('%s.failed', $filename);
627
628
            // query whether or not the file has already been imported
629
            if (is_file($failedFilename) ||
630
                is_file($importedFilename) ||
631
                is_file($inProgressFilename)
632
            ) {
633
                // log a debug message and exit
634
                $systemLogger->debug(sprintf('Import running, found inProgress file %s', $inProgressFilename));
635
                return;
636
            }
637
638
            // flag file as in progress
639
            touch($inProgressFilename);
640
641
            // track the start time
642
            $startTime = microtime(true);
643
644
            // initialize serial and filename
645
            $this->setSerial($serial);
646
            $this->setFilename($filename);
647
648
            // load the system logger
649
            $systemLogger = $this->getSystemLogger();
650
651
            // initialize the global global data to import a bunch
652
            $this->setUp();
653
654
            // initialize the lexer instance itself
655
            $lexer = new Lexer($this->getLexerConfig());
656
657
            // initialize the interpreter
658
            $interpreter = new Interpreter();
659
            $interpreter->addObserver(array($this, 'importRow'));
660
661
            // query whether or not we want to use the strict mode
662
            if (!$this->getConfiguration()->isStrictMode()) {
663
                $interpreter->unstrict();
664
            }
665
666
            // log a message that the file has to be imported
667
            $systemLogger->debug(sprintf('Now start importing file %s', $filename));
668
669
            // parse the CSV file to be imported
670
            $lexer->parse($filename, $interpreter);
671
672
            // track the time needed for the import in seconds
673
            $endTime = microtime(true) - $startTime;
674
675
            // clean up the data after importing the bunch
676
            $this->tearDown();
677
678
            // log a message that the file has successfully been imported
679
            $systemLogger->debug(sprintf('Succesfully imported file %s in %f s', $filename, $endTime));
680
681
            // rename flag file, because import has been successfull
682
            rename($inProgressFilename, $importedFilename);
683
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
684
        } catch (\Exception $e) {
685
            // rename the flag file, because import failed and write the stack trace
686
            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...
687
            file_put_contents($failedFilename, $e->__toString());
688
689
            // clean up the data after importing the bunch
690
            $this->tearDown();
691
692
            // re-throw the exception
693
            throw $e;
694
        }
695
    }
696
697
    /**
698
     * This method queries whether or not the passed filename matches
699
     * the pattern, based on the subjects configured prefix.
700
     *
701
     * @param string $filename The filename to match
702
     *
703
     * @return boolean TRUE if the filename matches, else FALSE
704
     */
705
    protected function match($filename)
706
    {
707
708
        // prepare the pattern to query whether the file has to be processed or not
709
        $pattern = sprintf('/^.*\/%s.*\\.csv$/', $this->getConfiguration()->getPrefix());
710
711
        // stop processing, if the filename doesn't match
712
        return (boolean) preg_match($pattern, $filename);
713
    }
714
715
    /**
716
     * Initialize and return the lexer configuration.
717
     *
718
     * @return \Goodby\CSV\Import\Standard\LexerConfig The lexer configuration
719
     */
720
    protected function getLexerConfig()
721
    {
722
723
        // initialize the lexer configuration
724
        $config = new LexerConfig();
725
726
        // query whether or not a delimiter character has been configured
727
        if ($delimiter = $this->getConfiguration()->getDelimiter()) {
728
            $config->setDelimiter($delimiter);
729
        }
730
731
        // query whether or not a custom escape character has been configured
732
        if ($escape = $this->getConfiguration()->getEscape()) {
733
            $config->setEscape($escape);
734
        }
735
736
        // query whether or not a custom enclosure character has been configured
737
        if ($enclosure = $this->getConfiguration()->getEnclosure()) {
738
            $config->setEnclosure($enclosure);
739
        }
740
741
        // query whether or not a custom source charset has been configured
742
        if ($fromCharset = $this->getConfiguration()->getFromCharset()) {
743
            $config->setFromCharset($fromCharset);
744
        }
745
746
        // query whether or not a custom target charset has been configured
747
        if ($toCharset = $this->getConfiguration()->getToCharset()) {
748
            $config->setToCharset($toCharset);
749
        }
750
751
        // return the lexer configuratio
752
        return $config;
753
    }
754
755
    /**
756
     * Imports the passed row into the database.
757
     *
758
     * If the import failed, the exception will be catched and logged,
759
     * but the import process will be continued.
760
     *
761
     * @param array $row The row with the data to be imported
762
     *
763
     * @return void
764
     */
765
    public function importRow(array $row)
766
    {
767
768
        // raise the line number and reset the skip row flag
769
        $this->lineNumber++;
770
        $this->skipRow = false;
771
772
        // initialize the headers with the columns from the first line
773
        if (sizeof($this->headers) === 0) {
774
            foreach ($row as $value => $key) {
775
                $this->headers[$this->mapAttributeCodeByHeaderMapping($key)] = $value;
776
            }
777
            return;
778
        }
779
780
        // process the observers
781
        foreach ($this->getObservers() as $observers) {
782
            // invoke the pre-import/import and post-import observers
783
            foreach ($observers as $observer) {
784
                // query whether or not we have to skip the row
785
                if ($this->skipRow) {
786
                    break;
787
                }
788
                // if not, process the next observer
789
                if ($observer instanceof ObserverInterface) {
790
                    $row = $observer->handle($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...
791
                }
792
            }
793
        }
794
795
        // log a debug message with the actual line nr/file information
796
        $this->getSystemLogger()->debug(
797
            sprintf(
798
                'Successfully processed row (operation: %s) in file %s on line %d',
799
                $this->operationName,
800
                $this->filename,
801
                $this->lineNumber
802
            )
803
        );
804
    }
805
806
    /**
807
     * Map the passed attribute code, if a header mapping exists and return the
808
     * mapped mapping.
809
     *
810
     * @param string $attributeCode The attribute code to map
811
     *
812
     * @return string The mapped attribute code, or the original one
813
     */
814
    public function mapAttributeCodeByHeaderMapping($attributeCode)
815
    {
816
817
        // load the header mappings
818
        $headerMappings = $this->getHeaderMappings();
819
820
        // query weather or not we've a mapping, if yes, map the attribute code
821
        if (isset($headerMappings[$attributeCode])) {
822
            $attributeCode = $headerMappings[$attributeCode];
823
        }
824
825
        // return the (mapped) attribute code
826
        return $attributeCode;
827
    }
828
829
    /**
830
     * Queries whether or not that the subject needs an OK file to be processed.
831
     *
832
     * @return boolean TRUE if the subject needs an OK file, else FALSE
833
     */
834
    public function isOkFileNeeded()
835
    {
836
        return $this->getConfiguration()->isOkFileNeeded();
837
    }
838
839
    /**
840
     * Return's the default store.
841
     *
842
     * @return array The default store
843
     */
844
    public function getDefaultStore()
845
    {
846
        return $this->defaultStore;
847
    }
848
849
    /**
850
     * Set's the store view code the create the product/attributes for.
851
     *
852
     * @param string $storeViewCode The store view code
853
     *
854
     * @return void
855
     */
856
    public function setStoreViewCode($storeViewCode)
857
    {
858
        $this->storeViewCode = $storeViewCode;
859
    }
860
861
    /**
862
     * Return's the store view code the create the product/attributes for.
863
     *
864
     * @param string|null $default The default value to return, if the store view code has not been set
865
     *
866
     * @return string The store view code
867
     */
868
    public function getStoreViewCode($default = null)
869
    {
870
871
        // return the store view code, if available
872
        if ($this->storeViewCode != null) {
873
            return $this->storeViewCode;
874
        }
875
876
        // if NOT and a default code is available
877
        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...
878
            // return the default value
879
            return $default;
880
        }
881
    }
882
883
    /**
884
     * Return's the root category for the actual view store.
885
     *
886
     * @return array The store's root category
887
     * @throws \Exception Is thrown if the root category for the passed store code is NOT available
888
     */
889
    public function getRootCategory()
890
    {
891
892
        // load the default store
893
        $defaultStore = $this->getDefaultStore();
894
895
        // load the actual store view code
896
        $storeViewCode = $this->getStoreViewCode($defaultStore[MemberNames::CODE]);
897
898
        // query weather or not we've a root category or not
899
        if (isset($this->rootCategories[$storeViewCode])) {
900
            return $this->rootCategories[$storeViewCode];
901
        }
902
903
        // throw an exception if the root category is NOT available
904
        throw new \Exception(sprintf('Root category for %s is not available', $storeViewCode));
905
    }
906
907
    /**
908
     * Return's the Magento configuration value.
909
     *
910
     * @param string  $path    The Magento path of the requested configuration value
911
     * @param mixed   $default The default value that has to be returned, if the requested configuration value is not set
912
     * @param string  $scope   The scope the configuration value has been set
913
     * @param integer $scopeId The scope ID the configuration value has been set
914
     *
915
     * @return mixed The configuration value
916
     * @throws \Exception Is thrown, if nor a value can be found or a default value has been passed
917
     */
918
    public function getCoreConfigData($path, $default = null, $scope = ScopeKeys::SCOPE_DEFAULT, $scopeId = 0)
919
    {
920
921
        // initialize the core config data
922
        $coreConfigData = array(
923
            MemberNames::PATH => $path,
924
            MemberNames::SCOPE => $scope,
925
            MemberNames::SCOPE_ID => $scopeId
926
        );
927
928
        // generate the UID from the passed data
929
        $uniqueIdentifier = $this->coreConfigDataUidGenerator->generate($coreConfigData);
930
931
        // iterate over the core config data and try to find the requested configuration value
932
        if (isset($this->coreConfigData[$uniqueIdentifier])) {
933
            return $this->coreConfigData[$uniqueIdentifier][MemberNames::VALUE];
934
        }
935
936
        // query whether or not we've to query for the configuration value on fallback level 'websites' also
937
        if ($scope === ScopeKeys::SCOPE_STORES && isset($this->stores[$scopeId])) {
938
            // replace scope with 'websites' and website ID
939
            $coreConfigData = array_merge(
940
                $coreConfigData,
941
                array(
942
                    MemberNames::SCOPE    => ScopeKeys::SCOPE_WEBSITES,
943
                    MemberNames::SCOPE_ID => $this->stores[$scopeId][MemberNames::WEBSITE_ID]
944
                )
945
            );
946
947
            // generate the UID from the passed data, merged with the 'websites' scope and ID
948
            $uniqueIdentifier = $this->coreConfigDataUidGenerator->generate($coreConfigData);
949
950
            // query whether or not, the configuration value on 'websites' level
951 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...
952
                return $this->coreConfigData[$uniqueIdentifier][MemberNames::VALUE];
953
            }
954
        }
955
956
        // replace scope with 'default' and scope ID '0'
957
        $coreConfigData = array_merge(
958
            $coreConfigData,
959
            array(
960
                MemberNames::SCOPE    => ScopeKeys::SCOPE_DEFAULT,
961
                MemberNames::SCOPE_ID => 0
962
            )
963
        );
964
965
        // generate the UID from the passed data, merged with the 'default' scope and ID 0
966
        $uniqueIdentifier = $this->coreConfigDataUidGenerator->generate($coreConfigData);
967
968
        // query whether or not, the configuration value on 'default' level
969 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...
970
            return $this->coreConfigData[$uniqueIdentifier][MemberNames::VALUE];
971
        }
972
973
        // if not, return the passed default value
974
        if ($default !== null) {
975
            return $default;
976
        }
977
978
        // throw an exception if no value can be found
979
        // in the Magento configuration
980
        throw new \Exception(
981
            sprintf(
982
                'Can\'t find a value for configuration "%s-%s-%d" in "core_config_data"',
983
                $path,
984
                $scope,
985
                $scopeId
986
            )
987
        );
988
    }
989
}
990