Completed
Push — master ( 6b7465...e232d3 )
by Tim
9s
created

Simple::start()   C

Complexity

Conditions 8
Paths 13

Size

Total Lines 52
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 8
Bugs 2 Features 1
Metric Value
c 8
b 2
f 1
dl 0
loc 52
ccs 0
cts 31
cp 0
rs 6.8493
cc 8
eloc 25
nc 13
nop 0
crap 72

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
40
/**
41
 * The M2IF - Console Tool implementation.
42
 *
43
 * This is a example console tool implementation that should give developers an impression
44
 * on how the M2IF could be used to implement their own Magento 2 importer.
45
 *
46
 * @author    Tim Wagner <[email protected]>
47
 * @copyright 2016 TechDivision GmbH <[email protected]>
48
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
49
 * @link      https://github.com/techdivision/import-cli-simple
50
 * @link      http://www.techdivision.com
51
 */
52
class Simple
53
{
54
55
    /**
56
     * The default style to write messages to the symfony console.
57
     *
58
     * @var string
59
     */
60
    const DEFAULT_STYLE = 'info';
61
62
    /**
63
     * The PID filename to use.
64
     *
65
     * @var string
66
     */
67
    const PID_FILENAME = 'importer.pid';
68
69
    /**
70
     * The TechDivision company name as ANSI art.
71
     *
72
     * @var string
73
     */
74
    protected $ansiArt = ' _______        _     _____  _       _     _
75
|__   __|      | |   |  __ \(_)     (_)   (_)
76
   | | ___  ___| |__ | |  | |___   ___ ___ _  ___  _ __
77
   | |/ _ \/ __| \'_ \| |  | | \ \ / / / __| |/ _ \| \'_ \
78
   | |  __/ (__| | | | |__| | |\ V /| \__ \ | (_) | | | |
79
   |_|\___|\___|_| |_|_____/|_| \_/ |_|___/_|\___/|_| |_|
80
';
81
82
    /**
83
     * The log level => console style mapping.
84
     *
85
     * @var array
86
     */
87
    protected $logLevelStyleMapping = array(
88
        LogLevel::INFO      => 'info',
89
        LogLevel::DEBUG     => 'comment',
90
        LogLevel::ERROR     => 'error',
91
        LogLevel::ALERT     => 'error',
92
        LogLevel::CRITICAL  => 'error',
93
        LogLevel::EMERGENCY => 'error',
94
        LogLevel::WARNING   => 'error',
95
        LogLevel::NOTICE    => 'info'
96
    );
97
98
    /**
99
     * The actions unique serial.
100
     *
101
     * @var string
102
     */
103
    protected $serial;
104
105
    /**
106
     * The system logger implementation.
107
     *
108
     * @var \Psr\Log\LoggerInterface
109
     */
110
    protected $systemLogger;
111
112
    /**
113
     * The RegistryProcessor instance to handle running threads.
114
     *
115
     * @var \TechDivision\Import\Services\RegistryProcessorInterface
116
     */
117
    protected $registryProcessor;
118
119
    /**
120
     * The processor to read/write the necessary import data.
121
     *
122
     * @var \TechDivision\Import\Services\ImportProcessorInterface
123
     */
124
    protected $importProcessor;
125
126
    /**
127
     * The system configuration.
128
     *
129
     * @var \TechDivision\Import\ConfigurationInterface
130
     */
131
    protected $configuration;
132
133
    /**
134
     * The input stream to read console information from.
135
     *
136
     * @var \Symfony\Component\Console\Input\InputInterface
137
     */
138
    protected $input;
139
140
    /**
141
     * The output stream to write console information to.
142
     *
143
     * @var \Symfony\Component\Console\Output\OutputInterface
144
     */
145
    protected $output;
146
147
    /**
148
     * The matches for the last processed CSV filename.
149
     *
150
     * @var array
151
     */
152
    protected $matches = array();
153
154
    /**
155
     * The number of imported bunches.
156
     *
157
     * @var integer
158
     */
159
    protected $bunches = 0;
160
161
    /**
162
     * The PID for the running processes.
163
     *
164
     * @var array
165
     */
166
    protected $pid = null;
167
168
    /**
169
     * The CSV files that has to be imported.
170
     *
171
     * @var array
172
     */
173
    protected $files = array();
174
175
    /**
176
     * Set's the unique serial for this import process.
177
     *
178
     * @param string $serial The unique serial
179
     *
180
     * @return void
181
     */
182
    public function setSerial($serial)
183
    {
184
        $this->serial = $serial;
185
    }
186
187
    /**
188
     * Return's the unique serial for this import process.
189
     *
190
     * @return string The unique serial
191
     */
192
    public function getSerial()
193
    {
194
        return $this->serial;
195
    }
196
197
    /**
198
     * Set's the system logger.
199
     *
200
     * @param \Psr\Log\LoggerInterface $systemLogger The system logger
201
     *
202
     * @return void
203
     */
204
    public function setSystemLogger(LoggerInterface $systemLogger)
205
    {
206
        $this->systemLogger = $systemLogger;
207
    }
208
209
    /**
210
     * Return's the system logger.
211
     *
212
     * @return \Psr\Log\LoggerInterface The system logger instance
213
     */
214
    public function getSystemLogger()
215
    {
216
        return $this->systemLogger;
217
    }
218
219
    /**
220
     * Sets's the RegistryProcessor instance to handle the running threads.
221
     *
222
     * @param \TechDivision\Import\Services\RegistryProcessorInterface $registryProcessor The registry processor instance
223
     *
224
     * @return void
225
     */
226
    public function setRegistryProcessor(RegistryProcessorInterface $registryProcessor)
227
    {
228
        $this->registryProcessor = $registryProcessor;
229
    }
230
231
    /**
232
     * Return's the RegistryProcessor instance to handle the running threads.
233
     *
234
     * @return \TechDivision\Import\Services\RegistryProcessor The registry processor instance
235
     */
236
    public function getRegistryProcessor()
237
    {
238
        return $this->registryProcessor;
239
    }
240
241
    /**
242
     * Set's the import processor instance.
243
     *
244
     * @param \TechDivision\Import\Services\ImportProcessorInterface $importProcessor The import processor instance
245
     *
246
     * @return void
247
     */
248 1
    public function setImportProcessor(ImportProcessorInterface $importProcessor)
249
    {
250 1
        $this->importProcessor = $importProcessor;
251 1
    }
252
253
    /**
254
     * Return's the import processor instance.
255
     *
256
     * @return \TechDivision\Import\Services\ImportProcessorInterface The import processor instance
257
     */
258 1
    public function getImportProcessor()
259
    {
260 1
        return $this->importProcessor;
261
    }
262
263
    /**
264
     * Set's the system configuration.
265
     *
266
     * @param \TechDivision\Import\ConfigurationInterface $configuration The system configuration
267
     *
268
     * @return void
269
     */
270
    public function setConfiguration(ConfigurationInterface $configuration)
271
    {
272
        $this->configuration = $configuration;
273
    }
274
275
    /**
276
     * Return's the system configuration.
277
     *
278
     * @return \TechDivision\Import\ConfigurationInterface The system configuration
279
     */
280
    public function getConfiguration()
281
    {
282
        return $this->configuration;
283
    }
284
285
    /**
286
     * Set's the input stream to read console information from.
287
     *
288
     * @param \Symfony\Component\Console\Input\InputInterface $input An IutputInterface instance
289
     *
290
     * @return void
291
     */
292
    public function setInput(InputInterface $input)
293
    {
294
        $this->input = $input;
295
    }
296
297
    /**
298
     * Return's the input stream to read console information from.
299
     *
300
     * @return \Symfony\Component\Console\Input\InputInterface An IutputInterface instance
301
     */
302
    protected function getInput()
303
    {
304
        return $this->input;
305
    }
306
307
    /**
308
     * Set's the output stream to write console information to.
309
     *
310
     * @param \Symfony\Component\Console\Output\OutputInterface $output An OutputInterface instance
311
     *
312
     * @return void
313
     */
314
    public function setOutput(OutputInterface $output)
315
    {
316
        $this->output = $output;
317
    }
318
319
    /**
320
     * Return's the output stream to write console information to.
321
     *
322
     * @return \Symfony\Component\Console\Output\OutputInterface An OutputInterface instance
323
     */
324
    protected function getOutput()
325
    {
326
        return $this->output;
327
    }
328
329
    /**
330
     * Return's the source directory that has to be watched for new files.
331
     *
332
     * @return string The source directory
333
     */
334
    protected function getSourceDir()
335
    {
336
        return $this->getConfiguration()->getSourceDir();
337
    }
338
339
    /**
340
     * Parse the temporary upload directory for new files to be imported.
341
     *
342
     * @return void
343
     * @throws \Exception Is thrown if the import can't be finished successfully
344
     */
345
    public function import()
346
    {
347
348
        // track the start time
349
        $startTime = microtime(true);
350
351
        try {
352
            // generate the serial for the new job
353
            $this->setSerial(Uuid::uuid4()->__toString());
354
355
            // prepare the global data for the import process
356
            $this->start();
357
            $this->setUp();
358
            $this->processSubjects();
359
            $this->archive();
360
            $this->tearDown();
361
            $this->finish();
362
363
            // track the time needed for the import in seconds
364
            $endTime = microtime(true) - $startTime;
365
366
            // log a message that import has been finished
367
            $this->log(
368
                sprintf(
369
                    'Successfully finished import with serial %s in %f s',
370
                    $this->getSerial(),
371
                    $endTime
372
                ),
373
                LogLevel::INFO
374
            );
375
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
376
        } catch (\Exception $e) {
377
            // tear down
378
            $this->tearDown();
379
            $this->finish();
380
381
            // track the time needed for the import in seconds
382
            $endTime = microtime(true) - $startTime;
383
384
            // log a message that the file import failed
385
            $this->getSystemLogger()->error($e->__toString());
386
387
            // log a message that import has been finished
388
            $this->getSystemLogger()->info(
389
                sprintf(
390
                    'Can\'t finish import with serial %s in %f s',
391
                    $this->getSerial(),
392
                    $endTime
393
                )
394
            );
395
396
            // re-throw the exception
397
            throw $e;
398
        }
399
    }
400
401
    /**
402
     * Clears the PID file from a previous import process, if it has not
403
     * been cleaned up properly.
404
     *
405
     * @return void
406
     */
407
    public function clearPid()
408
    {
409
410
        // track the start time
411
        $startTime = microtime(true);
412
413
        try {
414
            // query whether or not a PID file exists and delete it
415
            if (file_exists($pidFilename = $this->getPidFilename())) {
416
                if (!unlink($pidFilename)) {
417
                    throw new \Exception(sprintf('Can\'t delete PID file %s', $pidFilename));
418
                }
419
            }
420
421
            // track the time needed for the import in seconds
422
            $endTime = microtime(true) - $startTime;
423
424
            // log a message that PID file has successfully been removed
425
            $this->log(sprintf('Successfully removed PID file %s in %f s', $pidFilename, $endTime), LogLevel::INFO);
426
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
427
        } catch (\Exception $e) {
428
            // track the time needed for the import in seconds
429
            $endTime = microtime(true) - $startTime;
430
431
            // log a message that the file import failed
432
            $this->getSystemLogger()->error($e->__toString());
433
434
            // log a message that PID file has not been removed
435
            $this->getSystemLogger()->error(sprintf('Can\'t remove PID file %s in %f s', $this->getPidFilename(), $endTime));
436
437
            // re-throw the exception
438
            throw $e;
439
        }
440
    }
441
442
    /**
443
     * This method start's the import process by initializing
444
     * the status and appends it to the registry.
445
     *
446
     * @return void
447
     * @throws \Exception Is thrown, an import process is already running
448
     */
449
    protected function start()
450
    {
451
452
        // query whether or not an import is running AND an existing PID has to be ignored
453
        if (file_exists($pidFilename = $this->getPidFilename()) && !$this->getConfiguration()->isIgnorePid()) {
454
            throw new \Exception(sprintf('At least one import process is already running (check PID: %s)', $pidFilename));
455
        } elseif (file_exists($pidFilename = $this->getPidFilename()) && $this->getConfiguration()->isIgnorePid()) {
456
            $this->log(sprintf('At least one import process is already running (PID: %s)', $pidFilename), LogLevel::WARNING);
457
        }
458
459
        // write the TechDivision ANSI art icon to the console
460
        $this->log($this->ansiArt);
461
462
        // log the debug information, if debug mode is enabled
463
        if ($this->getConfiguration()->isDebugMode()) {
464
            // log the system's PHP configuration
465
            $this->log(sprintf('PHP version: %s', phpversion()), LogLevel::DEBUG);
466
            $this->log('-------------------- Loaded Extensions -----------------------', LogLevel::DEBUG);
467
            $this->log(implode(', ', $loadedExtensions = get_loaded_extensions()), LogLevel::DEBUG);
468
            $this->log('--------------------------------------------------------------', LogLevel::DEBUG);
469
470
            // write a warning for low performance, if XDebug extension is activated
471
            if (in_array('xdebug', $loadedExtensions)) {
472
                $this->log('Low performance exptected, as result of enabled XDebug extension!', LogLevel::WARNING);
473
            }
474
        }
475
476
        // log a message that import has been started
477
        $this->log(
478
            sprintf(
479
                'Now start import with serial %s (operation: %s)',
480
                $this->getSerial(),
481
                $this->getConfiguration()->getOperationName()
482
            ),
483
            LogLevel::INFO
484
        );
485
486
        // initialize the status
487
        $status = array(
488
            RegistryKeys::STATUS => 1,
489
            RegistryKeys::SOURCE_DIRECTORY => $this->getConfiguration()->getSourceDir()
490
        );
491
492
        // initialize the status information for the subjects */
493
        /** @var \TechDivision\Import\Configuration\SubjectInterface $subject */
494
        foreach ($this->getConfiguration()->getSubjects() as $subject) {
495
            $status[$subject->getPrefix()] = array();
496
        }
497
498
        // append it to the registry
499
        $this->getRegistryProcessor()->setAttribute($this->getSerial(), $status);
500
    }
501
502
    /**
503
     * Prepares the global data for the import process.
504
     *
505
     * @return void
506
     */
507
    protected function setUp()
508
    {
509
510
        // load the registry
511
        $importProcessor = $this->getImportProcessor();
512
        $registryProcessor = $this->getRegistryProcessor();
513
514
        // initialize the array for the global data
515
        $globalData = array();
516
517
        // initialize the global data
518
        $globalData[RegistryKeys::STORES] = $importProcessor->getStores();
519
        $globalData[RegistryKeys::LINK_TYPES] = $importProcessor->getLinkTypes();
520
        $globalData[RegistryKeys::TAX_CLASSES] = $importProcessor->getTaxClasses();
521
        $globalData[RegistryKeys::DEFAULT_STORE] = $importProcessor->getDefaultStore();
522
        $globalData[RegistryKeys::STORE_WEBSITES] = $importProcessor->getStoreWebsites();
523
        $globalData[RegistryKeys::LINK_ATTRIBUTES] = $importProcessor->getLinkAttributes();
524
        $globalData[RegistryKeys::ROOT_CATEGORIES] = $importProcessor->getRootCategories();
525
        $globalData[RegistryKeys::CORE_CONFIG_DATA] = $importProcessor->getCoreConfigData();
526
        $globalData[RegistryKeys::ATTRIBUTE_SETS] = $eavAttributeSets = $importProcessor->getEavAttributeSetsByEntityTypeId(4);
527
528
        // prepare the categories
529
        $categories = array();
530
        foreach ($importProcessor->getCategories() as $category) {
531
            // expload the entity IDs from the category path
532
            $entityIds = explode('/', $category[MemberNames::PATH]);
533
534
            // cut-off the root category
535
            array_shift($entityIds);
536
537
            // continue with the next category if no entity IDs are available
538
            if (sizeof($entityIds) === 0) {
539
                continue;
540
            }
541
542
            // initialize the array for the path elements
543
            $path = array();
544
            foreach ($importProcessor->getCategoryVarcharsByEntityIds($entityIds) as $cat) {
545
                $path[] = $cat[MemberNames::VALUE];
546
            }
547
548
            // append the catogory with the string path as key
549
            $categories[implode('/', $path)] = $category;
550
        }
551
552
        // initialize the array with the categories
553
        $globalData[RegistryKeys::CATEGORIES] = $categories;
554
555
        // prepare an array with the EAV attributes grouped by their attribute set name as keys
556
        $eavAttributes = array();
557
        foreach (array_keys($eavAttributeSets) as $eavAttributeSetName) {
558
            $eavAttributes[$eavAttributeSetName] = $importProcessor->getEavAttributesByEntityTypeIdAndAttributeSetName(4, $eavAttributeSetName);
559
        }
560
561
        // initialize the array with the EAV attributes
562
        $globalData[RegistryKeys::EAV_ATTRIBUTES] = $eavAttributes;
563
564
        // add the status with the global data
565
        $registryProcessor->mergeAttributesRecursive(
566
            $this->getSerial(),
567
            array(RegistryKeys::GLOBAL_DATA => $globalData)
568
        );
569
570
        // log a message that the global data has been prepared
571
        $this->log(
572
            sprintf(
573
                'Successfully prepared global data for import with serial %s',
574
                $this->getSerial()
575
            ),
576
            LogLevel::INFO
577
        );
578
    }
579
580
    /**
581
     * Process all the subjects defined in the system configuration.
582
     *
583
     * @return void
584
     * @throws \Exception Is thrown, if one of the subjects can't be processed
585
     */
586
    protected function processSubjects()
587
    {
588
589
        try {
590
            // load system logger and registry
591
            $importProcessor = $this->getImportProcessor();
592
593
            // load the subjects
594
            $subjects = $this->getConfiguration()->getSubjects();
595
596
            // start the transaction
597
            $importProcessor->getConnection()->beginTransaction();
598
599
            // process all the subjects found in the system configuration
600
            foreach ($subjects as $subject) {
601
                $this->processSubject($subject);
602
            }
603
604
            // commit the transaction
605
            $importProcessor->getConnection()->commit();
606
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
607
        } catch (\Exception $e) {
608
            // rollback the transaction
609
            $importProcessor->getConnection()->rollBack();
610
611
            // re-throw the exception
612
            throw $e;
613
        }
614
    }
615
616
    /**
617
     * Process the subject with the passed name/identifier.
618
     *
619
     * We create a new, fresh and separate subject for EVERY file here, because this would be
620
     * the starting point to parallelize the import process in a multithreaded/multiprocessed
621
     * environment.
622
     *
623
     * @param \TechDivision\Import\Configuration\SubjectInterface $subject The subject configuration
624
     *
625
     * @return void
626
     * @throws \Exception Is thrown, if the subject can't be processed
627
     */
628
    protected function processSubject(\TechDivision\Import\Configuration\SubjectInterface $subject)
629
    {
630
631
        // clear the filecache
632
        clearstatcache();
633
634
        // load the actual status
635
        $status = $this->getRegistryProcessor()->getAttribute($serial = $this->getSerial());
636
637
        // query whether or not the configured source directory is available
638 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...
639
            throw new \Exception(sprintf('Configured source directory %s is not available!', $sourceDir));
640
        }
641
642
        // initialize the array with the CSV files found in the source directory
643
        $this->files = glob(sprintf('%s/*.csv', $sourceDir));
644
645
        // query whether or not CSV files to be imported are available
646
        if (sizeof($this->files) === 0) {
647
            // log a message that no files to be imported are found
648
            $this->log(
649
                sprintf(
650
                    'Can\'t find any CSV files for subject %s to be imported in directory %s',
651
                    $subject->getClassName(),
652
                    $sourceDir
653
                ),
654
                LogLevel::INFO
655
            );
656
            return;
657
        }
658
659
        // immediately add the PID to lock this import process
660
        $this->addPid($this->getSerial());
661
662
        // sorting the files for the apropriate order
663
        usort($files, function ($a, $b) {
664
            return strcmp($a, $b);
665
        });
666
667
        // log a debug message
668
        $this->log(
669
            sprintf(
670
                'Now checking directory %s for files to be imported',
671
                $sourceDir
672
            ),
673
            LogLevel::DEBUG
674
        );
675
676
        // iterate through all CSV files and process the subjects
677
        foreach ($this->files as $pathname) {
678
            // query whether or not that the file is part of the actual bunch
679
            if ($this->isPartOfBunch($subject->getPrefix(), $pathname)) {
680
                // initialize the subject and import the bunch
681
                $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...
682
683
                // query whether or not the subject needs an OK file,
684
                // if yes remove the filename from the file
685
                if ($subjectInstance->needsOkFile()) {
686
                    $this->removeFromOkFile($pathname);
687
                }
688
689
                // finally import the CSV file
690
                $subjectInstance->import($serial, $pathname);
691
692
                // query whether or not, we've to export artefacts
693
                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...
694
                    $subjectInstance->export(
695
                        $this->matches[BunchKeys::FILENAME],
696
                        $this->matches[BunchKeys::COUNTER]
697
                    );
698
                }
699
700
                // raise the number of the imported bunches
701
                $this->bunches++;
702
            }
703
        }
704
705
        // finally remove the PID from the file and the PID file itself, if empty
706
        $this->removePid($serial);
707
708
        // reset the matches, because the exported artefacts
709
        $this->matches = array();
710
711
        // and and log a message that the subject has been processed
712
        $this->log(
713
            sprintf(
714
                'Successfully processed subject %s with %d bunch(es)!',
715
                $subject->getClassName(),
716
                $this->bunches
717
            ),
718
            LogLevel::DEBUG
719
        );
720
    }
721
722
    /**
723
     * Queries whether or not, the passed filename is part of a bunch or not.
724
     *
725
     * @param string $prefix   The prefix to query for
726
     * @param string $filename The filename to query for
727
     *
728
     * @return boolean TRUE if the filename is part, else FALSE
729
     */
730 2
    public function isPartOfBunch($prefix, $filename)
731
    {
732
733
        // initialize the pattern
734 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...
735
736
        // query whether or not, this is the first file to be processed
737 2
        if (sizeof($this->matches) === 0) {
738
            // initialize the pattern to query whether the FIRST file has to be processed or not
739 2
            $pattern = sprintf(
740 2
                '/^.*\/(?<%s>%s)_(?<%s>.*)_(?<%s>\d+)\\.csv$/',
741 2
                BunchKeys::PREFIX,
742 2
                $prefix,
743 2
                BunchKeys::FILENAME,
744
                BunchKeys::COUNTER
745 2
            );
746
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
747 2
        } else {
748
            // initialize the pattern to query whether the NEXT file is part of a bunch or not
749 2
            $pattern = sprintf(
750 2
                '/^.*\/(?<%s>%s)_(?<%s>%s)_(?<%s>\d+)\\.csv$/',
751 2
                BunchKeys::PREFIX,
752 2
                $this->matches[BunchKeys::PREFIX],
753 2
                BunchKeys::FILENAME,
754 2
                $this->matches[BunchKeys::FILENAME],
755
                BunchKeys::COUNTER
756 2
            );
757
        }
758
759
        // initialize the array for the matches
760 2
        $matches = array();
761
762
        // update the matches, if the pattern matches
763 2
        if ($result = preg_match($pattern, $filename, $matches)) {
764 2
            $this->matches = $matches;
765 2
        }
766
767
        // stop processing, if the filename doesn't match
768 2
        return (boolean) $result;
769
    }
770
771
    /**
772
     * Return's an array with the names of the expected OK files for the actual subject.
773
     *
774
     * @return array The array with the expected OK filenames
775
     */
776
    protected function getOkFilenames()
777
    {
778
779
        // load the array with the available bunch keys
780
        $bunchKeys = BunchKeys::getAllKeys();
781
782
        // initialize the array for the available okFilenames
783
        $okFilenames = array();
784
785
        // prepare the OK filenames based on the found CSV file information
786
        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...
787
            // intialize the array for the parts of the names (prefix, filename + counter)
788
            $parts = array();
789
            // load the parts from the matches
790
            for ($z = 0; $z < $i; $z++) {
791
                $parts[] = $this->matches[$bunchKeys[$z]];
792
            }
793
794
            // query whether or not, the OK file exists, if yes append it
795
            if (file_exists($okFilename = sprintf('%s/%s.ok', $this->getSourceDir(), implode('_', $parts)))) {
796
                $okFilenames[] = $okFilename;
797
            }
798
        }
799
800
        // prepare and return the pattern for the OK file
801
        return $okFilenames;
802
    }
803
804
    /**
805
     * Query whether or not, the passed CSV filename is in the OK file. If the filename was found,
806
     * it'll be returned and the method return TRUE.
807
     *
808
     * If the filename is NOT in the OK file, the method return's FALSE and the CSV should NOT be
809
     * imported/moved.
810
     *
811
     * @param string $filename The CSV filename to query for
812
     *
813
     * @return void
814
     * @throws \Exception Is thrown, if the passed filename is NOT in the OK file or it can NOT be removed from it
815
     */
816
    protected function removeFromOkFile($filename)
817
    {
818
819
        try {
820
            // load the expected OK filenames
821
            $okFilenames = $this->getOkFilenames();
822
823
            // iterate over the found OK filenames (should usually be only one, but could be more)
824
            foreach ($okFilenames as $okFilename) {
825
                // if the OK filename matches the CSV filename AND the OK file is empty
826
                if (basename($filename, '.csv') === basename($okFilename, '.ok') && filesize($okFilename) === 0) {
827
                    unlink($okFilename);
828
                    return;
829
                }
830
831
                // else, remove the CSV filename from the OK file
832
                $this->removeLineFromFile(basename($filename), $okFilename);
833
                return;
834
            }
835
836
            // throw an exception if either no OK file has been found,
837
            // or the CSV file is not in one of the OK files
838
            throw new \Exception(
839
                sprintf(
840
                    'Can\'t found filename %s in one of the expected OK files: %s',
841
                    $filename,
842
                    implode(', ', $okFilenames)
843
                )
844
            );
845
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
846
        } catch (\Exception $e) {
847
            // wrap and re-throw the exception
848
            throw new \Exception(
849
                sprintf(
850
                    'Can\'t remove filename %s from OK file: %s',
851
                    $filename,
852
                    $okFilename
853
                ),
854
                null,
855
                $e
856
            );
857
        }
858
    }
859
860
    /**
861
     * Factory method to create new handler instances.
862
     *
863
     * @param \TechDivision\Import\Configuration\Subject $subject The subject configuration
864
     *
865
     * @return object The handler instance
866
     */
867
    public function subjectFactory($subject)
868
    {
869
870
        // load the subject class name
871
        $className = $subject->getClassName();
872
873
        // the database connection to use
874
        $connection = $this->getImportProcessor()->getConnection();
875
876
        // initialize a new handler with the passed class name
877
        $instance = new $className();
878
879
        // $instance the handler instance
880
        $instance->setConfiguration($subject);
881
        $instance->setSystemLogger($this->getSystemLogger());
882
        $instance->setRegistryProcessor($this->getRegistryProcessor());
883
884
        // instanciate and set the product processor, if specified
885
        if ($processorFactory = $subject->getProcessorFactory()) {
886
            $productProcessor = $processorFactory::factory($connection, $subject);
887
            $instance->setProductProcessor($productProcessor);
888
        }
889
890
        // return the subject instance
891
        return $instance;
892
    }
893
894
    /**
895
     * Lifecycle callback that will be inovked after the
896
     * import process has been finished.
897
     *
898
     * @return void
899
     * @throws \Exception Is thrown, if the
900
     */
901
    protected function archive()
902
    {
903
904
        // query whether or not, the import artefacts have to be archived
905
        if (!$this->getConfiguration()->haveArchiveArtefacts()) {
906
            $this->log(sprintf('Archiving functionality has not been activated'), LogLevel::INFO);
907
            return;
908
        }
909
910
        // if no files have been imported, return immediately
911
        if ($this->bunches === 0) {
912
            $this->log(sprintf('Found no files to archive'), LogLevel::INFO);
913
            return;
914
        }
915
916
        // clear the filecache
917
        clearstatcache();
918
919
        // load the actual status
920
        $status = $this->getRegistryProcessor()->getAttribute($this->getSerial());
921
922
        // query whether or not the configured source directory is available
923 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...
924
            throw new \Exception(sprintf('Configured source directory %s is not available!', $sourceDir));
925
        }
926
927
        // init file iterator on source directory
928
        $fileIterator = new \FilesystemIterator($sourceDir);
929
930
        // log the number of files that has to be archived
931
        $this->log(sprintf('Found %d files to archive in directory %s', $this->bunches, $sourceDir), LogLevel::INFO);
932
933
        // initialize the directory to create the archive in
934
        $archiveDir = sprintf('%s/%s', $this->getConfiguration()->getTargetDir(), $this->getConfiguration()->getArchiveDir());
935
936
        // query whether or not the directory already exists
937
        if (!is_dir($archiveDir)) {
938
            mkdir($archiveDir);
939
        }
940
941
        // create the ZIP archive
942
        $archive = new \ZipArchive();
943
        $archive->open($archiveFile = sprintf('%s/%s.zip', $archiveDir, $this->getSerial()), \ZipArchive::CREATE);
944
945
        // iterate through all files and add them to the ZIP archive
946
        foreach ($fileIterator as $filename) {
947
            $archive->addFile($filename);
948
        }
949
950
        // save the ZIP archive
951
        $archive->close();
952
953
        // finally remove the directory with the imported files
954
        $this->removeDir($sourceDir);
955
956
        // and and log a message that the import artefacts have been archived
957
        $this->log(sprintf('Successfully archived imported files to %s!', $archiveFile), LogLevel::INFO);
958
    }
959
960
    /**
961
     * Removes the passed directory recursively.
962
     *
963
     * @param string $src Name of the directory to remove
964
     *
965
     * @return void
966
     * @throws \Exception Is thrown, if the directory can not be removed
967
     */
968
    protected function removeDir($src)
969
    {
970
971
        // open the directory
972
        $dir = opendir($src);
973
974
        // remove files/folders recursively
975
        while (false !== ($file = readdir($dir))) {
976
            if (($file != '.') && ($file != '..')) {
977
                $full = $src . '/' . $file;
978
                if (is_dir($full)) {
979
                    $this->removeDir($full);
980
                } else {
981
                    if (!unlink($full)) {
982
                        throw new \Exception(sprintf('Can\'t remove file %s', $full));
983
                    }
984
                }
985
            }
986
        }
987
988
        // close handle and remove directory itself
989
        closedir($dir);
990
        if (!rmdir($src)) {
991
            throw new \Exception(sprintf('Can\'t remove directory %s', $src));
992
        }
993
    }
994
995
    /**
996
     * Simple method that writes the passed method the the console and the
997
     * system logger, if configured and a log level has been passed.
998
     *
999
     * @param string $msg      The message to log
1000
     * @param string $logLevel The log level to use
1001
     *
1002
     * @return void
1003
     */
1004
    protected function log($msg, $logLevel = null)
1005
    {
1006
1007
        // initialize the formatter helper
1008
        $helper = new FormatterHelper();
1009
1010
        // map the log level to the console style
1011
        $style = $this->mapLogLevelToStyle($logLevel);
1012
1013
        // format the message, according to the passed log level and write it to the console
1014
        $this->getOutput()->writeln($logLevel ? $helper->formatBlock($msg, $style) : $msg);
1015
1016
        // log the message if a log level has been passed
1017
        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...
1018
            $systemLogger->log($logLevel, $msg);
1019
        }
1020
    }
1021
1022
    /**
1023
     * Map's the passed log level to a valid symfony console style.
1024
     *
1025
     * @param string $logLevel The log level to map
1026
     *
1027
     * @return string The apropriate symfony console style
1028
     */
1029
    protected function mapLogLevelToStyle($logLevel)
1030
    {
1031
1032
        // query whether or not the log level is mapped
1033
        if (isset($this->logLevelStyleMapping[$logLevel])) {
1034
            return $this->logLevelStyleMapping[$logLevel];
1035
        }
1036
1037
        // return the default style => info
1038
        return Simple::DEFAULT_STYLE;
1039
    }
1040
1041
    /**
1042
     * Return's the PID filename to use.
1043
     *
1044
     * @return string The PID filename
1045
     */
1046
    protected function getPidFilename()
1047
    {
1048
        return sprintf('%s/%s', sys_get_temp_dir(), Simple::PID_FILENAME);
1049
    }
1050
1051
    /**
1052
     * Persist the passed PID to PID filename.
1053
     *
1054
     * @param string $pid The PID of the actual import process to added
1055
     *
1056
     * @return void
1057
     * @throws \Exception Is thrown, if the PID can not be added
1058
     */
1059
    protected function addPid($pid)
1060
    {
1061
1062
        // open the PID file
1063
        $fh = fopen($pidFilename = $this->getPidFilename(), 'a');
1064
1065
        // append the PID to the PID file
1066
        if (fwrite($fh, $pid . PHP_EOL) === false) {
1067
            throw new \Exception(sprintf('Can\'t write PID %s to PID file %s', $pid, $pidFilename));
1068
        }
1069
1070
        // close the file handle
1071
        fclose($fh);
1072
    }
1073
1074
    /**
1075
     * Remove's the actual PID from the PID file.
1076
     *
1077
     * @param string $pid The PID of the actual import process to be removed
1078
     *
1079
     * @return void
1080
     * @throws \Exception Is thrown, if the PID can not be removed
1081
     */
1082
    protected function removePid($pid)
1083
    {
1084
        try {
1085
            // remove the PID from the PID file
1086
            $this->removeLineFromFile($pid, $this->getPidFilename());
1087
        } catch (LineNotFoundException $lnfe) {
1088
            $this->getSystemLogger()->notice(sprintf('PID %s is can not be found in PID file %s', $pid, $this->getPidFilename()));
1089
        } catch (\Exception $e) {
1090
            throw new \Exception(sprintf('Can\'t remove PID %s from PID file %s', $pid, $this->getPidFilename()), null, $e);
1091
        }
1092
    }
1093
1094
    /**
1095
     * Lifecycle callback that will be inovked after the
1096
     * import process has been finished.
1097
     *
1098
     * @return void
1099
     */
1100
    protected function tearDown()
1101
    {
1102
    }
1103
1104
    /**
1105
     * This method finishes the import process and cleans the registry.
1106
     *
1107
     * @return void
1108
     */
1109
    protected function finish()
1110
    {
1111
        // remove the import status from the registry
1112
        $this->getRegistryProcessor()->removeAttribute($this->getSerial());
1113
    }
1114
1115
1116
    /**
1117
     * Remove's the passed line from the file with the passed name.
1118
     *
1119
     * @param string $line     The line to be removed
1120
     * @param string $filename The name of the file the line has to be removed
1121
     *
1122
     * @return void
1123
     * @throws \Exception Is thrown, if the line is not found or can not be removed
1124
     */
1125
    protected function removeLineFromFile($line, $filename)
1126
    {
1127
1128
        // open the PID file
1129
        $fh = fopen($filename, 'r+');
1130
1131
        // initialize the array for the PIDs found in the PID file
1132
        $lines = array();
1133
1134
        // initialize the flag if the line has been found
1135
        $found = false;
1136
1137
        // read the lines with the PIDs from the PID file
1138
        while (($buffer = fgets($fh, 4096)) !== false) {
1139
            // remove the new line
1140
            $buffer = trim($buffer, PHP_EOL);
1141
            // if the line is the one to be removed, ignore the line
1142
            if ($line === $buffer) {
1143
                $found = true;
1144
                continue;
1145
            }
1146
1147
            // add the found PID to the array
1148
            $lines[] = $buffer;
1149
        }
1150
1151
        // query whether or not, we found the line
1152
        if (!$found) {
1153
            throw new LineNotFoundException(sprintf('Line %s can not be found in file %s', $line, $filename));
1154
        }
1155
1156
        // if there are NO more lines, delete the file
1157
        if (sizeof($lines) === 0) {
1158
            fclose($fh);
1159
            unlink($filename);
1160
            return;
1161
        }
1162
1163
        // empty the file and rewind the file pointer
1164
        ftruncate($fh, 0);
1165
        rewind($fh);
1166
1167
        // append the existing lines to the file
1168
        foreach ($lines as $ln) {
1169
            if (fwrite($fh, $ln . PHP_EOL) === false) {
1170
                throw new \Exception(sprintf('Can\'t write %s to file %s', $ln, $filename));
1171
            }
1172
        }
1173
1174
        // finally close the file
1175
        fclose($fh);
1176
    }
1177
}
1178