Completed
Pull Request — master (#58)
by Tim
02:39
created

Simple   B

Complexity

Total Complexity 47

Size/Duplication

Total Lines 565
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 6.83%

Importance

Changes 33
Bugs 2 Features 3
Metric Value
wmc 47
c 33
b 2
f 3
lcom 1
cbo 10
dl 0
loc 565
ccs 11
cts 161
cp 0.0683
rs 8.439

19 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 20 1
A shutdown() 0 19 4
A getSystemLogger() 0 4 1
A getRegistryProcessor() 0 4 1
A getImportProcessor() 0 4 1
A getConfiguration() 0 4 1
A getInput() 0 4 1
A getOutput() 0 4 1
A getSerial() 0 4 1
A lock() 0 22 3
B unlock() 0 16 5
B removeLineFromFile() 0 57 8
A process() 0 67 3
A pluginFactory() 0 9 1
C setUp() 0 50 7
A tearDown() 0 4 1
A log() 0 17 4
A mapLogLevelToStyle() 0 11 2
A getPidFilename() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Simple often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Simple, and based on these observations, apply Extract Interface, too.

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\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 system logger implementation.
106
     *
107
     * @var \Psr\Log\LoggerInterface
108
     */
109
    protected $systemLogger;
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 constructor to initialize the instance.
155
     *
156
     * @param \Psr\Log\LoggerInterface                                 $systemLogger      The system logger
157
     * @param \TechDivision\Import\Services\RegistryProcessorInterface $registryProcessor The registry processor instance
158
     * @param \TechDivision\Import\Services\ImportProcessorInterface   $importProcessor   The import processor instance
159
     * @param \TechDivision\Import\ConfigurationInterface              $configuration     The system configuration
160
     * @param \Symfony\Component\Console\Input\InputInterface          $input             An InputInterface instance
161
     * @param \Symfony\Component\Console\Output\OutputInterface        $output            An OutputInterface instance
162
     */
163 1
    public function __construct(
164
        LoggerInterface $systemLogger,
165
        RegistryProcessorInterface $registryProcessor,
166
        ImportProcessorInterface $importProcessor,
167
        ConfigurationInterface $configuration,
168
        InputInterface $input,
169
        OutputInterface $output
170
    ) {
171
172
        // register the shutdown function
173 1
        register_shutdown_function(array($this, 'shutdown'));
174
175
        // initialize the values
176 1
        $this->systemLogger = $systemLogger;
177 1
        $this->registryProcessor = $registryProcessor;
178 1
        $this->importProcessor = $importProcessor;
179 1
        $this->configuration = $configuration;
180 1
        $this->input = $input;
181 1
        $this->output = $output;
182 1
    }
183
184
    /**
185
     * The shutdown handler to catch fatal errors.
186
     *
187
     * This method is need to make sure, that an existing PID file will be removed
188
     * if a fatal error has been triggered.
189
     *
190
     * @return void
191
     */
192
    public function shutdown()
193
    {
194
195
        // check if there was a fatal error caused shutdown
196
        if ($lastError = error_get_last()) {
197
            // initialize error type and message
198
            $type = 0;
199
            $message = '';
200
            // extract the last error values
201
            extract($lastError);
202
            // query whether we've a fatal/user error
203
            if ($type === E_ERROR || $type === E_USER_ERROR) {
204
                // clean-up the PID file
205
                $this->unlock();
206
                // log the fatal error message
207
                $this->log($message, LogLevel::ERROR);
208
            }
209
        }
210
    }
211
212
    /**
213
     * Return's the system logger.
214
     *
215
     * @return \Psr\Log\LoggerInterface The system logger instance
216
     */
217
    public function getSystemLogger()
218
    {
219
        return $this->systemLogger;
220
    }
221
222
    /**
223
     * Return's the RegistryProcessor instance to handle the running threads.
224
     *
225
     * @return \TechDivision\Import\Services\RegistryProcessor The registry processor instance
226
     */
227
    public function getRegistryProcessor()
228
    {
229
        return $this->registryProcessor;
230
    }
231
232
    /**
233
     * Return's the import processor instance.
234
     *
235
     * @return \TechDivision\Import\Services\ImportProcessorInterface The import processor instance
236
     */
237
    public function getImportProcessor()
238
    {
239
        return $this->importProcessor;
240
    }
241
242
    /**
243
     * Return's the system configuration.
244
     *
245
     * @return \TechDivision\Import\ConfigurationInterface The system configuration
246
     */
247
    public function getConfiguration()
248
    {
249
        return $this->configuration;
250
    }
251
252
    /**
253
     * Return's the input stream to read console information from.
254
     *
255
     * @return \Symfony\Component\Console\Input\InputInterface An IutputInterface instance
256
     */
257
    public function getInput()
258
    {
259
        return $this->input;
260
    }
261
262
    /**
263
     * Return's the output stream to write console information to.
264
     *
265
     * @return \Symfony\Component\Console\Output\OutputInterface An OutputInterface instance
266
     */
267 1
    public function getOutput()
268
    {
269 1
        return $this->output;
270
    }
271
272
    /**
273
     * Return's the unique serial for this import process.
274
     *
275
     * @return string The unique serial
276
     */
277
    public function getSerial()
278
    {
279
        return $this->serial;
280
    }
281
282
    /**
283
     * Persist the UUID of the actual import process to the PID file.
284
     *
285
     * @return void
286
     * @throws \Exception Is thrown, if the PID can not be added
287
     */
288
    public function lock()
289
    {
290
291
        // query whether or not, the PID has already been set
292
        if ($this->pid === $this->getSerial()) {
293
            return;
294
        }
295
296
        // if not, initialize the PID
297
        $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...
298
299
        // open the PID file
300
        $fh = fopen($pidFilename = $this->getPidFilename(), 'a');
301
302
        // append the PID to the PID file
303
        if (fwrite($fh, $this->pid . PHP_EOL) === false) {
304
            throw new \Exception(sprintf('Can\'t write PID %s to PID file %s', $this->pid, $pidFilename));
305
        }
306
307
        // close the file handle
308
        fclose($fh);
309
    }
310
311
    /**
312
     * Remove's the UUID of the actual import process from the PID file.
313
     *
314
     * @return void
315
     * @throws \Exception Is thrown, if the PID can not be removed
316
     */
317
    public function unlock()
318
    {
319
        try {
320
            // remove the PID from the PID file if set
321
            if ($this->pid === $this->getSerial()) {
322
                $this->removeLineFromFile($this->pid, $this->getPidFilename());
323
            }
324
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
325
        } catch (FileNotFoundException $fnfe) {
326
            $this->getSystemLogger()->notice(sprintf('PID file %s doesn\'t exist', $this->getPidFilename()));
327
        } catch (LineNotFoundException $lnfe) {
328
            $this->getSystemLogger()->notice(sprintf('PID %s is can not be found in PID file %s', $this->pid, $this->getPidFilename()));
329
        } catch (\Exception $e) {
330
            throw new \Exception(sprintf('Can\'t remove PID %s from PID file %s', $this->pid, $this->getPidFilename()), null, $e);
331
        }
332
    }
333
334
    /**
335
     * Remove's the passed line from the file with the passed name.
336
     *
337
     * @param string $line     The line to be removed
338
     * @param string $filename The name of the file the line has to be removed
339
     *
340
     * @return void
341
     * @throws \Exception Is thrown, if the file doesn't exists, the line is not found or can not be removed
342
     */
343
    public function removeLineFromFile($line, $filename)
344
    {
345
346
        // query whether or not the filename
347
        if (!file_exists($filename)) {
348
            throw new FileNotFoundException(sprintf('File %s doesn\' exists', $filename));
349
        }
350
351
        // open the PID file
352
        $fh = fopen($filename, 'r+');
353
354
        // initialize the array for the PIDs found in the PID file
355
        $lines = array();
356
357
        // initialize the flag if the line has been found
358
        $found = false;
359
360
        // read the lines with the PIDs from the PID file
361
        while (($buffer = fgets($fh, 4096)) !== false) {
362
            // remove the new line
363
            $buffer = trim($buffer, PHP_EOL);
364
            // if the line is the one to be removed, ignore the line
365
            if ($line === $buffer) {
366
                $found = true;
367
                continue;
368
            }
369
370
            // add the found PID to the array
371
            $lines[] = $buffer;
372
        }
373
374
        // query whether or not, we found the line
375
        if (!$found) {
376
            throw new LineNotFoundException(sprintf('Line %s can not be found in file %s', $line, $filename));
377
        }
378
379
        // if there are NO more lines, delete the file
380
        if (sizeof($lines) === 0) {
381
            fclose($fh);
382
            unlink($filename);
383
            return;
384
        }
385
386
        // empty the file and rewind the file pointer
387
        ftruncate($fh, 0);
388
        rewind($fh);
389
390
        // append the existing lines to the file
391
        foreach ($lines as $ln) {
392
            if (fwrite($fh, $ln . PHP_EOL) === false) {
393
                throw new \Exception(sprintf('Can\'t write %s to file %s', $ln, $filename));
394
            }
395
        }
396
397
        // finally close the file
398
        fclose($fh);
399
    }
400
401
    /**
402
     * Process the given operation.
403
     *
404
     * @return void
405
     * @throws \Exception Is thrown if the operation can't be finished successfully
406
     */
407
    public function process()
408
    {
409
410
        try {
411
            // track the start time
412
            $startTime = microtime(true);
413
414
            // start the transaction
415
            $this->getImportProcessor()->getConnection()->beginTransaction();
416
417
            // prepare the global data for the import process
418
            $this->setUp();
419
420
            // process the plugins defined in the configuration
421
            foreach ($this->getConfiguration()->getPlugins() as $pluginConfiguration) {
422
                $this->pluginFactory($pluginConfiguration)->process();
423
            }
424
425
            // tear down the  instance
426
            $this->tearDown();
427
428
            // commit the transaction
429
            $this->getImportProcessor()->getConnection()->commit();
430
431
            // track the time needed for the import in seconds
432
            $endTime = microtime(true) - $startTime;
433
434
            // log a message that import has been finished
435
            $this->log(
436
                sprintf(
437
                    'Successfully finished import with serial %s in %f s',
438
                    $this->getSerial(),
439
                    $endTime
440
                ),
441
                LogLevel::INFO
442
            );
443
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
444
        } catch (\Exception $e) {
445
            // tear down
446
            $this->tearDown();
447
448
            // rollback the transaction
449
            $this->getImportProcessor()->getConnection()->rollBack();
450
451
            // finally, if a PID has been set (because CSV files has been found),
452
            // remove it from the PID file to unlock the importer
453
            $this->unlock();
454
455
            // track the time needed for the import in seconds
456
            $endTime = microtime(true) - $startTime;
457
458
            // log a message that the file import failed
459
            $this->getSystemLogger()->error($e->__toString());
460
461
            // log a message that import has been finished
462
            $this->getSystemLogger()->info(
463
                sprintf(
464
                    'Can\'t finish import with serial %s in %f s',
465
                    $this->getSerial(),
466
                    $endTime
467
                )
468
            );
469
470
            // re-throw the exception
471
            throw $e;
472
        }
473
    }
474
475
    /**
476
     * Factory method to create new plugin instances.
477
     *
478
     * @param \TechDivision\Import\Configuration\PluginConfigurationInterface $pluginConfiguration The plugin configuration instance
479
     *
480
     * @return object The plugin instance
481
     */
482
    protected function pluginFactory(PluginConfigurationInterface $pluginConfiguration)
483
    {
484
485
        // load the plugin class name
486
        $className = $pluginConfiguration->getClassName();
487
488
        // initialize and return the plugin instance
489
        return new $className($this, $pluginConfiguration);
490
    }
491
492
    /**
493
     * Lifecycle callback that will be inovked before the
494
     * import process has been started.
495
     *
496
     * @return void
497
     */
498
    protected function setUp()
499
    {
500
501
        // generate the serial for the new job
502
        $this->serial = Uuid::uuid4()->__toString();
503
504
        // query whether or not an import is running AND an existing PID has to be ignored
505
        if (file_exists($pidFilename = $this->getPidFilename()) && !$this->getConfiguration()->isIgnorePid()) {
506
            throw new \Exception(sprintf('At least one import process is already running (check PID: %s)', $pidFilename));
507
        } elseif (file_exists($pidFilename = $this->getPidFilename()) && $this->getConfiguration()->isIgnorePid()) {
508
            $this->log(sprintf('At least one import process is already running (PID: %s)', $pidFilename), LogLevel::WARNING);
509
        }
510
511
        // write the TechDivision ANSI art icon to the console
512
        $this->log($this->ansiArt);
513
514
        // log the debug information, if debug mode is enabled
515
        if ($this->getConfiguration()->isDebugMode()) {
516
            // log the system's PHP configuration
517
            $this->log(sprintf('PHP version: %s', phpversion()), LogLevel::DEBUG);
518
            $this->log('-------------------- Loaded Extensions -----------------------', LogLevel::DEBUG);
519
            $this->log(implode(', ', $loadedExtensions = get_loaded_extensions()), LogLevel::DEBUG);
520
            $this->log('--------------------------------------------------------------', LogLevel::DEBUG);
521
522
            // write a warning for low performance, if XDebug extension is activated
523
            if (in_array('xdebug', $loadedExtensions)) {
524
                $this->log('Low performance exptected, as result of enabled XDebug extension!', LogLevel::WARNING);
525
            }
526
        }
527
528
        // log a message that import has been started
529
        $this->log(
530
            sprintf(
531
                'Now start import with serial %s (operation: %s)',
532
                $this->getSerial(),
533
                $this->getConfiguration()->getOperationName()
534
            ),
535
            LogLevel::INFO
536
        );
537
538
        // initialize the status
539
        $status = array(
540
            RegistryKeys::STATUS => 1,
541
            RegistryKeys::BUNCHES => 0,
542
            RegistryKeys::SOURCE_DIRECTORY => $this->getConfiguration()->getSourceDir()
543
        );
544
545
        // append it to the registry
546
        $this->getRegistryProcessor()->setAttribute($this->getSerial(), $status);
547
    }
548
549
    /**
550
     * Lifecycle callback that will be inovked after the
551
     * import process has been finished.
552
     *
553
     * @return void
554
     */
555
    protected function tearDown()
556
    {
557
        $this->getRegistryProcessor()->removeAttribute($this->getSerial());
558
    }
559
560
    /**
561
     * Simple method that writes the passed method the the console and the
562
     * system logger, if configured and a log level has been passed.
563
     *
564
     * @param string $msg      The message to log
565
     * @param string $logLevel The log level to use
566
     *
567
     * @return void
568
     */
569
    protected function log($msg, $logLevel = null)
570
    {
571
572
        // initialize the formatter helper
573
        $helper = new FormatterHelper();
574
575
        // map the log level to the console style
576
        $style = $this->mapLogLevelToStyle($logLevel);
577
578
        // format the message, according to the passed log level and write it to the console
579
        $this->getOutput()->writeln($logLevel ? $helper->formatBlock($msg, $style) : $msg);
580
581
        // log the message if a log level has been passed
582
        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...
583
            $systemLogger->log($logLevel, $msg);
584
        }
585
    }
586
587
    /**
588
     * Map's the passed log level to a valid symfony console style.
589
     *
590
     * @param string $logLevel The log level to map
591
     *
592
     * @return string The apropriate symfony console style
593
     */
594
    protected function mapLogLevelToStyle($logLevel)
595
    {
596
597
        // query whether or not the log level is mapped
598
        if (isset($this->logLevelStyleMapping[$logLevel])) {
599
            return $this->logLevelStyleMapping[$logLevel];
600
        }
601
602
        // return the default style => info
603
        return Simple::DEFAULT_STYLE;
604
    }
605
606
    /**
607
     * Return's the PID filename to use.
608
     *
609
     * @return string The PID filename
610
     */
611
    protected function getPidFilename()
612
    {
613
        return $this->getConfiguration()->getPidFilename();
614
    }
615
}
616