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

Simple::removeFromOkFile()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 43
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 43
ccs 0
cts 15
cp 0
rs 8.439
cc 5
eloc 22
nc 7
nop 1
crap 30
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(
420
            sprintf(
421
                'Now start import with serial %s (operation: %s)',
422
                $this->getSerial(),
423
                $this->getConfiguration()->getOperationName()
424
            ),
425
            LogLevel::INFO
426
        );
427
428
        // initialize the status
429
        $status = array(
430
            RegistryKeys::STATUS => 1,
431
            RegistryKeys::SOURCE_DIRECTORY => $this->getConfiguration()->getSourceDir()
432
        );
433
434
        // initialize the status information for the subjects */
435
        /** @var \TechDivision\Import\Configuration\SubjectInterface $subject */
436
        foreach ($this->getConfiguration()->getSubjects() as $subject) {
437
            $status[$subject->getPrefix()] = array();
438
        }
439
440
        // append it to the registry
441
        $this->getRegistryProcessor()->setAttribute($this->getSerial(), $status);
442
    }
443
444
    /**
445
     * Prepares the global data for the import process.
446
     *
447
     * @return void
448
     */
449
    protected function setUp()
450
    {
451
452
        // load the registry
453
        $importProcessor = $this->getImportProcessor();
454
        $registryProcessor = $this->getRegistryProcessor();
455
456
        // initialize the array for the global data
457
        $globalData = array();
458
459
        // initialize the global data
460
        $globalData[RegistryKeys::STORES] = $importProcessor->getStores();
461
        $globalData[RegistryKeys::LINK_TYPES] = $importProcessor->getLinkTypes();
462
        $globalData[RegistryKeys::TAX_CLASSES] = $importProcessor->getTaxClasses();
463
        $globalData[RegistryKeys::DEFAULT_STORE] = $importProcessor->getDefaultStore();
464
        $globalData[RegistryKeys::STORE_WEBSITES] = $importProcessor->getStoreWebsites();
465
        $globalData[RegistryKeys::LINK_ATTRIBUTES] = $importProcessor->getLinkAttributes();
466
        $globalData[RegistryKeys::ROOT_CATEGORIES] = $importProcessor->getRootCategories();
467
        $globalData[RegistryKeys::CORE_CONFIG_DATA] = $importProcessor->getCoreConfigData();
468
        $globalData[RegistryKeys::ATTRIBUTE_SETS] = $eavAttributeSets = $importProcessor->getEavAttributeSetsByEntityTypeId(4);
469
470
        // prepare the categories
471
        $categories = array();
472
        foreach ($importProcessor->getCategories() as $category) {
473
            // expload the entity IDs from the category path
474
            $entityIds = explode('/', $category[MemberNames::PATH]);
475
476
            // cut-off the root category
477
            array_shift($entityIds);
478
479
            // continue with the next category if no entity IDs are available
480
            if (sizeof($entityIds) === 0) {
481
                continue;
482
            }
483
484
            // initialize the array for the path elements
485
            $path = array();
486
            foreach ($importProcessor->getCategoryVarcharsByEntityIds($entityIds) as $cat) {
487
                $path[] = $cat[MemberNames::VALUE];
488
            }
489
490
            // append the catogory with the string path as key
491
            $categories[implode('/', $path)] = $category;
492
        }
493
494
        // initialize the array with the categories
495
        $globalData[RegistryKeys::CATEGORIES] = $categories;
496
497
        // prepare an array with the EAV attributes grouped by their attribute set name as keys
498
        $eavAttributes = array();
499
        foreach (array_keys($eavAttributeSets) as $eavAttributeSetName) {
500
            $eavAttributes[$eavAttributeSetName] = $importProcessor->getEavAttributesByEntityTypeIdAndAttributeSetName(4, $eavAttributeSetName);
501
        }
502
503
        // initialize the array with the EAV attributes
504
        $globalData[RegistryKeys::EAV_ATTRIBUTES] = $eavAttributes;
505
506
        // add the status with the global data
507
        $registryProcessor->mergeAttributesRecursive(
508
            $this->getSerial(),
509
            array(RegistryKeys::GLOBAL_DATA => $globalData)
510
        );
511
512
        // log a message that the global data has been prepared
513
        $this->log(sprintf('Successfully prepared global data for import with serial %s', $this->getSerial()), LogLevel::INFO);
514
    }
515
516
    /**
517
     * Process all the subjects defined in the system configuration.
518
     *
519
     * @return void
520
     * @throws \Exception Is thrown, if one of the subjects can't be processed
521
     */
522
    protected function processSubjects()
523
    {
524
525
        try {
526
            // load system logger and registry
527
            $importProcessor = $this->getImportProcessor();
528
529
            // load the subjects
530
            $subjects = $this->getConfiguration()->getSubjects();
531
532
            // start the transaction
533
            $importProcessor->getConnection()->beginTransaction();
534
535
            // process all the subjects found in the system configuration
536
            foreach ($subjects as $subject) {
537
                $this->processSubject($subject);
538
            }
539
540
            // commit the transaction
541
            $importProcessor->getConnection()->commit();
542
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
543
        } catch (\Exception $e) {
544
            // rollback the transaction
545
            $importProcessor->getConnection()->rollBack();
546
547
            // re-throw the exception
548
            throw $e;
549
        }
550
    }
551
552
    /**
553
     * Process the subject with the passed name/identifier.
554
     *
555
     * We create a new, fresh and separate subject for EVERY file here, because this would be
556
     * the starting point to parallelize the import process in a multithreaded/multiprocessed
557
     * environment.
558
     *
559
     * @param \TechDivision\Import\Configuration\SubjectInterface $subject The subject configuration
560
     *
561
     * @return void
562
     * @throws \Exception Is thrown, if the subject can't be processed
563
     */
564
    protected function processSubject(\TechDivision\Import\Configuration\SubjectInterface $subject)
565
    {
566
567
        // clear the filecache
568
        clearstatcache();
569
570
        // load the actual status
571
        $status = $this->getRegistryProcessor()->getAttribute($this->getSerial());
572
573
        // query whether or not the configured source directory is available
574 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...
575
            throw new \Exception(sprintf('Configured source directory %s is not available!', $sourceDir));
576
        }
577
578
        // initialize the file iterator on source directory
579
        $files = glob(sprintf('%s/*.csv', $sourceDir));
580
581
        // sorting the files for the apropriate order
582
        usort($files, function ($a, $b) {
583
            return strcmp($a, $b);
584
        });
585
586
        // log a debug message
587
        $this->log(sprintf('Now checking directory %s for files to be imported', $sourceDir), LogLevel::DEBUG);
588
589
        // iterate through all CSV files and process the subjects
590
        foreach ($files as $pathname) {
591
            // query whether or not that the file is part of the actual bunch
592
            if ($this->isPartOfBunch($subject->getPrefix(), $pathname)) {
593
                // initialize the subject and import the bunch
594
                $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...
595
596
                // query whether or not the subject needs an OK file, if yes remove the filename from the file
597
                if ($subjectInstance->needsOkFile()) {
598
                    $this->removeFromOkFile($pathname);
599
                }
600
601
                // finally import the CSV file
602
                $subjectInstance->import($this->getSerial(), $pathname);
603
604
                // query whether or not, we've to export artefacts
605
                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...
606
                    $subjectInstance->export(
607
                        $this->matches[BunchKeys::FILENAME],
608
                        $this->matches[BunchKeys::COUNTER]
609
                    );
610
                }
611
612
                // raise the number of the imported bunches
613
                $this->bunches++;
614
            }
615
        }
616
617
        // reset the matches, because the exported artefacts
618
        $this->matches = array();
619
620
        // and and log a message that the subject has been processed
621
        $this->log(sprintf('Successfully processed subject %s with %d bunch(es)!', $subject->getClassName(), $this->bunches), LogLevel::DEBUG);
622
    }
623
624
    /**
625
     * Queries whether or not, the passed filename is part of a bunch or not.
626
     *
627
     * @param string $prefix   The prefix to query for
628
     * @param string $filename The filename to query for
629
     *
630
     * @return boolean TRUE if the filename is part, else FALSE
631
     */
632 2
    public function isPartOfBunch($prefix, $filename)
633
    {
634
635
        // initialize the pattern
636 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...
637
638
        // query whether or not, this is the first file to be processed
639 2
        if (sizeof($this->matches) === 0) {
640
            // initialize the pattern to query whether the FIRST file has to be processed or not
641 2
            $pattern = sprintf(
642 2
                '/^.*\/(?<%s>%s)_(?<%s>.*)_(?<%s>\d+)\\.csv$/',
643 2
                BunchKeys::PREFIX,
644
                $prefix,
645 2
                BunchKeys::FILENAME,
646 2
                BunchKeys::COUNTER
647
            );
648
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
649
        } else {
650
            // initialize the pattern to query whether the NEXT file is part of a bunch or not
651 2
            $pattern = sprintf(
652 2
                '/^.*\/(?<%s>%s)_(?<%s>%s)_(?<%s>\d+)\\.csv$/',
653 2
                BunchKeys::PREFIX,
654 2
                $this->matches[BunchKeys::PREFIX],
655 2
                BunchKeys::FILENAME,
656 2
                $this->matches[BunchKeys::FILENAME],
657 2
                BunchKeys::COUNTER
658
            );
659
        }
660
661
        // initialize the array for the matches
662 2
        $matches = array();
663
664
        // update the matches, if the pattern matches
665 2
        if ($result = preg_match($pattern, $filename, $matches)) {
666 2
            $this->matches = $matches;
667
        }
668
669
        // stop processing, if the filename doesn't match
670 2
        return (boolean) $result;
671
    }
672
673
    /**
674
     * Return's an array with the names of the expected OK files for the actual subject.
675
     *
676
     * @return array The array with the expected OK filenames
677
     */
678
    protected function getOkFilenames()
679
    {
680
681
        // load the array with the available bunch keys
682
        $bunchKeys = BunchKeys::getAllKeys();
683
684
        // initialize the array for the available okFilenames
685
        $okFilenames = array();
686
687
        // prepare the OK filenames based on the found CSV file information
688
        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...
689
            // intialize the array for the parts of the names (prefix, filename + counter)
690
            $parts = array();
691
            // load the parts from the matches
692
            for ($z = 0; $z < $i; $z++) {
693
                $parts[] = $this->matches[$bunchKeys[$z]];
694
            }
695
696
            // query whether or not, the OK file exists, if yes append it
697
            if (file_exists($okFilename = sprintf('%s/%s.ok', $this->getSourceDir(), implode('_', $parts)))) {
698
                $okFilenames[] = $okFilename;
699
            }
700
        }
701
702
        // prepare and return the pattern for the OK file
703
        return $okFilenames;
704
    }
705
706
    /**
707
     * Query whether or not, the passed CSV filename is in the OK file. If the filename was found,
708
     * it'll be returned and the method return TRUE.
709
     *
710
     * If the filename is NOT in the OK file, the method return's FALSE and the CSV should NOT be
711
     * imported/moved.
712
     *
713
     * @param string $filename The CSV filename to query for
714
     *
715
     * @return void
716
     * @throws \Exception Is thrown, if the passed filename is NOT in the OK file or it can NOT be removed from it
717
     */
718
    protected function removeFromOkFile($filename)
719
    {
720
721
        try {
722
            // load the expected OK filenames
723
            $okFilenames = $this->getOkFilenames();
724
725
            // iterate over the found OK filenames (should usually be only one, but could be more)
726
            foreach ($okFilenames as $okFilename) {
727
                // if the OK filename matches the CSV filename AND the OK file is empty
728
                if (basename($filename, '.csv') === basename($okFilename, '.ok') && filesize($okFilename) === 0) {
729
                    unlink($okFilename);
730
                    return;
731
                }
732
733
                // else, remove the CSV filename from the OK file
734
                $this->removeLineFromFile(basename($filename), $okFilename);
735
                return;
736
            }
737
738
            // throw an exception if either no OK file has been found,
739
            // or the CSV file is not in one of the OK files
740
            throw new \Exception(
741
                sprintf(
742
                    'Can\'t found filename %s in one of the expected OK files: %s',
743
                    $filename,
744
                    implode(', ', $okFilenames)
745
                )
746
            );
747
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
748
        } catch (\Exception $e) {
749
            // wrap and re-throw the exception
750
            throw new \Exception(
751
                sprintf(
752
                    'Can\'t remove filename %s from OK file: %s',
753
                    $filename,
754
                    $okFilename
755
                ),
756
                null,
757
                $e
758
            );
759
        }
760
    }
761
762
    /**
763
     * Factory method to create new handler instances.
764
     *
765
     * @param \TechDivision\Import\Configuration\Subject $subject The subject configuration
766
     *
767
     * @return object The handler instance
768
     */
769
    public function subjectFactory($subject)
770
    {
771
772
        // load the subject class name
773
        $className = $subject->getClassName();
774
775
        // the database connection to use
776
        $connection = $this->getImportProcessor()->getConnection();
777
778
        // initialize a new handler with the passed class name
779
        $instance = new $className();
780
781
        // $instance the handler instance
782
        $instance->setConfiguration($subject);
783
        $instance->setSystemLogger($this->getSystemLogger());
784
        $instance->setRegistryProcessor($this->getRegistryProcessor());
785
786
        // instanciate and set the product processor, if specified
787
        if ($processorFactory = $subject->getProcessorFactory()) {
788
            $productProcessor = $processorFactory::factory($connection, $subject);
789
            $instance->setProductProcessor($productProcessor);
790
        }
791
792
        // return the subject instance
793
        return $instance;
794
    }
795
796
    /**
797
     * Lifecycle callback that will be inovked after the
798
     * import process has been finished.
799
     *
800
     * @return void
801
     * @throws \Exception Is thrown, if the
802
     */
803
    protected function archive()
804
    {
805
806
        // query whether or not, the import artefacts have to be archived
807
        if (!$this->getConfiguration()->haveArchiveArtefacts()) {
808
            $this->log(sprintf('Archiving functionality has not been activated'), LogLevel::INFO);
809
            return;
810
        }
811
812
        // if no files have been imported, return immediately
813
        if ($this->bunches === 0) {
814
            $this->log(sprintf('Found no files to archive'), LogLevel::INFO);
815
            return;
816
        }
817
818
        // clear the filecache
819
        clearstatcache();
820
821
        // load the actual status
822
        $status = $this->getRegistryProcessor()->getAttribute($this->getSerial());
823
824
        // query whether or not the configured source directory is available
825 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...
826
            throw new \Exception(sprintf('Configured source directory %s is not available!', $sourceDir));
827
        }
828
829
        // init file iterator on source directory
830
        $fileIterator = new \FilesystemIterator($sourceDir);
831
832
        // log the number of files that has to be archived
833
        $this->log(sprintf('Found %d files to archive in directory %s', $this->bunches, $sourceDir), LogLevel::INFO);
834
835
        // initialize the directory to create the archive in
836
        $archiveDir = sprintf('%s/%s', $this->getConfiguration()->getTargetDir(), $this->getConfiguration()->getArchiveDir());
837
838
        // query whether or not the directory already exists
839
        if (!is_dir($archiveDir)) {
840
            mkdir($archiveDir);
841
        }
842
843
        // create the ZIP archive
844
        $archive = new \ZipArchive();
845
        $archive->open($archiveFile = sprintf('%s/%s.zip', $archiveDir, $this->getSerial()), \ZipArchive::CREATE);
846
847
        // iterate through all files and add them to the ZIP archive
848
        foreach ($fileIterator as $filename) {
849
            $archive->addFile($filename);
850
        }
851
852
        // save the ZIP archive
853
        $archive->close();
854
855
        // finally remove the directory with the imported files
856
        $this->removeDir($sourceDir);
857
858
        // and and log a message that the import artefacts have been archived
859
        $this->log(sprintf('Successfully archived imported files to %s!', $archiveFile), LogLevel::INFO);
860
    }
861
862
    /**
863
     * Removes the passed directory recursively.
864
     *
865
     * @param string $src Name of the directory to remove
866
     *
867
     * @return void
868
     * @throws \Exception Is thrown, if the directory can not be removed
869
     */
870
    protected function removeDir($src)
871
    {
872
873
        // open the directory
874
        $dir = opendir($src);
875
876
        // remove files/folders recursively
877
        while (false !== ($file = readdir($dir))) {
878
            if (($file != '.') && ($file != '..')) {
879
                $full = $src . '/' . $file;
880
                if (is_dir($full)) {
881
                    $this->removeDir($full);
882
                } else {
883
                    if (!unlink($full)) {
884
                        throw new \Exception(sprintf('Can\'t remove file %s', $full));
885
                    }
886
                }
887
            }
888
        }
889
890
        // close handle and remove directory itself
891
        closedir($dir);
892
        if (!rmdir($src)) {
893
            throw new \Exception(sprintf('Can\'t remove directory %s', $src));
894
        }
895
    }
896
897
    /**
898
     * Simple method that writes the passed method the the console and the
899
     * system logger, if configured and a log level has been passed.
900
     *
901
     * @param string $msg      The message to log
902
     * @param string $logLevel The log level to use
903
     *
904
     * @return void
905
     */
906
    protected function log($msg, $logLevel = null)
907
    {
908
909
        // initialize the formatter helper
910
        $helper = new FormatterHelper();
911
912
        // map the log level to the console style
913
        $style = $this->mapLogLevelToStyle($logLevel);
914
915
        // format the message, according to the passed log level and write it to the console
916
        $this->getOutput()->writeln($logLevel ? $helper->formatBlock($msg, $style) : $msg);
917
918
        // log the message if a log level has been passed
919
        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...
920
            $systemLogger->log($logLevel, $msg);
921
        }
922
    }
923
924
    /**
925
     * Map's the passed log level to a valid symfony console style.
926
     *
927
     * @param string $logLevel The log level to map
928
     *
929
     * @return string The apropriate symfony console style
930
     */
931
    protected function mapLogLevelToStyle($logLevel)
932
    {
933
934
        // query whether or not the log level is mapped
935
        if (isset($this->logLevelStyleMapping[$logLevel])) {
936
            return $this->logLevelStyleMapping[$logLevel];
937
        }
938
939
        // return the default style => info
940
        return Simple::DEFAULT_STYLE;
941
    }
942
943
    /**
944
     * Return's the PID filename to use.
945
     *
946
     * @return string The PID filename
947
     */
948
    protected function getPidFilename()
949
    {
950
        return sprintf('%s/%s', sys_get_temp_dir(), Simple::PID_FILENAME);
951
    }
952
953
    /**
954
     * Persist the passed PID to PID filename.
955
     *
956
     * @param string $pid The PID of the actual import process to added
957
     *
958
     * @return void
959
     * @throws \Exception Is thrown, if the PID can not be added
960
     */
961
    protected function addPid($pid)
962
    {
963
964
        // open the PID file
965
        $fh = fopen($pidFilename = $this->getPidFilename(), 'a');
966
967
        // append the PID to the PID file
968
        if (fwrite($fh, $pid . PHP_EOL) === false) {
969
            throw new \Exception(sprintf('Can\'t write PID %s to PID file %s', $pid, $pidFilename));
970
        }
971
972
        // close the file handle
973
        fclose($fh);
974
    }
975
976
    /**
977
     * Remove's the actual PID from the PID file.
978
     *
979
     * @param string $pid The PID of the actual import process to be removed
980
     *
981
     * @return void
982
     * @throws \Exception Is thrown, if the PID can not be removed
983
     */
984
    protected function removePid($pid)
985
    {
986
        try {
987
            // remove the PID from the PID file
988
            $this->removeLineFromFile($pid, $this->getPidFilename());
989
        } catch (LineNotFoundException $lnfe) {
990
            $this->getSystemLogger()->notice(sprintf('PID % is can not be found in PID file %s', $pid, $this->getPidFilename()));
991
        } catch (\Exception $e) {
992
            throw new \Exception(sprintf('Can\'t remove PID %s from PID file %s', $pid, $this->getPidFilename()), null, $e);
993
        }
994
    }
995
996
    /**
997
     * Lifecycle callback that will be inovked after the
998
     * import process has been finished.
999
     *
1000
     * @return void
1001
     */
1002
    protected function tearDown()
1003
    {
1004
        // finally remove the PID from the file and the PID file itself, if empty
1005
        $this->removePid($this->getSerial());
1006
    }
1007
1008
    /**
1009
     * This method finishes the import process and cleans the registry.
1010
     *
1011
     * @return void
1012
     */
1013
    protected function finish()
1014
    {
1015
        // remove the import status from the registry
1016
        $this->getRegistryProcessor()->removeAttribute($this->getSerial());
1017
    }
1018
1019
1020
    /**
1021
     * Remove's the passed line from the file with the passed name.
1022
     *
1023
     * @param string $line     The line to be removed
1024
     * @param string $filename The name of the file the line has to be removed
1025
     *
1026
     * @return void
1027
     * @throws \Exception Is thrown, if the line is not found or can not be removed
1028
     */
1029
    protected function removeLineFromFile($line, $filename)
1030
    {
1031
1032
        // open the PID file
1033
        $fh = fopen($filename, 'r+');
1034
1035
        // initialize the array for the PIDs found in the PID file
1036
        $lines = array();
1037
1038
        // initialize the flag if the line has been found
1039
        $found = false;
1040
1041
        // read the lines with the PIDs from the PID file
1042
        while (($buffer = fgets($fh, 4096)) !== false) {
1043
            // remove the new line
1044
            $buffer = trim($buffer, PHP_EOL);
1045
            // if the line is the one to be removed, ignore the line
1046
            if ($line === $buffer) {
1047
                $found = true;
1048
                continue;
1049
            }
1050
1051
            // add the found PID to the array
1052
            $lines[] = $buffer;
1053
        }
1054
1055
        // query whether or not, we found the line
1056
        if (!$found) {
1057
            throw new LineNotFoundException(sprintf('Line %s can not be found in file %s', $line, $filename));
1058
        }
1059
1060
        // if there are NO more lines, delete the file
1061
        if (sizeof($lines) === 0) {
1062
            fclose($fh);
1063
            unlink($filename);
1064
            return;
1065
        }
1066
1067
        // empty the file and rewind the file pointer
1068
        ftruncate($fh, 0);
1069
        rewind($fh);
1070
1071
        // append the existing lines to the file
1072
        foreach ($lines as $ln) {
1073
            if (fwrite($fh, $ln . PHP_EOL) === false) {
1074
                throw new \Exception(sprintf('Can\'t write %s to file %s', $ln, $filename));
1075
            }
1076
        }
1077
1078
        // finally close the file
1079
        fclose($fh);
1080
    }
1081
}
1082