Completed
Push — master ( f1c7b3...39a3ca )
by Tim
10s
created

Simple::lock()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 22
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 22
ccs 0
cts 9
cp 0
rs 9.2
cc 3
eloc 8
nc 3
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 Symfony\Component\Console\Input\InputInterface;
27
use Symfony\Component\Console\Output\OutputInterface;
28
use Symfony\Component\Console\Helper\FormatterHelper;
29
use TechDivision\Import\Utils\LoggerKeys;
30
use TechDivision\Import\Utils\RegistryKeys;
31
use TechDivision\Import\ApplicationInterface;
32
use TechDivision\Import\ConfigurationInterface;
33
use TechDivision\Import\Configuration\PluginConfigurationInterface;
34
use TechDivision\Import\Services\ImportProcessorInterface;
35
use TechDivision\Import\Services\RegistryProcessorInterface;
36
use TechDivision\Import\Cli\Exceptions\LineNotFoundException;
37
use TechDivision\Import\Cli\Exceptions\FileNotFoundException;
38
39
/**
40
 * The M2IF - Console Tool implementation.
41
 *
42
 * This is a example console tool implementation that should give developers an impression
43
 * on how the M2IF could be used to implement their own Magento 2 importer.
44
 *
45
 * @author    Tim Wagner <[email protected]>
46
 * @copyright 2016 TechDivision GmbH <[email protected]>
47
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
48
 * @link      https://github.com/techdivision/import-cli-simple
49
 * @link      http://www.techdivision.com
50
 */
51
class Simple implements ApplicationInterface
52
{
53
54
    /**
55
     * The default style to write messages to the symfony console.
56
     *
57
     * @var string
58
     */
59
    const DEFAULT_STYLE = 'info';
60
61
    /**
62
     * The TechDivision company name as ANSI art.
63
     *
64
     * @var string
65
     */
66
    protected $ansiArt = ' _______        _     _____  _       _     _
67
|__   __|      | |   |  __ \(_)     (_)   (_)
68
   | | ___  ___| |__ | |  | |___   ___ ___ _  ___  _ __
69
   | |/ _ \/ __| \'_ \| |  | | \ \ / / / __| |/ _ \| \'_ \
70
   | |  __/ (__| | | | |__| | |\ V /| \__ \ | (_) | | | |
71
   |_|\___|\___|_| |_|_____/|_| \_/ |_|___/_|\___/|_| |_|
72
';
73
74
    /**
75
     * The log level => console style mapping.
76
     *
77
     * @var array
78
     */
79
    protected $logLevelStyleMapping = array(
80
        LogLevel::INFO      => 'info',
81
        LogLevel::DEBUG     => 'comment',
82
        LogLevel::ERROR     => 'error',
83
        LogLevel::ALERT     => 'error',
84
        LogLevel::CRITICAL  => 'error',
85
        LogLevel::EMERGENCY => 'error',
86
        LogLevel::WARNING   => 'error',
87
        LogLevel::NOTICE    => 'info'
88
    );
89
90
    /**
91
     * The PID for the running processes.
92
     *
93
     * @var array
94
     */
95
    protected $pid;
96
97
    /**
98
     * The actions unique serial.
99
     *
100
     * @var string
101
     */
102
    protected $serial;
103
104
    /**
105
     * The array with the system logger instances.
106
     *
107
     * @var array
108
     */
109
    protected $systemLoggers;
110
111
    /**
112
     * The RegistryProcessor instance to handle running threads.
113
     *
114
     * @var \TechDivision\Import\Services\RegistryProcessorInterface
115
     */
116
    protected $registryProcessor;
117
118
    /**
119
     * The processor to read/write the necessary import data.
120
     *
121
     * @var \TechDivision\Import\Services\ImportProcessorInterface
122
     */
123
    protected $importProcessor;
124
125
    /**
126
     * The system configuration.
127
     *
128
     * @var \TechDivision\Import\ConfigurationInterface
129
     */
130
    protected $configuration;
131
132
    /**
133
     * The input stream to read console information from.
134
     *
135
     * @var \Symfony\Component\Console\Input\InputInterface
136
     */
137
    protected $input;
138
139
    /**
140
     * The output stream to write console information to.
141
     *
142
     * @var \Symfony\Component\Console\Output\OutputInterface
143
     */
144
    protected $output;
145
146
    /**
147
     * The plugins to be processed.
148
     *
149
     * @var array
150
     */
151
    protected $plugins = array();
152
153
    /**
154
     * The flag that stop's processing the operation.
155
     *
156
     * @var boolean
157
     */
158
    protected $stopped = false;
159
160
    /**
161
     * The constructor to initialize the instance.
162
     *
163
     * @param \TechDivision\Import\Services\RegistryProcessorInterface $registryProcessor The registry processor instance
164
     * @param \TechDivision\Import\Services\ImportProcessorInterface   $importProcessor   The import processor instance
165
     * @param \TechDivision\Import\ConfigurationInterface              $configuration     The system configuration
166
     * @param \Symfony\Component\Console\Input\InputInterface          $input             An InputInterface instance
167
     * @param \Symfony\Component\Console\Output\OutputInterface        $output            An OutputInterface instance
168
     * @param array                                                    $systemLoggers     The array with the system logger instances
169
     */
170 1
    public function __construct(
171
        RegistryProcessorInterface $registryProcessor,
172
        ImportProcessorInterface $importProcessor,
173
        ConfigurationInterface $configuration,
174
        InputInterface $input,
175
        OutputInterface $output,
176
        array $systemLoggers
177
    ) {
178
179
        // register the shutdown function
180 1
        register_shutdown_function(array($this, 'shutdown'));
181
182
        // initialize the values
183 1
        $this->registryProcessor = $registryProcessor;
184 1
        $this->importProcessor = $importProcessor;
185 1
        $this->configuration = $configuration;
186 1
        $this->input = $input;
187 1
        $this->output = $output;
188 1
        $this->systemLoggers = $systemLoggers;
189 1
    }
190
191
    /**
192
     * The shutdown handler to catch fatal errors.
193
     *
194
     * This method is need to make sure, that an existing PID file will be removed
195
     * if a fatal error has been triggered.
196
     *
197
     * @return void
198
     */
199
    public function shutdown()
200
    {
201
202
        // check if there was a fatal error caused shutdown
203
        if ($lastError = error_get_last()) {
204
            // initialize error type and message
205
            $type = 0;
206
            $message = '';
207
            // extract the last error values
208
            extract($lastError);
209
            // query whether we've a fatal/user error
210
            if ($type === E_ERROR || $type === E_USER_ERROR) {
211
                // clean-up the PID file
212
                $this->unlock();
213
                // log the fatal error message
214
                $this->log($message, LogLevel::ERROR);
215
            }
216
        }
217
    }
218
219
    /**
220
     * Return's the logger with the passed name, by default the system logger.
221
     *
222
     * @param string $name The name of the requested system logger
223
     *
224
     * @return \Psr\Log\LoggerInterface The logger instance
225
     * @throws \Exception Is thrown, if the requested logger is NOT available
226
     */
227
    public function getSystemLogger($name = LoggerKeys::SYSTEM)
228
    {
229
230
        // query whether or not, the requested logger is available
231
        if (isset($this->systemLoggers[$name])) {
232
            return $this->systemLoggers[$name];
233
        }
234
235
        // throw an exception if the requested logger is NOT available
236
        throw new \Exception(sprintf('The requested logger \'%s\' is not available', $name));
237
    }
238
239
    /**
240
     * Query whether or not the system logger with the passed name is available.
241
     *
242
     * @param string $name The name of the requested system logger
243
     *
244
     * @return boolean TRUE if the logger with the passed name exists, else FALSE
245
     */
246
    public function hasSystemLogger($name = LoggerKeys::SYSTEM)
247
    {
248
        return isset($this->systemLoggers[$name]);
249
    }
250
251
    /**
252
     * Return's the array with the system logger instances.
253
     *
254
     * @return array The logger instance
255
     */
256
    public function getSystemLoggers()
257
    {
258
        return $this->systemLoggers;
259
    }
260
261
    /**
262
     * Return's the RegistryProcessor instance to handle the running threads.
263
     *
264
     * @return \TechDivision\Import\Services\RegistryProcessor The registry processor instance
265
     */
266
    public function getRegistryProcessor()
267
    {
268
        return $this->registryProcessor;
269
    }
270
271
    /**
272
     * Return's the import processor instance.
273
     *
274
     * @return \TechDivision\Import\Services\ImportProcessorInterface The import processor instance
275
     */
276
    public function getImportProcessor()
277
    {
278
        return $this->importProcessor;
279
    }
280
281
    /**
282
     * Return's the system configuration.
283
     *
284
     * @return \TechDivision\Import\ConfigurationInterface The system configuration
285
     */
286
    public function getConfiguration()
287
    {
288
        return $this->configuration;
289
    }
290
291
    /**
292
     * Return's the input stream to read console information from.
293
     *
294
     * @return \Symfony\Component\Console\Input\InputInterface An IutputInterface instance
295
     */
296
    public function getInput()
297
    {
298
        return $this->input;
299
    }
300
301
    /**
302
     * Return's the output stream to write console information to.
303
     *
304
     * @return \Symfony\Component\Console\Output\OutputInterface An OutputInterface instance
305
     */
306 1
    public function getOutput()
307
    {
308 1
        return $this->output;
309
    }
310
311
    /**
312
     * Return's the unique serial for this import process.
313
     *
314
     * @return string The unique serial
315
     */
316
    public function getSerial()
317
    {
318
        return $this->serial;
319
    }
320
321
    /**
322
     * Persist the UUID of the actual import process to the PID file.
323
     *
324
     * @return void
325
     * @throws \Exception Is thrown, if the PID can not be added
326
     */
327
    public function lock()
328
    {
329
330
        // query whether or not, the PID has already been set
331
        if ($this->pid === $this->getSerial()) {
332
            return;
333
        }
334
335
        // if not, initialize the PID
336
        $this->pid = $this->getSerial();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->getSerial() of type string is incompatible with the declared type array of property $pid.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
337
338
        // open the PID file
339
        $fh = fopen($pidFilename = $this->getPidFilename(), 'a');
340
341
        // append the PID to the PID file
342
        if (fwrite($fh, $this->pid . PHP_EOL) === false) {
343
            throw new \Exception(sprintf('Can\'t write PID %s to PID file %s', $this->pid, $pidFilename));
344
        }
345
346
        // close the file handle
347
        fclose($fh);
348
    }
349
350
    /**
351
     * Remove's the UUID of the actual import process from the PID file.
352
     *
353
     * @return void
354
     * @throws \Exception Is thrown, if the PID can not be removed
355
     */
356
    public function unlock()
357
    {
358
        try {
359
            // remove the PID from the PID file if set
360
            if ($this->pid === $this->getSerial()) {
361
                $this->removeLineFromFile($this->pid, $this->getPidFilename());
362
            }
363
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
364
        } catch (FileNotFoundException $fnfe) {
365
            $this->getSystemLogger()->notice(sprintf('PID file %s doesn\'t exist', $this->getPidFilename()));
366
        } catch (LineNotFoundException $lnfe) {
367
            $this->getSystemLogger()->notice(sprintf('PID %s is can not be found in PID file %s', $this->pid, $this->getPidFilename()));
368
        } catch (\Exception $e) {
369
            throw new \Exception(sprintf('Can\'t remove PID %s from PID file %s', $this->pid, $this->getPidFilename()), null, $e);
370
        }
371
    }
372
373
    /**
374
     * Remove's the passed line from the file with the passed name.
375
     *
376
     * @param string $line     The line to be removed
377
     * @param string $filename The name of the file the line has to be removed
378
     *
379
     * @return void
380
     * @throws \Exception Is thrown, if the file doesn't exists, the line is not found or can not be removed
381
     */
382
    public function removeLineFromFile($line, $filename)
383
    {
384
385
        // query whether or not the filename
386
        if (!file_exists($filename)) {
387
            throw new FileNotFoundException(sprintf('File %s doesn\' exists', $filename));
388
        }
389
390
        // open the PID file
391
        $fh = fopen($filename, 'r+');
392
393
        // initialize the array for the PIDs found in the PID file
394
        $lines = array();
395
396
        // initialize the flag if the line has been found
397
        $found = false;
398
399
        // read the lines with the PIDs from the PID file
400
        while (($buffer = fgets($fh, 4096)) !== false) {
401
            // remove the new line
402
            $buffer = trim($buffer, PHP_EOL);
403
            // if the line is the one to be removed, ignore the line
404
            if ($line === $buffer) {
405
                $found = true;
406
                continue;
407
            }
408
409
            // add the found PID to the array
410
            $lines[] = $buffer;
411
        }
412
413
        // query whether or not, we found the line
414
        if (!$found) {
415
            throw new LineNotFoundException(sprintf('Line %s can not be found in file %s', $line, $filename));
416
        }
417
418
        // if there are NO more lines, delete the file
419
        if (sizeof($lines) === 0) {
420
            fclose($fh);
421
            unlink($filename);
422
            return;
423
        }
424
425
        // empty the file and rewind the file pointer
426
        ftruncate($fh, 0);
427
        rewind($fh);
428
429
        // append the existing lines to the file
430
        foreach ($lines as $ln) {
431
            if (fwrite($fh, $ln . PHP_EOL) === false) {
432
                throw new \Exception(sprintf('Can\'t write %s to file %s', $ln, $filename));
433
            }
434
        }
435
436
        // finally close the file
437
        fclose($fh);
438
    }
439
440
    /**
441
     * Process the given operation.
442
     *
443
     * @return void
444
     * @throws \Exception Is thrown if the operation can't be finished successfully
445
     */
446
    public function process()
447
    {
448
449
        try {
450
            // track the start time
451
            $startTime = microtime(true);
452
453
            // start the transaction
454
            $this->getImportProcessor()->getConnection()->beginTransaction();
455
456
            // prepare the global data for the import process
457
            $this->setUp();
458
459
            // process the plugins defined in the configuration
460
            foreach ($this->getConfiguration()->getPlugins() as $pluginConfiguration) {
461
                // query whether or not the operation has been stopped
462
                if ($this->isStopped()) {
463
                    break;
464
                }
465
                // process the plugin if not
466
                $this->pluginFactory($pluginConfiguration)->process();
467
            }
468
469
            // tear down the  instance
470
            $this->tearDown();
471
472
            // commit the transaction
473
            $this->getImportProcessor()->getConnection()->commit();
474
475
            // track the time needed for the import in seconds
476
            $endTime = microtime(true) - $startTime;
477
478
            // log a message that import has been finished
479
            $this->log(
480
                sprintf(
481
                    'Successfully finished import with serial %s in %f s',
482
                    $this->getSerial(),
483
                    $endTime
484
                ),
485
                LogLevel::INFO
486
            );
487
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
488
        } catch (\Exception $e) {
489
            // tear down
490
            $this->tearDown();
491
492
            // rollback the transaction
493
            $this->getImportProcessor()->getConnection()->rollBack();
494
495
            // finally, if a PID has been set (because CSV files has been found),
496
            // remove it from the PID file to unlock the importer
497
            $this->unlock();
498
499
            // track the time needed for the import in seconds
500
            $endTime = microtime(true) - $startTime;
501
502
            // log a message that the file import failed
503
            foreach ($this->systemLoggers as $systemLogger) {
504
                $systemLogger->error($e->__toString());
505
            }
506
507
            // log a message that import has been finished
508
            $this->getSystemLogger()->info(
509
                sprintf(
510
                    'Can\'t finish import with serial %s in %f s',
511
                    $this->getSerial(),
512
                    $endTime
513
                )
514
            );
515
516
            // re-throw the exception
517
            throw $e;
518
        }
519
    }
520
521
    /**
522
     * Stop processing the operation.
523
     *
524
     * @param string $reason The reason why the operation has been stopped
525
     *
526
     * @return void
527
     */
528
    public function stop($reason)
529
    {
530
531
        // log a message that the operation has been stopped
532
        $this->log($reason, LogLevel::INFO);
533
534
        // stop processing the plugins by setting the flag to TRUE
535
        $this->stopped = true;
536
    }
537
538
    /**
539
     * Return's TRUE if the operation has been stopped, else FALSE.
540
     *
541
     * @return boolean TRUE if the process has been stopped, else FALSE
542
     */
543
    public function isStopped()
544
    {
545
        return $this->stopped;
546
    }
547
548
    /**
549
     * Factory method to create new plugin instances.
550
     *
551
     * @param \TechDivision\Import\Configuration\PluginConfigurationInterface $pluginConfiguration The plugin configuration instance
552
     *
553
     * @return object The plugin instance
554
     */
555
    protected function pluginFactory(PluginConfigurationInterface $pluginConfiguration)
556
    {
557
558
        // load the plugin class name
559
        $className = $pluginConfiguration->getClassName();
560
561
        // initialize and return the plugin instance
562
        return new $className($this, $pluginConfiguration);
563
    }
564
565
    /**
566
     * Lifecycle callback that will be inovked before the
567
     * import process has been started.
568
     *
569
     * @return void
570
     */
571
    protected function setUp()
572
    {
573
574
        // generate the serial for the new job
575
        $this->serial = Uuid::uuid4()->__toString();
576
577
        // query whether or not an import is running AND an existing PID has to be ignored
578
        if (file_exists($pidFilename = $this->getPidFilename()) && !$this->getConfiguration()->isIgnorePid()) {
579
            throw new \Exception(sprintf('At least one import process is already running (check PID: %s)', $pidFilename));
580
        } elseif (file_exists($pidFilename = $this->getPidFilename()) && $this->getConfiguration()->isIgnorePid()) {
581
            $this->log(sprintf('At least one import process is already running (PID: %s)', $pidFilename), LogLevel::WARNING);
582
        }
583
584
        // write the TechDivision ANSI art icon to the console
585
        $this->log($this->ansiArt);
586
587
        // log the debug information, if debug mode is enabled
588
        if ($this->getConfiguration()->isDebugMode()) {
589
            // log the system's PHP configuration
590
            $this->log(sprintf('PHP version: %s', phpversion()), LogLevel::DEBUG);
591
            $this->log('-------------------- Loaded Extensions -----------------------', LogLevel::DEBUG);
592
            $this->log(implode(', ', $loadedExtensions = get_loaded_extensions()), LogLevel::DEBUG);
593
            $this->log('--------------------------------------------------------------', LogLevel::DEBUG);
594
595
            // write a warning for low performance, if XDebug extension is activated
596
            if (in_array('xdebug', $loadedExtensions)) {
597
                $this->log('Low performance exptected, as result of enabled XDebug extension!', LogLevel::WARNING);
598
            }
599
        }
600
601
        // log a message that import has been started
602
        $this->log(
603
            sprintf(
604
                'Now start import with serial %s (operation: %s)',
605
                $this->getSerial(),
606
                $this->getConfiguration()->getOperationName()
607
            ),
608
            LogLevel::INFO
609
        );
610
611
        // initialize the status
612
        $status = array(
613
            RegistryKeys::STATUS => 1,
614
            RegistryKeys::BUNCHES => 0,
615
            RegistryKeys::SOURCE_DIRECTORY => $this->getConfiguration()->getSourceDir(),
616
            RegistryKeys::MISSING_OPTION_VALUES => array()
617
        );
618
619
        // append it to the registry
620
        $this->getRegistryProcessor()->setAttribute($this->getSerial(), $status);
621
    }
622
623
    /**
624
     * Lifecycle callback that will be inovked after the
625
     * import process has been finished.
626
     *
627
     * @return void
628
     */
629
    protected function tearDown()
630
    {
631
        $this->getRegistryProcessor()->removeAttribute($this->getSerial());
632
    }
633
634
    /**
635
     * Simple method that writes the passed method the the console and the
636
     * system logger, if configured and a log level has been passed.
637
     *
638
     * @param string $msg      The message to log
639
     * @param string $logLevel The log level to use
640
     *
641
     * @return void
642
     */
643
    protected function log($msg, $logLevel = null)
644
    {
645
646
        // initialize the formatter helper
647
        $helper = new FormatterHelper();
648
649
        // map the log level to the console style
650
        $style = $this->mapLogLevelToStyle($logLevel);
651
652
        // format the message, according to the passed log level and write it to the console
653
        $this->getOutput()->writeln($logLevel ? $helper->formatBlock($msg, $style) : $msg);
654
655
        // log the message if a log level has been passed
656
        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...
657
            $systemLogger->log($logLevel, $msg);
658
        }
659
    }
660
661
    /**
662
     * Map's the passed log level to a valid symfony console style.
663
     *
664
     * @param string $logLevel The log level to map
665
     *
666
     * @return string The apropriate symfony console style
667
     */
668
    protected function mapLogLevelToStyle($logLevel)
669
    {
670
671
        // query whether or not the log level is mapped
672
        if (isset($this->logLevelStyleMapping[$logLevel])) {
673
            return $this->logLevelStyleMapping[$logLevel];
674
        }
675
676
        // return the default style => info
677
        return Simple::DEFAULT_STYLE;
678
    }
679
680
    /**
681
     * Return's the PID filename to use.
682
     *
683
     * @return string The PID filename
684
     */
685
    protected function getPidFilename()
686
    {
687
        return $this->getConfiguration()->getPidFilename();
688
    }
689
}
690