Completed
Pull Request — master (#43)
by Tim
03:25
created

Simple::start()   B

Complexity

Conditions 8
Paths 13

Size

Total Lines 55
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 7
Bugs 1 Features 1
Metric Value
c 7
b 1
f 1
dl 0
loc 55
ccs 0
cts 25
cp 0
rs 7.4033
cc 8
eloc 26
nc 13
nop 0
crap 72

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * TechDivision\Import\Cli\Simple
5
 *
6
 * NOTICE OF LICENSE
7
 *
8
 * This source file is subject to the Open Software License (OSL 3.0)
9
 * that is available through the world-wide-web at this URL:
10
 * http://opensource.org/licenses/osl-3.0.php
11
 *
12
 * PHP version 5
13
 *
14
 * @author    Tim Wagner <[email protected]>
15
 * @copyright 2016 TechDivision GmbH <[email protected]>
16
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
17
 * @link      https://github.com/techdivision/import-cli-simple
18
 * @link      http://www.techdivision.com
19
 */
20
21
namespace TechDivision\Import\Cli;
22
23
use Rhumsaa\Uuid\Uuid;
24
use Monolog\Logger;
25
use Psr\Log\LogLevel;
26
use Psr\Log\LoggerInterface;
27
use Symfony\Component\Console\Input\InputInterface;
28
use Symfony\Component\Console\Output\OutputInterface;
29
use Symfony\Component\Console\Helper\FormatterHelper;
30
use TechDivision\Import\Utils\BunchKeys;
31
use TechDivision\Import\Utils\MemberNames;
32
use TechDivision\Import\Utils\RegistryKeys;
33
use TechDivision\Import\ConfigurationInterface;
34
use TechDivision\Import\Subjects\SubjectInterface;
35
use TechDivision\Import\Services\ImportProcessorInterface;
36
use TechDivision\Import\Services\RegistryProcessorInterface;
37
use TechDivision\Import\Subjects\ExportableSubjectInterface;
38
use TechDivision\Import\Cli\Exceptions\LineNotFoundException;
39
40
/**
41
 * The M2IF - Console Tool implementation.
42
 *
43
 * This is a example console tool implementation that should give developers an impression
44
 * on how the M2IF could be used to implement their own Magento 2 importer.
45
 *
46
 * @author    Tim Wagner <[email protected]>
47
 * @copyright 2016 TechDivision GmbH <[email protected]>
48
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
49
 * @link      https://github.com/techdivision/import-cli-simple
50
 * @link      http://www.techdivision.com
51
 */
52
class Simple
53
{
54
55
    /**
56
     * The default style to write messages to the symfony console.
57
     *
58
     * @var string
59
     */
60
    const DEFAULT_STYLE = 'info';
61
62
    /**
63
     * The PID filename to use.
64
     *
65
     * @var string
66
     */
67
    const PID_FILENAME = 'importer.pid';
68
69
    /**
70
     * The TechDivision company name as ANSI art.
71
     *
72
     * @var string
73
     */
74
    protected $ansiArt = ' _______        _     _____  _       _     _
75
|__   __|      | |   |  __ \(_)     (_)   (_)
76
   | | ___  ___| |__ | |  | |___   ___ ___ _  ___  _ __
77
   | |/ _ \/ __| \'_ \| |  | | \ \ / / / __| |/ _ \| \'_ \
78
   | |  __/ (__| | | | |__| | |\ V /| \__ \ | (_) | | | |
79
   |_|\___|\___|_| |_|_____/|_| \_/ |_|___/_|\___/|_| |_|
80
';
81
82
    /**
83
     * The log level => console style mapping.
84
     *
85
     * @var array
86
     */
87
    protected $logLevelStyleMapping = array(
88
        LogLevel::INFO      => 'info',
89
        LogLevel::DEBUG     => 'comment',
90
        LogLevel::ERROR     => 'error',
91
        LogLevel::ALERT     => 'error',
92
        LogLevel::CRITICAL  => 'error',
93
        LogLevel::EMERGENCY => 'error',
94
        LogLevel::WARNING   => 'error',
95
        LogLevel::NOTICE    => 'info'
96
    );
97
98
    /**
99
     * The actions unique serial.
100
     *
101
     * @var string
102
     */
103
    protected $serial;
104
105
    /**
106
     * The system logger implementation.
107
     *
108
     * @var \Psr\Log\LoggerInterface
109
     */
110
    protected $systemLogger;
111
112
    /**
113
     * The RegistryProcessor instance to handle running threads.
114
     *
115
     * @var \TechDivision\Import\Services\RegistryProcessorInterface
116
     */
117
    protected $registryProcessor;
118
119
    /**
120
     * The processor to read/write the necessary import data.
121
     *
122
     * @var \TechDivision\Import\Services\ImportProcessorInterface
123
     */
124
    protected $importProcessor;
125
126
    /**
127
     * The system configuration.
128
     *
129
     * @var \TechDivision\Import\ConfigurationInterface
130
     */
131
    protected $configuration;
132
133
    /**
134
     * The input stream to read console information from.
135
     *
136
     * @var \Symfony\Component\Console\Input\InputInterface
137
     */
138
    protected $input;
139
140
    /**
141
     * The output stream to write console information to.
142
     *
143
     * @var \Symfony\Component\Console\Output\OutputInterface
144
     */
145
    protected $output;
146
147
    /**
148
     * The matches for the last processed CSV filename.
149
     *
150
     * @var array
151
     */
152
    protected $matches = array();
153
154
    /**
155
     * The number of imported bunches.
156
     *
157
     * @var integer
158
     */
159
    protected $bunches = 0;
160
161
    /**
162
     * The PID for the running processes.
163
     *
164
     * @var array
165
     */
166
    protected $pid = null;
167
168
    /**
169
     * 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
            // remove the filename from the firs OK file that can be found
726
            foreach ($okFilenames as $okFilename) {
727
                $this->removeLineFromFile(basename($filename), $okFilename);
728
                return;
729
            }
730
731
            throw new \Exception(sprintf('Can\'t remove filename %s from one of the expected OK files: %s', $filename, implode(', ', $okFilenames)));
732
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
733
        } catch (\Exception $e) {
734
            throw new \Exception(sprintf('Can\'t remove filename %s from OK file: %s', $filename, $okFilename), null, $e);
735
        }
736
    }
737
738
    /**
739
     * Factory method to create new handler instances.
740
     *
741
     * @param \TechDivision\Import\Configuration\Subject $subject The subject configuration
742
     *
743
     * @return object The handler instance
744
     */
745
    public function subjectFactory($subject)
746
    {
747
748
        // load the subject class name
749
        $className = $subject->getClassName();
750
751
        // the database connection to use
752
        $connection = $this->getImportProcessor()->getConnection();
753
754
        // initialize a new handler with the passed class name
755
        $instance = new $className();
756
757
        // $instance the handler instance
758
        $instance->setConfiguration($subject);
759
        $instance->setSystemLogger($this->getSystemLogger());
760
        $instance->setRegistryProcessor($this->getRegistryProcessor());
761
762
        // instanciate and set the product processor, if specified
763
        if ($processorFactory = $subject->getProcessorFactory()) {
764
            $productProcessor = $processorFactory::factory($connection, $subject);
765
            $instance->setProductProcessor($productProcessor);
766
        }
767
768
        // return the subject instance
769
        return $instance;
770
    }
771
772
    /**
773
     * Lifecycle callback that will be inovked after the
774
     * import process has been finished.
775
     *
776
     * @return void
777
     * @throws \Exception Is thrown, if the
778
     */
779
    protected function archive()
780
    {
781
782
        // query whether or not, the import artefacts have to be archived
783
        if (!$this->getConfiguration()->haveArchiveArtefacts()) {
784
            $this->log(sprintf('Archiving functionality has not been activated'), LogLevel::INFO);
785
            return;
786
        }
787
788
        // if no files have been imported, return immediately
789
        if ($this->bunches === 0) {
790
            $this->log(sprintf('Found no files to archive'), LogLevel::INFO);
791
            return;
792
        }
793
794
        // clear the filecache
795
        clearstatcache();
796
797
        // load the actual status
798
        $status = $this->getRegistryProcessor()->getAttribute($this->getSerial());
799
800
        // query whether or not the configured source directory is available
801 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...
802
            throw new \Exception(sprintf('Configured source directory %s is not available!', $sourceDir));
803
        }
804
805
        // init file iterator on source directory
806
        $fileIterator = new \FilesystemIterator($sourceDir);
807
808
        // log the number of files that has to be archived
809
        $this->log(sprintf('Found %d files to archive in directory %s', $this->bunches, $sourceDir), LogLevel::INFO);
810
811
        // initialize the directory to create the archive in
812
        $archiveDir = sprintf('%s/%s', $this->getConfiguration()->getTargetDir(), $this->getConfiguration()->getArchiveDir());
813
814
        // query whether or not the directory already exists
815
        if (!is_dir($archiveDir)) {
816
            mkdir($archiveDir);
817
        }
818
819
        // create the ZIP archive
820
        $archive = new \ZipArchive();
821
        $archive->open($archiveFile = sprintf('%s/%s.zip', $archiveDir, $this->getSerial()), \ZipArchive::CREATE);
822
823
        // iterate through all files and add them to the ZIP archive
824
        foreach ($fileIterator as $filename) {
825
            $archive->addFile($filename);
826
        }
827
828
        // save the ZIP archive
829
        $archive->close();
830
831
        // finally remove the directory with the imported files
832
        $this->removeDir($sourceDir);
833
834
        // and and log a message that the import artefacts have been archived
835
        $this->log(sprintf('Successfully archived imported files to %s!', $archiveFile), LogLevel::INFO);
836
    }
837
838
    /**
839
     * Removes the passed directory recursively.
840
     *
841
     * @param string $src Name of the directory to remove
842
     *
843
     * @return void
844
     * @throws \Exception Is thrown, if the directory can not be removed
845
     */
846
    protected function removeDir($src)
847
    {
848
849
        // open the directory
850
        $dir = opendir($src);
851
852
        // remove files/folders recursively
853
        while (false !== ($file = readdir($dir))) {
854
            if (($file != '.') && ($file != '..')) {
855
                $full = $src . '/' . $file;
856
                if (is_dir($full)) {
857
                    $this->removeDir($full);
858
                } else {
859
                    if (!unlink($full)) {
860
                        throw new \Exception(sprintf('Can\'t remove file %s', $full));
861
                    }
862
                }
863
            }
864
        }
865
866
        // close handle and remove directory itself
867
        closedir($dir);
868
        if (!rmdir($src)) {
869
            throw new \Exception(sprintf('Can\'t remove directory %s', $src));
870
        }
871
    }
872
873
    /**
874
     * Simple method that writes the passed method the the console and the
875
     * system logger, if configured and a log level has been passed.
876
     *
877
     * @param string $msg      The message to log
878
     * @param string $logLevel The log level to use
879
     *
880
     * @return void
881
     */
882
    protected function log($msg, $logLevel = null)
883
    {
884
885
        // initialize the formatter helper
886
        $helper = new FormatterHelper();
887
888
        // map the log level to the console style
889
        $style = $this->mapLogLevelToStyle($logLevel);
890
891
        // format the message, according to the passed log level and write it to the console
892
        $this->getOutput()->writeln($logLevel ? $helper->formatBlock($msg, $style) : $msg);
893
894
        // log the message if a log level has been passed
895
        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...
896
            $systemLogger->log($logLevel, $msg);
897
        }
898
    }
899
900
    /**
901
     * Map's the passed log level to a valid symfony console style.
902
     *
903
     * @param string $logLevel The log level to map
904
     *
905
     * @return string The apropriate symfony console style
906
     */
907
    protected function mapLogLevelToStyle($logLevel)
908
    {
909
910
        // query whether or not the log level is mapped
911
        if (isset($this->logLevelStyleMapping[$logLevel])) {
912
            return $this->logLevelStyleMapping[$logLevel];
913
        }
914
915
        // return the default style => info
916
        return Simple::DEFAULT_STYLE;
917
    }
918
919
    /**
920
     * Return's the PID filename to use.
921
     *
922
     * @return string The PID filename
923
     */
924
    protected function getPidFilename()
925
    {
926
        return sprintf('%s/%s', sys_get_temp_dir(), Simple::PID_FILENAME);
927
    }
928
929
    /**
930
     * Persist the passed PID to PID filename.
931
     *
932
     * @param string $pid The PID of the actual import process to added
933
     *
934
     * @return void
935
     * @throws \Exception Is thrown, if the PID can not be added
936
     */
937
    protected function addPid($pid)
938
    {
939
940
        // open the PID file
941
        $fh = fopen($pidFilename = $this->getPidFilename(), 'a');
942
943
        // append the PID to the PID file
944
        if (fwrite($fh, $pid . PHP_EOL) === false) {
945
            throw new \Exception(sprintf('Can\'t write PID %s to PID file %s', $pid, $pidFilename));
946
        }
947
948
        // close the file handle
949
        fclose($fh);
950
    }
951
952
    /**
953
     * Remove's the actual PID from the PID file.
954
     *
955
     * @param string $pid The PID of the actual import process to be removed
956
     *
957
     * @return void
958
     * @throws \Exception Is thrown, if the PID can not be removed
959
     */
960
    protected function removePid($pid)
961
    {
962
        try {
963
            // remove the PID from the PID file
964
            $this->removeLineFromFile($pid, $this->getPidFilename());
965
        } catch (LineNotFoundException $lnfe) {
966
            $this->getSystemLogger()->notice(sprintf('PID % is can not be found in PID file %s', $pid, $this->getPidFilename()));
967
        } catch (\Exception $e) {
968
            throw new \Exception(sprintf('Can\'t remove PID %s from PID file %s', $pid, $this->getPidFilename()), null, $e);
969
        }
970
    }
971
972
    /**
973
     * Lifecycle callback that will be inovked after the
974
     * import process has been finished.
975
     *
976
     * @return void
977
     */
978
    protected function tearDown()
979
    {
980
        // finally remove the PID from the file and the PID file itself, if empty
981
        $this->removePid($this->getSerial());
982
    }
983
984
    /**
985
     * This method finishes the import process and cleans the registry.
986
     *
987
     * @return void
988
     */
989
    protected function finish()
990
    {
991
        // remove the import status from the registry
992
        $this->getRegistryProcessor()->removeAttribute($this->getSerial());
993
    }
994
995
996
    /**
997
     * Remove's the passed line from the file with the passed name.
998
     *
999
     * @param string $line     The line to be removed
1000
     * @param string $filename The name of the file the line has to be removed
1001
     *
1002
     * @return void
1003
     * @throws \Exception Is thrown, if the line is not found or can not be removed
1004
     */
1005
    protected function removeLineFromFile($line, $filename)
1006
    {
1007
1008
        // open the PID file
1009
        $fh = fopen($filename, 'r+');
1010
1011
        // initialize the array for the PIDs found in the PID file
1012
        $lines = array();
1013
1014
        // initialize the flag if the line has been found
1015
        $found = false;
1016
1017
        // read the lines with the PIDs from the PID file
1018
        while (($buffer = fgets($fh, 4096)) !== false) {
1019
            // remove the new line
1020
            $buffer = trim($buffer, PHP_EOL);
1021
            // if the line is the one to be removed, ignore the line
1022
            if ($line === $buffer) {
1023
                $found = true;
1024
                continue;
1025
            }
1026
1027
            // add the found PID to the array
1028
            $lines[] = $buffer;
1029
        }
1030
1031
        // query whether or not, we found the line
1032
        if (!$found) {
1033
            throw new LineNotFoundException(sprintf('Line %s can not be found in file %s', $line, $filename));
1034
        }
1035
1036
        // if there are NO more lines, delete the file
1037
        if (sizeof($lines) === 0) {
1038
            fclose($fh);
1039
            unlink($filename);
1040
            return;
1041
        }
1042
1043
        // empty the file and rewind the file pointer
1044
        ftruncate($fh, 0);
1045
        rewind($fh);
1046
1047
        // append the existing lines to the file
1048
        foreach ($lines as $ln) {
1049
            if (fwrite($fh, $ln . PHP_EOL) === false) {
1050
                throw new \Exception(sprintf('Can\'t write %s to file %s', $ln, $filename));
1051
            }
1052
        }
1053
1054
        // finally close the file
1055
        fclose($fh);
1056
    }
1057
}
1058