Completed
Push — master ( 70cce3...6b7465 )
by Tim
02:52
created

Simple::processSubjects()   B

Complexity

Conditions 3
Paths 9

Size

Total Lines 29
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

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