Completed
Push — master ( 324e90...8b0047 )
by Tim
9s
created

Simple::addPid()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 5

Duplication

Lines 3
Ratio 21.43 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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