Completed
Pull Request — master (#40)
by Tim
03:07
created

Simple::getConfiguration()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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