Completed
Pull Request — master (#48)
by Tim
02:56
created

Simple::setUp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 11
Bugs 1 Features 2
Metric Value
c 11
b 1
f 2
dl 0
loc 3
ccs 0
cts 2
cp 0
rs 10
cc 1
eloc 1
nc 1
nop 0
crap 2
1
<?php
2
3
/**
4
 * TechDivision\Import\Cli\Simple
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-cli-simple
18
 * @link      http://www.techdivision.com
19
 */
20
21
namespace TechDivision\Import\Cli;
22
23
use Rhumsaa\Uuid\Uuid;
24
use Monolog\Logger;
25
use Psr\Log\LogLevel;
26
use Psr\Log\LoggerInterface;
27
use Symfony\Component\Console\Input\InputInterface;
28
use Symfony\Component\Console\Output\OutputInterface;
29
use Symfony\Component\Console\Helper\FormatterHelper;
30
use TechDivision\Import\Utils\BunchKeys;
31
use TechDivision\Import\Utils\MemberNames;
32
use TechDivision\Import\Utils\RegistryKeys;
33
use TechDivision\Import\ConfigurationInterface;
34
use TechDivision\Import\Subjects\SubjectInterface;
35
use TechDivision\Import\Services\ImportProcessorInterface;
36
use TechDivision\Import\Services\RegistryProcessorInterface;
37
use TechDivision\Import\Subjects\ExportableSubjectInterface;
38
use TechDivision\Import\Cli\Exceptions\LineNotFoundException;
39
use TechDivision\Import\Cli\Exceptions\FileNotFoundException;
40
41
/**
42
 * The M2IF - Console Tool implementation.
43
 *
44
 * This is a example console tool implementation that should give developers an impression
45
 * on how the M2IF could be used to implement their own Magento 2 importer.
46
 *
47
 * @author    Tim Wagner <[email protected]>
48
 * @copyright 2016 TechDivision GmbH <[email protected]>
49
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
50
 * @link      https://github.com/techdivision/import-cli-simple
51
 * @link      http://www.techdivision.com
52
 */
53
class Simple
54
{
55
56
    /**
57
     * The default style to write messages to the symfony console.
58
     *
59
     * @var string
60
     */
61
    const DEFAULT_STYLE = 'info';
62
63
    /**
64
     * The TechDivision company name as ANSI art.
65
     *
66
     * @var string
67
     */
68
    protected $ansiArt = ' _______        _     _____  _       _     _
69
|__   __|      | |   |  __ \(_)     (_)   (_)
70
   | | ___  ___| |__ | |  | |___   ___ ___ _  ___  _ __
71
   | |/ _ \/ __| \'_ \| |  | | \ \ / / / __| |/ _ \| \'_ \
72
   | |  __/ (__| | | | |__| | |\ V /| \__ \ | (_) | | | |
73
   |_|\___|\___|_| |_|_____/|_| \_/ |_|___/_|\___/|_| |_|
74
';
75
76
    /**
77
     * The log level => console style mapping.
78
     *
79
     * @var array
80
     */
81
    protected $logLevelStyleMapping = array(
82
        LogLevel::INFO      => 'info',
83
        LogLevel::DEBUG     => 'comment',
84
        LogLevel::ERROR     => 'error',
85
        LogLevel::ALERT     => 'error',
86
        LogLevel::CRITICAL  => 'error',
87
        LogLevel::EMERGENCY => 'error',
88
        LogLevel::WARNING   => 'error',
89
        LogLevel::NOTICE    => 'info'
90
    );
91
92
    /**
93
     * The actions unique serial.
94
     *
95
     * @var string
96
     */
97
    protected $serial;
98
99
    /**
100
     * The system logger implementation.
101
     *
102
     * @var \Psr\Log\LoggerInterface
103
     */
104
    protected $systemLogger;
105
106
    /**
107
     * The RegistryProcessor instance to handle running threads.
108
     *
109
     * @var \TechDivision\Import\Services\RegistryProcessorInterface
110
     */
111
    protected $registryProcessor;
112
113
    /**
114
     * The processor to read/write the necessary import data.
115
     *
116
     * @var \TechDivision\Import\Services\ImportProcessorInterface
117
     */
118
    protected $importProcessor;
119
120
    /**
121
     * The system configuration.
122
     *
123
     * @var \TechDivision\Import\ConfigurationInterface
124
     */
125
    protected $configuration;
126
127
    /**
128
     * The input stream to read console information from.
129
     *
130
     * @var \Symfony\Component\Console\Input\InputInterface
131
     */
132
    protected $input;
133
134
    /**
135
     * The output stream to write console information to.
136
     *
137
     * @var \Symfony\Component\Console\Output\OutputInterface
138
     */
139
    protected $output;
140
141
    /**
142
     * The matches for the last processed CSV filename.
143
     *
144
     * @var array
145
     */
146
    protected $matches = array();
147
148
    /**
149
     * The number of imported bunches.
150
     *
151
     * @var integer
152
     */
153
    protected $bunches = 0;
154
155
    /**
156
     * The PID for the running processes.
157
     *
158
     * @var array
159
     */
160
    protected $pid = null;
161
162
    /**
163
     * The flag that the next steps has to be skipped.
164
     *
165
     * @var string
166
     */
167
    protected $skipSteps = false;
168
169
    /**
170
     * The constructor to initialize the instance.
171
     */
172 3
    public function __construct()
173
    {
174 3
        register_shutdown_function(array($this, 'shutdown'));
175 3
    }
176
177
    /**
178
     * The shutdown handler to catch fatal errors.
179
     *
180
     * This method is need to make sure, that an existing PID file will be removed
181
     * if a fatal error has been triggered.
182
     *
183
     * @return void
184
     */
185
    public function shutdown()
186
    {
187
188
        // check if there was a fatal error caused shutdown
189
        if ($lastError = error_get_last()) {
190
            // initialize error type and message
191
            $type = 0;
192
            $message = '';
193
            // extract the last error values
194
            extract($lastError);
195
            // query whether we've a fatal/user error
196
            if ($type === E_ERROR || $type === E_USER_ERROR) {
197
                // clean-up the PID file
198
                $this->unlock();
199
                // log the fatal error message
200
                $this->log($message, LogLevel::ERROR);
201
            }
202
        }
203
    }
204
205
    /**
206
     * Set's the unique serial for this import process.
207
     *
208
     * @param string $serial The unique serial
209
     *
210
     * @return void
211
     */
212
    public function setSerial($serial)
213
    {
214
        $this->serial = $serial;
215
    }
216
217
    /**
218
     * Return's the unique serial for this import process.
219
     *
220
     * @return string The unique serial
221
     */
222
    public function getSerial()
223
    {
224
        return $this->serial;
225
    }
226
227
    /**
228
     * Set's the system logger.
229
     *
230
     * @param \Psr\Log\LoggerInterface $systemLogger The system logger
231
     *
232
     * @return void
233
     */
234
    public function setSystemLogger(LoggerInterface $systemLogger)
235
    {
236
        $this->systemLogger = $systemLogger;
237
    }
238
239
    /**
240
     * Return's the system logger.
241
     *
242
     * @return \Psr\Log\LoggerInterface The system logger instance
243
     */
244
    public function getSystemLogger()
245
    {
246
        return $this->systemLogger;
247
    }
248
249
    /**
250
     * Sets's the RegistryProcessor instance to handle the running threads.
251
     *
252
     * @param \TechDivision\Import\Services\RegistryProcessorInterface $registryProcessor The registry processor instance
253
     *
254
     * @return void
255
     */
256
    public function setRegistryProcessor(RegistryProcessorInterface $registryProcessor)
257
    {
258
        $this->registryProcessor = $registryProcessor;
259
    }
260
261
    /**
262
     * Return's the RegistryProcessor instance to handle the running threads.
263
     *
264
     * @return \TechDivision\Import\Services\RegistryProcessor The registry processor instance
265
     */
266
    public function getRegistryProcessor()
267
    {
268
        return $this->registryProcessor;
269
    }
270
271
    /**
272
     * Set's the import processor instance.
273
     *
274
     * @param \TechDivision\Import\Services\ImportProcessorInterface $importProcessor The import processor instance
275
     *
276
     * @return void
277
     */
278 1
    public function setImportProcessor(ImportProcessorInterface $importProcessor)
279
    {
280 1
        $this->importProcessor = $importProcessor;
281 1
    }
282
283
    /**
284
     * Return's the import processor instance.
285
     *
286
     * @return \TechDivision\Import\Services\ImportProcessorInterface The import processor instance
287
     */
288 1
    public function getImportProcessor()
289
    {
290 1
        return $this->importProcessor;
291
    }
292
293
    /**
294
     * Set's the system configuration.
295
     *
296
     * @param \TechDivision\Import\ConfigurationInterface $configuration The system configuration
297
     *
298
     * @return void
299
     */
300
    public function setConfiguration(ConfigurationInterface $configuration)
301
    {
302
        $this->configuration = $configuration;
303
    }
304
305
    /**
306
     * Return's the system configuration.
307
     *
308
     * @return \TechDivision\Import\ConfigurationInterface The system configuration
309
     */
310
    public function getConfiguration()
311
    {
312
        return $this->configuration;
313
    }
314
315
    /**
316
     * Set's the input stream to read console information from.
317
     *
318
     * @param \Symfony\Component\Console\Input\InputInterface $input An IutputInterface instance
319
     *
320
     * @return void
321
     */
322
    public function setInput(InputInterface $input)
323
    {
324
        $this->input = $input;
325
    }
326
327
    /**
328
     * Return's the input stream to read console information from.
329
     *
330
     * @return \Symfony\Component\Console\Input\InputInterface An IutputInterface instance
331
     */
332
    protected function getInput()
333
    {
334
        return $this->input;
335
    }
336
337
    /**
338
     * Set's the output stream to write console information to.
339
     *
340
     * @param \Symfony\Component\Console\Output\OutputInterface $output An OutputInterface instance
341
     *
342
     * @return void
343
     */
344
    public function setOutput(OutputInterface $output)
345
    {
346
        $this->output = $output;
347
    }
348
349
    /**
350
     * Return's the output stream to write console information to.
351
     *
352
     * @return \Symfony\Component\Console\Output\OutputInterface An OutputInterface instance
353
     */
354
    protected function getOutput()
355
    {
356
        return $this->output;
357
    }
358
359
    /**
360
     * Return's the source directory that has to be watched for new files.
361
     *
362
     * @return string The source directory
363
     */
364
    protected function getSourceDir()
365
    {
366
        return $this->getConfiguration()->getSourceDir();
367
    }
368
369
    /**
370
     * Parse the temporary upload directory for new files to be imported.
371
     *
372
     * @return void
373
     * @throws \Exception Is thrown if the import can't be finished successfully
374
     */
375
    public function import()
376
    {
377
378
        // track the start time
379
        $startTime = microtime(true);
380
381
        try {
382
            // generate the serial for the new job
383
            $this->setSerial(Uuid::uuid4()->__toString());
384
385
            // prepare the global data for the import process
386
            $this->start();
387
            $this->setUp();
388
            $this->processSubjects();
389
            $this->archive();
390
            $this->tearDown();
391
            $this->finish();
392
393
            // track the time needed for the import in seconds
394
            $endTime = microtime(true) - $startTime;
395
396
            // log a message that import has been finished
397
            $this->log(
398
                sprintf(
399
                    'Successfully finished import with serial %s in %f s',
400
                    $this->getSerial(),
401
                    $endTime
402
                ),
403
                LogLevel::INFO
404
            );
405
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
406
        } catch (\Exception $e) {
407
            // tear down
408
            $this->tearDown();
409
            $this->finish();
410
411
            // track the time needed for the import in seconds
412
            $endTime = microtime(true) - $startTime;
413
414
            // log a message that the file import failed
415
            $this->getSystemLogger()->error($e->__toString());
416
417
            // log a message that import has been finished
418
            $this->getSystemLogger()->info(
419
                sprintf(
420
                    'Can\'t finish import with serial %s in %f s',
421
                    $this->getSerial(),
422
                    $endTime
423
                )
424
            );
425
426
            // re-throw the exception
427
            throw $e;
428
        }
429
    }
430
431
    /**
432
     * Clears the PID file from a previous import process, if it has not
433
     * been cleaned up properly.
434
     *
435
     * @return void
436
     */
437
    public function clearPid()
438
    {
439
440
        // track the start time
441
        $startTime = microtime(true);
442
443
        try {
444
            // query whether or not a PID file exists and delete it
445
            if (file_exists($pidFilename = $this->getPidFilename())) {
446
                if (!unlink($pidFilename)) {
447
                    throw new \Exception(sprintf('Can\'t delete PID file %s', $pidFilename));
448
                }
449
            }
450
451
            // track the time needed for the import in seconds
452
            $endTime = microtime(true) - $startTime;
453
454
            // log a message that PID file has successfully been removed
455
            $this->log(sprintf('Successfully removed PID file %s in %f s', $pidFilename, $endTime), LogLevel::INFO);
456
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
457
        } catch (\Exception $e) {
458
            // track the time needed for the import in seconds
459
            $endTime = microtime(true) - $startTime;
460
461
            // log a message that the file import failed
462
            $this->getSystemLogger()->error($e->__toString());
463
464
            // log a message that PID file has not been removed
465
            $this->getSystemLogger()->error(sprintf('Can\'t remove PID file %s in %f s', $this->getPidFilename(), $endTime));
466
467
            // re-throw the exception
468
            throw $e;
469
        }
470
    }
471
472
    /**
473
     * This method start's the import process by initializing
474
     * the status and appends it to the registry.
475
     *
476
     * @return void
477
     * @throws \Exception Is thrown, an import process is already running
478
     */
479
    protected function start()
480
    {
481
482
        // query whether or not an import is running AND an existing PID has to be ignored
483
        if (file_exists($pidFilename = $this->getPidFilename()) && !$this->getConfiguration()->isIgnorePid()) {
484
            throw new \Exception(sprintf('At least one import process is already running (check PID: %s)', $pidFilename));
485
        } elseif (file_exists($pidFilename = $this->getPidFilename()) && $this->getConfiguration()->isIgnorePid()) {
486
            $this->log(sprintf('At least one import process is already running (PID: %s)', $pidFilename), LogLevel::WARNING);
487
        }
488
489
        // write the TechDivision ANSI art icon to the console
490
        $this->log($this->ansiArt);
491
492
        // log the debug information, if debug mode is enabled
493
        if ($this->getConfiguration()->isDebugMode()) {
494
            // log the system's PHP configuration
495
            $this->log(sprintf('PHP version: %s', phpversion()), LogLevel::DEBUG);
496
            $this->log('-------------------- Loaded Extensions -----------------------', LogLevel::DEBUG);
497
            $this->log(implode(', ', $loadedExtensions = get_loaded_extensions()), LogLevel::DEBUG);
498
            $this->log('--------------------------------------------------------------', LogLevel::DEBUG);
499
500
            // write a warning for low performance, if XDebug extension is activated
501
            if (in_array('xdebug', $loadedExtensions)) {
502
                $this->log('Low performance exptected, as result of enabled XDebug extension!', LogLevel::WARNING);
503
            }
504
        }
505
506
        // log a message that import has been started
507
        $this->log(
508
            sprintf(
509
                'Now start import with serial %s (operation: %s)',
510
                $this->getSerial(),
511
                $this->getConfiguration()->getOperationName()
512
            ),
513
            LogLevel::INFO
514
        );
515
516
        // initialize the status
517
        $status = array(
518
            RegistryKeys::STATUS => 1,
519
            RegistryKeys::SOURCE_DIRECTORY => $this->getConfiguration()->getSourceDir()
520
        );
521
522
        // initialize the status information for the subjects */
523
        /** @var \TechDivision\Import\Configuration\SubjectInterface $subject */
524
        foreach ($this->getConfiguration()->getSubjects() as $subject) {
525
            $status[$subject->getPrefix()] = array();
526
        }
527
528
        // append it to the registry
529
        $this->getRegistryProcessor()->setAttribute($this->getSerial(), $status);
530
    }
531
532
    /**
533
     * Prepares the global data for the import process.
534
     *
535
     * @return void
536
     */
537
    protected function prepareGlobalData()
538
    {
539
540
        // load the registry
541
        $importProcessor = $this->getImportProcessor();
542
        $registryProcessor = $this->getRegistryProcessor();
543
544
        // initialize the array for the global data
545
        $globalData = array();
546
547
        // initialize the global data
548
        $globalData[RegistryKeys::STORES] = $importProcessor->getStores();
549
        $globalData[RegistryKeys::LINK_TYPES] = $importProcessor->getLinkTypes();
550
        $globalData[RegistryKeys::TAX_CLASSES] = $importProcessor->getTaxClasses();
551
        $globalData[RegistryKeys::DEFAULT_STORE] = $importProcessor->getDefaultStore();
552
        $globalData[RegistryKeys::STORE_WEBSITES] = $importProcessor->getStoreWebsites();
553
        $globalData[RegistryKeys::LINK_ATTRIBUTES] = $importProcessor->getLinkAttributes();
554
        $globalData[RegistryKeys::ROOT_CATEGORIES] = $importProcessor->getRootCategories();
555
        $globalData[RegistryKeys::CORE_CONFIG_DATA] = $importProcessor->getCoreConfigData();
556
        $globalData[RegistryKeys::ATTRIBUTE_SETS] = $eavAttributeSets = $importProcessor->getEavAttributeSetsByEntityTypeId(4);
557
558
        // prepare the categories
559
        $categories = array();
560
        foreach ($importProcessor->getCategories() as $category) {
561
            // expload the entity IDs from the category path
562
            $entityIds = explode('/', $category[MemberNames::PATH]);
563
564
            // cut-off the root category
565
            array_shift($entityIds);
566
567
            // continue with the next category if no entity IDs are available
568
            if (sizeof($entityIds) === 0) {
569
                continue;
570
            }
571
572
            // initialize the array for the path elements
573
            $path = array();
574
            foreach ($importProcessor->getCategoryVarcharsByEntityIds($entityIds) as $cat) {
575
                $path[] = $cat[MemberNames::VALUE];
576
            }
577
578
            // append the catogory with the string path as key
579
            $categories[implode('/', $path)] = $category;
580
        }
581
582
        // initialize the array with the categories
583
        $globalData[RegistryKeys::CATEGORIES] = $categories;
584
585
        // prepare an array with the EAV attributes grouped by their attribute set name as keys
586
        $eavAttributes = array();
587
        foreach (array_keys($eavAttributeSets) as $eavAttributeSetName) {
588
            $eavAttributes[$eavAttributeSetName] = $importProcessor->getEavAttributesByEntityTypeIdAndAttributeSetName(4, $eavAttributeSetName);
589
        }
590
591
        // initialize the array with the EAV attributes
592
        $globalData[RegistryKeys::EAV_ATTRIBUTES] = $eavAttributes;
593
594
        // add the status with the global data
595
        $registryProcessor->mergeAttributesRecursive(
596
            $this->getSerial(),
597
            array(RegistryKeys::GLOBAL_DATA => $globalData)
598
        );
599
600
        // log a message that the global data has been prepared
601
        $this->log(
602
            sprintf(
603
                'Successfully prepared global data for import with serial %s',
604
                $this->getSerial()
605
            ),
606
            LogLevel::INFO
607
        );
608
    }
609
610
    /**
611
     * This method queries whether or not the next step has to be processed or not.
612
     *
613
     * @return boolean TRUE if the next step has to be skipped, else FALSE
614
     * @throws \Exception Is thrown, if the configured source directory is not available
615
     */
616
    protected function skipStep()
617
    {
618
619
        // query whether or not, the method has already been
620
        // processed with a negative result
621
        if ($this->skipSteps) {
622
            return $this->skipSteps;
623
        }
624
625
        // clear the filecache
626
        clearstatcache();
627
628
        // load the actual status
629
        $status = $this->getRegistryProcessor()->getAttribute($serial = $this->getSerial());
630
631
        // query whether or not the configured source directory is available
632 View Code Duplication
        if (!is_dir($sourceDir = $status[RegistryKeys::SOURCE_DIRECTORY])) {
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...
633
            throw new \Exception(sprintf('Configured source directory %s is not available!', $sourceDir));
634
        }
635
636
        // load the subjects
637
        $subjects = $this->getConfiguration()->getSubjects();
638
639
        // query whether or not at least one subject has been configured
640
        if ($subjects->count() === 0) {
641
            // log a message that no OK files are available
642
            $this->log(sprintf('Can\'t find any subject configured', $sourceDir), LogLevel::WARNING);
643
            return $this->skipSteps = true;
0 ignored issues
show
Documentation Bug introduced by
The property $skipSteps was declared of type string, but true is of type boolean. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
644
        }
645
646
        // load the OK filenames from the source directory
647
        $okFiles = glob(sprintf('%s/*.ok', $sourceDir));
648
649
        // load the first configured subject
650
        $firstSubject = $subjects->first();
651
652
        // query whether or not at least one OK file is available if the FIRST subject needs one
653
        if (sizeof($okFiles) === 0 && $firstSubject->isOkFileNeeded()) {
654
            // log a message that no OK files are available
655
            $this->log(
656
                sprintf(
657
                    'Can\'t find any OK files for first subject %s in source directory %s',
658
                    $firstSubject->getClassName(),
659
                    $sourceDir
660
                ),
661
                LogLevel::WARNING
662
            );
663
            return $this->skipSteps = true;
664
        }
665
666
        // return FALSE the step should be processed
667
        return $this->skipSteps;
668
    }
669
670
    /**
671
     * Process all the subjects defined in the system configuration.
672
     *
673
     * @return void
674
     * @throws \Exception Is thrown, if one of the subjects can't be processed
675
     */
676
    protected function processSubjects()
677
    {
678
679
        try {
680
            // query whether or not this step has to be skipped
681
            if ($this->skipStep()) {
682
                return;
683
            }
684
685
            // immediately add the PID to lock this import process
686
            $this->lock();
687
688
            // prepare the global data
689
            $this->prepareGlobalData();
690
691
            // load system logger and registry
692
            $importProcessor = $this->getImportProcessor();
693
694
            // start the transaction
695
            $importProcessor->getConnection()->beginTransaction();
696
697
            // load the subjects
698
            $subjects = $this->getConfiguration()->getSubjects();
699
700
            // process all the subjects found in the system configuration
701
            foreach ($subjects as $subject) {
702
                $this->processSubject($subject);
703
            }
704
705
            // finally, if a PID has been set (because CSV files has been found),
706
            // remove it from the PID file to unlock the importer
707
            $this->unlock();
708
709
            // commit the transaction
710
            $importProcessor->getConnection()->commit();
711
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
712
        } catch (\Exception $e) {
713
            // finally, if a PID has been set (because CSV files has been found),
714
            // remove it from the PID file to unlock the importer
715
            $this->unlock();
716
            // rollback the transaction
717
            $importProcessor->getConnection()->rollBack();
0 ignored issues
show
Bug introduced by
The variable $importProcessor 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...
718
719
            // re-throw the exception
720
            throw $e;
721
        }
722
    }
723
724
    /**
725
     * Process the subject with the passed name/identifier.
726
     *
727
     * We create a new, fresh and separate subject for EVERY file here, because this would be
728
     * the starting point to parallelize the import process in a multithreaded/multiprocessed
729
     * environment.
730
     *
731
     * @param \TechDivision\Import\Configuration\SubjectInterface $subject The subject configuration
732
     *
733
     * @return void
734
     * @throws \Exception Is thrown, if the subject can't be processed
735
     */
736
    protected function processSubject(\TechDivision\Import\Configuration\SubjectInterface $subject)
737
    {
738
739
        // clear the filecache
740
        clearstatcache();
741
742
        // load the actual status
743
        $status = $this->getRegistryProcessor()->getAttribute($serial = $this->getSerial());
744
745
        // query whether or not the configured source directory is available
746 View Code Duplication
        if (!is_dir($sourceDir = $status[RegistryKeys::SOURCE_DIRECTORY])) {
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...
747
            throw new \Exception(sprintf('Source directory %s for subject %s is not available!', $sourceDir, $subject->getClassName()));
748
        }
749
750
        // initialize the array with the CSV files found in the source directory
751
        $files = glob(sprintf('%s/*.csv', $sourceDir));
752
753
        // sorting the files for the apropriate order
754
        usort($files, function ($a, $b) {
755
            return strcmp($a, $b);
756
        });
757
758
        // log a debug message
759
        $this->log(
760
            sprintf(
761
                'Now checking directory %s for files to be imported',
762
                $sourceDir
763
            ),
764
            LogLevel::DEBUG
765
        );
766
767
        // iterate through all CSV files and process the subjects
768
        foreach ($files as $pathname) {
769
            // query whether or not that the file is part of the actual bunch
770
            if ($this->isPartOfBunch($subject->getPrefix(), $pathname)) {
771
                // initialize the subject and import the bunch
772
                $subjectInstance = $this->subjectFactory($subject);
0 ignored issues
show
Documentation introduced by
$subject is of type object<TechDivision\Impo...ation\SubjectInterface>, but the function expects a object<TechDivision\Import\Configuration\Subject>.

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

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

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

function acceptsInteger($int) { }

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

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
773
774
                // query whether or not the subject needs an OK file,
775
                // if yes remove the filename from the file
776
                if ($subjectInstance->isOkFileNeeded()) {
777
                    $this->removeFromOkFile($pathname);
778
                }
779
780
                // finally import the CSV file
781
                $subjectInstance->import($serial, $pathname);
782
783
                // query whether or not, we've to export artefacts
784
                if ($subjectInstance instanceof ExportableSubjectInterface) {
0 ignored issues
show
Bug introduced by
The class TechDivision\Import\Subj...ortableSubjectInterface does not exist. Did you forget a USE statement, or did you not list all dependencies?

This error could be the result of:

1. Missing dependencies

PHP Analyzer uses your composer.json file (if available) to determine the dependencies of your project and to determine all the available classes and functions. It expects the composer.json to be in the root folder of your repository.

Are you sure this class is defined by one of your dependencies, or did you maybe not list a dependency in either the require or require-dev section?

2. Missing use statement

PHP does not complain about undefined classes in ìnstanceof checks. For example, the following PHP code will work perfectly fine:

if ($x instanceof DoesNotExist) {
    // Do something.
}

If you have not tested against this specific condition, such errors might go unnoticed.

Loading history...
785
                    $subjectInstance->export(
786
                        $this->matches[BunchKeys::FILENAME],
787
                        $this->matches[BunchKeys::COUNTER]
788
                    );
789
                }
790
791
                // raise the number of the imported bunches
792
                $this->bunches++;
793
            }
794
        }
795
796
        // reset the matches, because the exported artefacts
797
        $this->matches = array();
798
799
        // and and log a message that the subject has been processed
800
        $this->log(
801
            sprintf(
802
                'Successfully processed subject %s with %d bunch(es)!',
803
                $subject->getClassName(),
804
                $this->bunches
805
            ),
806
            LogLevel::DEBUG
807
        );
808
    }
809
810
    /**
811
     * Queries whether or not, the passed filename is part of a bunch or not.
812
     *
813
     * @param string $prefix   The prefix to query for
814
     * @param string $filename The filename to query for
815
     *
816
     * @return boolean TRUE if the filename is part, else FALSE
817
     */
818 2
    public function isPartOfBunch($prefix, $filename)
819
    {
820
821
        // initialize the pattern
822 2
        $pattern = '';
0 ignored issues
show
Unused Code introduced by
$pattern is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
823
824
        // query whether or not, this is the first file to be processed
825 2
        if (sizeof($this->matches) === 0) {
826
            // initialize the pattern to query whether the FIRST file has to be processed or not
827 2
            $pattern = sprintf(
828 2
                '/^.*\/(?<%s>%s)_(?<%s>.*)_(?<%s>\d+)\\.csv$/',
829 2
                BunchKeys::PREFIX,
830
                $prefix,
831 2
                BunchKeys::FILENAME,
832 2
                BunchKeys::COUNTER
833
            );
834
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
835
        } else {
836
            // initialize the pattern to query whether the NEXT file is part of a bunch or not
837 2
            $pattern = sprintf(
838 2
                '/^.*\/(?<%s>%s)_(?<%s>%s)_(?<%s>\d+)\\.csv$/',
839 2
                BunchKeys::PREFIX,
840 2
                $this->matches[BunchKeys::PREFIX],
841 2
                BunchKeys::FILENAME,
842 2
                $this->matches[BunchKeys::FILENAME],
843 2
                BunchKeys::COUNTER
844
            );
845
        }
846
847
        // initialize the array for the matches
848 2
        $matches = array();
849
850
        // update the matches, if the pattern matches
851 2
        if ($result = preg_match($pattern, $filename, $matches)) {
852 2
            $this->matches = $matches;
853
        }
854
855
        // stop processing, if the filename doesn't match
856 2
        return (boolean) $result;
857
    }
858
859
    /**
860
     * Return's an array with the names of the expected OK files for the actual subject.
861
     *
862
     * @return array The array with the expected OK filenames
863
     */
864
    protected function getOkFilenames()
865
    {
866
867
        // load the array with the available bunch keys
868
        $bunchKeys = BunchKeys::getAllKeys();
869
870
        // initialize the array for the available okFilenames
871
        $okFilenames = array();
872
873
        // prepare the OK filenames based on the found CSV file information
874
        for ($i = 1; $i <= sizeof($bunchKeys); $i++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function sizeof() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
875
            // intialize the array for the parts of the names (prefix, filename + counter)
876
            $parts = array();
877
            // load the parts from the matches
878
            for ($z = 0; $z < $i; $z++) {
879
                $parts[] = $this->matches[$bunchKeys[$z]];
880
            }
881
882
            // query whether or not, the OK file exists, if yes append it
883
            if (file_exists($okFilename = sprintf('%s/%s.ok', $this->getSourceDir(), implode('_', $parts)))) {
884
                $okFilenames[] = $okFilename;
885
            }
886
        }
887
888
        // prepare and return the pattern for the OK file
889
        return $okFilenames;
890
    }
891
892
    /**
893
     * Query whether or not, the passed CSV filename is in the OK file. If the filename was found,
894
     * it'll be returned and the method return TRUE.
895
     *
896
     * If the filename is NOT in the OK file, the method return's FALSE and the CSV should NOT be
897
     * imported/moved.
898
     *
899
     * @param string $filename The CSV filename to query for
900
     *
901
     * @return void
902
     * @throws \Exception Is thrown, if the passed filename is NOT in the OK file or it can NOT be removed from it
903
     */
904
    protected function removeFromOkFile($filename)
905
    {
906
907
        try {
908
            // load the expected OK filenames
909
            $okFilenames = $this->getOkFilenames();
910
911
            // iterate over the found OK filenames (should usually be only one, but could be more)
912
            foreach ($okFilenames as $okFilename) {
913
                // if the OK filename matches the CSV filename AND the OK file is empty
914
                if (basename($filename, '.csv') === basename($okFilename, '.ok') && filesize($okFilename) === 0) {
915
                    unlink($okFilename);
916
                    return;
917
                }
918
919
                // else, remove the CSV filename from the OK file
920
                $this->removeLineFromFile(basename($filename), $okFilename);
921
                return;
922
            }
923
924
            // throw an exception if either no OK file has been found,
925
            // or the CSV file is not in one of the OK files
926
            throw new \Exception(
927
                sprintf(
928
                    'Can\'t found filename %s in one of the expected OK files: %s',
929
                    $filename,
930
                    implode(', ', $okFilenames)
931
                )
932
            );
933
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
934
        } catch (\Exception $e) {
935
            // wrap and re-throw the exception
936
            throw new \Exception(
937
                sprintf(
938
                    'Can\'t remove filename %s from OK file: %s',
939
                    $filename,
940
                    $okFilename
941
                ),
942
                null,
943
                $e
944
            );
945
        }
946
    }
947
948
    /**
949
     * Factory method to create new handler instances.
950
     *
951
     * @param \TechDivision\Import\Configuration\Subject $subject The subject configuration
952
     *
953
     * @return object The handler instance
954
     */
955
    public function subjectFactory($subject)
956
    {
957
958
        // load the subject class name
959
        $className = $subject->getClassName();
960
961
        // the database connection to use
962
        $connection = $this->getImportProcessor()->getConnection();
963
964
        // initialize a new handler with the passed class name
965
        $instance = new $className();
966
967
        // $instance the handler instance
968
        $instance->setConfiguration($subject);
969
        $instance->setSystemLogger($this->getSystemLogger());
970
        $instance->setRegistryProcessor($this->getRegistryProcessor());
971
972
        // instanciate and set the product processor, if specified
973
        if ($processorFactory = $subject->getProcessorFactory()) {
974
            $productProcessor = $processorFactory::factory($connection, $subject);
975
            $instance->setProductProcessor($productProcessor);
976
        }
977
978
        // return the subject instance
979
        return $instance;
980
    }
981
982
    /**
983
     * Lifecycle callback that will be inovked after the
984
     * import process has been finished.
985
     *
986
     * @return void
987
     * @throws \Exception Is thrown, if the
988
     */
989
    protected function archive()
990
    {
991
992
        // query whether or not, the import artefacts have to be archived
993
        if (!$this->getConfiguration()->haveArchiveArtefacts()) {
994
            $this->log(sprintf('Archiving functionality has not been activated'), LogLevel::INFO);
995
            return;
996
        }
997
998
        // if no files have been imported, return immediately
999
        if ($this->bunches === 0) {
1000
            $this->log(sprintf('Found no files to archive'), LogLevel::INFO);
1001
            return;
1002
        }
1003
1004
        // clear the filecache
1005
        clearstatcache();
1006
1007
        // load the actual status
1008
        $status = $this->getRegistryProcessor()->getAttribute($this->getSerial());
1009
1010
        // query whether or not the configured source directory is available
1011 View Code Duplication
        if (!is_dir($sourceDir = $status[RegistryKeys::SOURCE_DIRECTORY])) {
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...
1012
            throw new \Exception(sprintf('Configured source directory %s is not available!', $sourceDir));
1013
        }
1014
1015
        // init file iterator on source directory
1016
        $fileIterator = new \FilesystemIterator($sourceDir);
1017
1018
        // log the number of files that has to be archived
1019
        $this->log(sprintf('Found %d files to archive in directory %s', $this->bunches, $sourceDir), LogLevel::INFO);
1020
1021
        // initialize the directory to create the archive in
1022
        $archiveDir = sprintf('%s/%s', $this->getConfiguration()->getTargetDir(), $this->getConfiguration()->getArchiveDir());
1023
1024
        // query whether or not the directory already exists
1025
        if (!is_dir($archiveDir)) {
1026
            mkdir($archiveDir);
1027
        }
1028
1029
        // create the ZIP archive
1030
        $archive = new \ZipArchive();
1031
        $archive->open($archiveFile = sprintf('%s/%s.zip', $archiveDir, $this->getSerial()), \ZipArchive::CREATE);
1032
1033
        // iterate through all files and add them to the ZIP archive
1034
        foreach ($fileIterator as $filename) {
1035
            $archive->addFile($filename);
1036
        }
1037
1038
        // save the ZIP archive
1039
        $archive->close();
1040
1041
        // finally remove the directory with the imported files
1042
        $this->removeDir($sourceDir);
1043
1044
        // and and log a message that the import artefacts have been archived
1045
        $this->log(sprintf('Successfully archived imported files to %s!', $archiveFile), LogLevel::INFO);
1046
    }
1047
1048
    /**
1049
     * Removes the passed directory recursively.
1050
     *
1051
     * @param string $src Name of the directory to remove
1052
     *
1053
     * @return void
1054
     * @throws \Exception Is thrown, if the directory can not be removed
1055
     */
1056
    protected function removeDir($src)
1057
    {
1058
1059
        // open the directory
1060
        $dir = opendir($src);
1061
1062
        // remove files/folders recursively
1063
        while (false !== ($file = readdir($dir))) {
1064
            if (($file != '.') && ($file != '..')) {
1065
                $full = $src . '/' . $file;
1066
                if (is_dir($full)) {
1067
                    $this->removeDir($full);
1068
                } else {
1069
                    if (!unlink($full)) {
1070
                        throw new \Exception(sprintf('Can\'t remove file %s', $full));
1071
                    }
1072
                }
1073
            }
1074
        }
1075
1076
        // close handle and remove directory itself
1077
        closedir($dir);
1078
        if (!rmdir($src)) {
1079
            throw new \Exception(sprintf('Can\'t remove directory %s', $src));
1080
        }
1081
    }
1082
1083
    /**
1084
     * Simple method that writes the passed method the the console and the
1085
     * system logger, if configured and a log level has been passed.
1086
     *
1087
     * @param string $msg      The message to log
1088
     * @param string $logLevel The log level to use
1089
     *
1090
     * @return void
1091
     */
1092
    protected function log($msg, $logLevel = null)
1093
    {
1094
1095
        // initialize the formatter helper
1096
        $helper = new FormatterHelper();
1097
1098
        // map the log level to the console style
1099
        $style = $this->mapLogLevelToStyle($logLevel);
1100
1101
        // format the message, according to the passed log level and write it to the console
1102
        $this->getOutput()->writeln($logLevel ? $helper->formatBlock($msg, $style) : $msg);
1103
1104
        // log the message if a log level has been passed
1105
        if ($logLevel && $systemLogger = $this->getSystemLogger()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $logLevel of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
1106
            $systemLogger->log($logLevel, $msg);
1107
        }
1108
    }
1109
1110
    /**
1111
     * Map's the passed log level to a valid symfony console style.
1112
     *
1113
     * @param string $logLevel The log level to map
1114
     *
1115
     * @return string The apropriate symfony console style
1116
     */
1117
    protected function mapLogLevelToStyle($logLevel)
1118
    {
1119
1120
        // query whether or not the log level is mapped
1121
        if (isset($this->logLevelStyleMapping[$logLevel])) {
1122
            return $this->logLevelStyleMapping[$logLevel];
1123
        }
1124
1125
        // return the default style => info
1126
        return Simple::DEFAULT_STYLE;
1127
    }
1128
1129
    /**
1130
     * Return's the PID filename to use.
1131
     *
1132
     * @return string The PID filename
1133
     */
1134
    protected function getPidFilename()
1135
    {
1136
        return $this->getConfiguration()->getPidFilename();
1137
    }
1138
1139
    /**
1140
     * Persist the UUID of the actual import process to the PID file.
1141
     *
1142
     * @return void
1143
     * @throws \Exception Is thrown, if the PID can not be added
1144
     */
1145
    protected function lock()
1146
    {
1147
1148
        // query whether or not, the PID has already been set
1149
        if ($this->pid === $this->getSerial()) {
1150
            return;
1151
        }
1152
1153
        // if not, initialize the PID
1154
        $this->pid = $this->getSerial();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getSerial() of type string is incompatible with the declared type array of property $pid.

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

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

Loading history...
1155
1156
        // open the PID file
1157
        $fh = fopen($pidFilename = $this->getPidFilename(), 'a');
1158
1159
        // append the PID to the PID file
1160
        if (fwrite($fh, $this->pid . PHP_EOL) === false) {
1161
            throw new \Exception(sprintf('Can\'t write PID %s to PID file %s', $this->pid, $pidFilename));
1162
        }
1163
1164
        // close the file handle
1165
        fclose($fh);
1166
    }
1167
1168
    /**
1169
     * Remove's the UUID of the actual import process from the PID file.
1170
     *
1171
     * @return void
1172
     * @throws \Exception Is thrown, if the PID can not be removed
1173
     */
1174
    protected function unlock()
1175
    {
1176
        try {
1177
            // remove the PID from the PID file if set
1178
            if ($this->pid === $this->getSerial()) {
1179
                $this->removeLineFromFile($this->pid, $this->getPidFilename());
1180
            }
1181
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
1182
        } catch (FileNotFoundException $fnfe) {
1183
            $this->getSystemLogger()->notice(sprintf('PID file %s doesn\'t exist', $this->getPidFilename()));
1184
        } catch (LineNotFoundException $lnfe) {
1185
            $this->getSystemLogger()->notice(sprintf('PID %s is can not be found in PID file %s', $this->pid, $this->getPidFilename()));
1186
        } catch (\Exception $e) {
1187
            throw new \Exception(sprintf('Can\'t remove PID %s from PID file %s', $this->pid, $this->getPidFilename()), null, $e);
1188
        }
1189
    }
1190
1191
    /**
1192
     * Lifecycle callback that will be inovked before the
1193
     * import process has been started.
1194
     *
1195
     * @return void
1196
     */
1197
    protected function setUp()
1198
    {
1199
    }
1200
1201
    /**
1202
     * Lifecycle callback that will be inovked after the
1203
     * import process has been finished.
1204
     *
1205
     * @return void
1206
     */
1207
    protected function tearDown()
1208
    {
1209
    }
1210
1211
    /**
1212
     * This method finishes the import process and cleans the registry.
1213
     *
1214
     * @return void
1215
     */
1216
    protected function finish()
1217
    {
1218
        // remove the import status from the registry
1219
        $this->getRegistryProcessor()->removeAttribute($this->getSerial());
1220
    }
1221
1222
1223
    /**
1224
     * Remove's the passed line from the file with the passed name.
1225
     *
1226
     * @param string $line     The line to be removed
1227
     * @param string $filename The name of the file the line has to be removed
1228
     *
1229
     * @return void
1230
     * @throws \Exception Is thrown, if the file doesn't exists, the line is not found or can not be removed
1231
     */
1232
    protected function removeLineFromFile($line, $filename)
1233
    {
1234
1235
        // query whether or not the filename
1236
        if (!file_exists($filename)) {
1237
            throw new FileNotFoundException(sprintf('File %s doesn\' exists', $filename));
1238
        }
1239
1240
        // open the PID file
1241
        $fh = fopen($filename, 'r+');
1242
1243
        // initialize the array for the PIDs found in the PID file
1244
        $lines = array();
1245
1246
        // initialize the flag if the line has been found
1247
        $found = false;
1248
1249
        // read the lines with the PIDs from the PID file
1250
        while (($buffer = fgets($fh, 4096)) !== false) {
1251
            // remove the new line
1252
            $buffer = trim($buffer, PHP_EOL);
1253
            // if the line is the one to be removed, ignore the line
1254
            if ($line === $buffer) {
1255
                $found = true;
1256
                continue;
1257
            }
1258
1259
            // add the found PID to the array
1260
            $lines[] = $buffer;
1261
        }
1262
1263
        // query whether or not, we found the line
1264
        if (!$found) {
1265
            throw new LineNotFoundException(sprintf('Line %s can not be found in file %s', $line, $filename));
1266
        }
1267
1268
        // if there are NO more lines, delete the file
1269
        if (sizeof($lines) === 0) {
1270
            fclose($fh);
1271
            unlink($filename);
1272
            return;
1273
        }
1274
1275
        // empty the file and rewind the file pointer
1276
        ftruncate($fh, 0);
1277
        rewind($fh);
1278
1279
        // append the existing lines to the file
1280
        foreach ($lines as $ln) {
1281
            if (fwrite($fh, $ln . PHP_EOL) === false) {
1282
                throw new \Exception(sprintf('Can\'t write %s to file %s', $ln, $filename));
1283
            }
1284
        }
1285
1286
        // finally close the file
1287
        fclose($fh);
1288
    }
1289
}
1290