Completed
Push — develop ( 21139f...eaa140 )
by Tom
12s
created

AbstractMagentoCommand   F

Complexity

Total Complexity 66

Size/Duplication

Total Lines 567
Duplicated Lines 0 %

Coupling/Cohesion

Components 5
Dependencies 12

Importance

Changes 6
Bugs 2 Features 0
Metric Value
wmc 66
c 6
b 2
f 0
lcom 5
cbo 12
dl 0
loc 567
rs 3.1913

29 Methods

Rating   Name   Duplication   Size   Complexity  
A initMagento() 0 10 2
A detectMagento() 0 21 4
A getComposer() 0 11 1
A checkDeprecatedAliases() 0 9 2
A formatActive() 0 4 1
C chooseInstallationFolder() 0 75 10
A getOrAskForArgument() 0 13 2
A initialize() 0 4 1
A _initWebsites() 0 9 2
A _getWebsiteCodeById() 0 12 3
A _getWebsiteIdByCode() 0 9 2
A getCommandConfig() 0 13 3
A writeSection() 0 8 1
A requireEnterprise() 0 7 2
A getCoreHelper() 0 7 2
A getComposerDownloadManager() 0 4 1
A createComposerPackageByConfig() 0 5 1
B downloadByComposerConfig() 0 22 4
B checkRepository() 0 26 5
A normalizePath() 0 7 2
A addDeprecatedAlias() 0 6 1
A _getModel() 0 8 2
A _getResourceModel() 0 8 2
A _getResourceSingleton() 0 8 2
A _parseBoolOption() 0 4 1
A run() 0 6 1
A isSourceTypeRepository() 0 4 1
A askForArrayEntry() 0 18 3
A getArgumentMessage() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like AbstractMagentoCommand 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 AbstractMagentoCommand, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace N98\Magento\Command;
4
5
use Composer\Factory as ComposerFactory;
6
use Composer\IO\ConsoleIO;
7
use Composer\Package\Loader\ArrayLoader as PackageLoader;
8
use Composer\Package\PackageInterface;
9
use InvalidArgumentException;
10
use N98\Util\Console\Helper\MagentoHelper;
11
use N98\Util\OperatingSystem;
12
use N98\Util\StringTyped;
13
use RuntimeException;
14
use Symfony\Component\Console\Command\Command;
15
use Symfony\Component\Console\Input\InputInterface;
16
use Symfony\Component\Console\Output\OutputInterface;
17
18
/**
19
 * Class AbstractMagentoCommand
20
 *
21
 * @package N98\Magento\Command
22
 *
23
 * @method \N98\Magento\Application getApplication() getApplication()
24
 */
25
abstract class AbstractMagentoCommand extends Command
26
{
27
    /**
28
     * @var int
29
     */
30
    const MAGENTO_MAJOR_VERSION_1 = 1;
31
32
    /**
33
     * @var int
34
     */
35
    const MAGENTO_MAJOR_VERSION_2 = 2;
36
37
    /**
38
     * @var string
39
     */
40
    protected $_magentoRootFolder = null;
41
42
    /**
43
     * @var int
44
     */
45
    protected $_magentoMajorVersion = self::MAGENTO_MAJOR_VERSION_1;
46
47
    /**
48
     * @var bool
49
     */
50
    protected $_magentoEnterprise = false;
51
52
    /**
53
     * @var array
54
     */
55
    protected $_deprecatedAlias = array();
56
57
    /**
58
     * @var array
59
     */
60
    protected $_websiteCodeMap = array();
61
62
    /**
63
     * Initializes the command just after the input has been validated.
64
     *
65
     * This is mainly useful when a lot of commands extends one main command
66
     * where some things need to be initialized based on the input arguments and options.
67
     *
68
     * @param InputInterface  $input  An InputInterface instance
69
     * @param OutputInterface $output An OutputInterface instance
70
     */
71
    protected function initialize(InputInterface $input, OutputInterface $output)
72
    {
73
        $this->checkDeprecatedAliases($input, $output);
74
    }
75
76
    private function _initWebsites()
77
    {
78
        $this->_websiteCodeMap = array();
79
        /** @var \Mage_Core_Model_Website[] $websites */
80
        $websites = \Mage::app()->getWebsites(false);
81
        foreach ($websites as $website) {
82
            $this->_websiteCodeMap[$website->getId()] = $website->getCode();
83
        }
84
    }
85
86
    /**
87
     * @param int $websiteId
88
     * @return string
89
     */
90
    protected function _getWebsiteCodeById($websiteId)
91
    {
92
        if (empty($this->_websiteCodeMap)) {
93
            $this->_initWebsites();
94
        }
95
96
        if (isset($this->_websiteCodeMap[$websiteId])) {
97
            return $this->_websiteCodeMap[$websiteId];
98
        }
99
100
        return '';
101
    }
102
103
    /**
104
     * @param string $websiteCode
105
     * @return int
106
     */
107
    protected function _getWebsiteIdByCode($websiteCode)
108
    {
109
        if (empty($this->_websiteCodeMap)) {
110
            $this->_initWebsites();
111
        }
112
        $websiteMap = array_flip($this->_websiteCodeMap);
113
114
        return $websiteMap[$websiteCode];
115
    }
116
117
    /**
118
     * @param string|null $commandClass
119
     * @return array
120
     */
121
    protected function getCommandConfig($commandClass = null)
122
    {
123
        if (null === $commandClass) {
124
            $commandClass = get_class($this);
125
        }
126
127
        $configArray = $this->getApplication()->getConfig();
128
        if (isset($configArray['commands'][$commandClass])) {
129
            return $configArray['commands'][$commandClass];
130
        }
131
132
        return;
133
    }
134
135
    /**
136
     * @param OutputInterface $output
137
     * @param string $text
138
     * @param string $style
139
     */
140
    protected function writeSection(OutputInterface $output, $text, $style = 'bg=blue;fg=white')
141
    {
142
        $output->writeln(array(
143
            '',
144
            $this->getHelper('formatter')->formatBlock($text, $style, true),
145
            '',
146
        ));
147
    }
148
149
    /**
150
     * Bootstrap magento shop
151
     *
152
     * @param bool $soft
153
     * @return bool
154
     */
155
    protected function initMagento($soft = false)
156
    {
157
        $application = $this->getApplication();
158
        $init = $application->initMagento($soft);
159
        if ($init) {
160
            $this->_magentoRootFolder = $application->getMagentoRootFolder();
161
        }
162
163
        return $init;
164
    }
165
166
    /**
167
     * Search for magento root folder
168
     *
169
     * @param OutputInterface $output
170
     * @param bool $silent print debug messages
171
     * @throws RuntimeException
172
     */
173
    public function detectMagento(OutputInterface $output, $silent = true)
174
    {
175
        $this->getApplication()->detectMagento();
176
177
        $this->_magentoEnterprise = $this->getApplication()->isMagentoEnterprise();
178
        $this->_magentoRootFolder = $this->getApplication()->getMagentoRootFolder();
179
        $this->_magentoMajorVersion = $this->getApplication()->getMagentoMajorVersion();
180
181
        if (!$silent) {
182
            $editionString = ($this->_magentoEnterprise ? ' (Enterprise Edition) ' : '');
183
            $output->writeln(
184
                '<info>Found Magento ' . $editionString . 'in folder "' . $this->_magentoRootFolder . '"</info>'
185
            );
186
        }
187
188
        if (!empty($this->_magentoRootFolder)) {
189
            return;
190
        }
191
192
        throw new RuntimeException('Magento folder could not be detected');
193
    }
194
195
    /**
196
     * Die if not Enterprise
197
     */
198
    protected function requireEnterprise(OutputInterface $output)
199
    {
200
        if (!$this->_magentoEnterprise) {
201
            $output->writeln('<error>Enterprise Edition is required but was not detected</error>');
202
            exit;
0 ignored issues
show
Coding Style Compatibility introduced by
The method requireEnterprise() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
203
        }
204
    }
205
206
    /**
207
     * @return \Mage_Core_Helper_Data
208
     */
209
    protected function getCoreHelper()
210
    {
211
        if ($this->_magentoMajorVersion == self::MAGENTO_MAJOR_VERSION_2) {
212
            return \Mage::helper('Mage_Core_Helper_Data');
213
        }
214
        return \Mage::helper('core');
215
    }
216
217
    /**
218
     * @param InputInterface $input
219
     * @param OutputInterface $output
220
     * @return \Composer\Downloader\DownloadManager
221
     */
222
    protected function getComposerDownloadManager($input, $output)
223
    {
224
        return $this->getComposer($input, $output)->getDownloadManager();
225
    }
226
227
    /**
228
     * @param array|PackageInterface $config
229
     * @return \Composer\Package\CompletePackage
230
     */
231
    protected function createComposerPackageByConfig($config)
232
    {
233
        $packageLoader = new PackageLoader();
234
        return $packageLoader->load($config);
0 ignored issues
show
Bug introduced by
It seems like $config defined by parameter $config on line 231 can also be of type object<Composer\Package\PackageInterface>; however, Composer\Package\Loader\ArrayLoader::load() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
235
    }
236
237
    /**
238
     * @param InputInterface $input
239
     * @param OutputInterface $output
240
     * @param array|PackageInterface $config
241
     * @param string $targetFolder
242
     * @param bool $preferSource
243
     * @return \Composer\Package\CompletePackage
244
     */
245
    protected function downloadByComposerConfig(InputInterface $input, OutputInterface $output, $config, $targetFolder,
246
        $preferSource = true
247
    ) {
248
        $dm = $this->getComposerDownloadManager($input, $output);
249
        if (!$config instanceof PackageInterface) {
250
            $package = $this->createComposerPackageByConfig($config);
251
        } else {
252
            $package = $config;
253
        }
254
255
        $helper = new \N98\Util\Console\Helper\MagentoHelper();
256
        $helper->detect($targetFolder);
257
        if ($this->isSourceTypeRepository($package->getSourceType()) && $helper->getRootFolder() == $targetFolder) {
258
            $package->setInstallationSource('source');
259
            $this->checkRepository($package, $targetFolder);
260
            $dm->update($package, $package, $targetFolder);
261
        } else {
262
            $dm->download($package, $targetFolder, $preferSource);
263
        }
264
265
        return $package;
266
    }
267
268
    /**
269
     * brings locally cached repository up to date if it is missing the requested tag
270
     *
271
     * @param PackageInterface $package
272
     * @param string $targetFolder
273
     */
274
    protected function checkRepository($package, $targetFolder)
275
    {
276
        if ($package->getSourceType() == 'git') {
277
            $command = sprintf(
278
                'cd %s && git rev-parse refs/tags/%s',
279
                escapeshellarg($this->normalizePath($targetFolder)),
280
                escapeshellarg($package->getSourceReference())
281
            );
282
            $existingTags = shell_exec($command);
283
            if (!$existingTags) {
284
                $command = sprintf('cd %s && git fetch', escapeshellarg($this->normalizePath($targetFolder)));
285
                shell_exec($command);
286
            }
287
        } elseif ($package->getSourceType() == 'hg') {
288
            $command = sprintf(
289
                'cd %s && hg log --template "{tags}" -r %s',
290
                escapeshellarg($targetFolder),
291
                escapeshellarg($package->getSourceReference())
292
            );
293
            $existingTag = shell_exec($command);
294
            if ($existingTag === $package->getSourceReference()) {
295
                $command = sprintf('cd %s && hg pull', escapeshellarg($targetFolder));
296
                shell_exec($command);
297
            }
298
        }
299
    }
300
301
    /**
302
     * normalize paths on windows / cygwin / msysgit
303
     *
304
     * when using a path value that has been created in a cygwin shell but then PHP uses it inside a cmd shell it needs
305
     * to be filtered.
306
     *
307
     * @param  string $path
308
     * @return string
309
     */
310
    protected function normalizePath($path)
311
    {
312
        if (defined('PHP_WINDOWS_VERSION_BUILD')) {
313
            $path = strtr($path, '/', '\\');
314
        }
315
        return $path;
316
    }
317
318
    /**
319
     * obtain composer
320
     *
321
     * @param InputInterface  $input
322
     * @param OutputInterface $output
323
     *
324
     * @return \Composer\Composer
325
     */
326
    protected function getComposer(InputInterface $input, OutputInterface $output)
327
    {
328
        $io = new ConsoleIO($input, $output, $this->getHelperSet());
0 ignored issues
show
Bug introduced by
It seems like $this->getHelperSet() can be null; however, __construct() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
329
        $config = array(
330
            'config' => array(
331
                'secure-http' => false,
332
            ),
333
        );
334
335
        return ComposerFactory::create($io, $config);
336
    }
337
338
    /**
339
     * @param string $alias
340
     * @param string $message
341
     * @return AbstractMagentoCommand
342
     */
343
    protected function addDeprecatedAlias($alias, $message)
344
    {
345
        $this->_deprecatedAlias[$alias] = $message;
346
347
        return $this;
348
    }
349
350
    /**
351
     * @param InputInterface $input
352
     * @param OutputInterface $output
353
     */
354
    protected function checkDeprecatedAliases(InputInterface $input, OutputInterface $output)
355
    {
356
        if (isset($this->_deprecatedAlias[$input->getArgument('command')])) {
357
            $output->writeln(
358
                '<error>Deprecated:</error> <comment>' . $this->_deprecatedAlias[$input->getArgument('command')] .
359
                '</comment>'
360
            );
361
        }
362
    }
363
364
    /**
365
     * Magento 1 / 2 switches
366
     *
367
     * @param string $mage1code Magento 1 class code
368
     * @param string $mage2class Magento 2 class name
369
     * @return \Mage_Core_Model_Abstract
370
     */
371
    protected function _getModel($mage1code, $mage2class)
372
    {
373
        if ($this->_magentoMajorVersion == self::MAGENTO_MAJOR_VERSION_2) {
374
            return \Mage::getModel($mage2class);
375
        } else {
376
            return \Mage::getModel($mage1code);
377
        }
378
    }
379
380
    /**
381
     * Magento 1 / 2 switches
382
     *
383
     * @param string $mage1code Magento 1 class code
384
     * @param string $mage2class Magento 2 class name
385
     * @return \Mage_Core_Model_Abstract
386
     */
387
    protected function _getResourceModel($mage1code, $mage2class)
388
    {
389
        if ($this->_magentoMajorVersion == self::MAGENTO_MAJOR_VERSION_2) {
390
            return \Mage::getResourceModel($mage2class);
391
        } else {
392
            return \Mage::getResourceModel($mage1code);
393
        }
394
    }
395
396
    /**
397
     * Magento 1 / 2 switches
398
     *
399
     * @param string $mage1code Magento 1 class code
400
     * @param string $mage2class Magento 2 class name
401
     * @return \Mage_Core_Model_Abstract
402
     */
403
    protected function _getResourceSingleton($mage1code, $mage2class)
404
    {
405
        if ($this->_magentoMajorVersion == self::MAGENTO_MAJOR_VERSION_2) {
406
            return \Mage::getResourceSingleton($mage2class);
407
        } else {
408
            return \Mage::getResourceSingleton($mage1code);
409
        }
410
    }
411
412
    /**
413
     * @param string $value
414
     * @return bool
415
     */
416
    protected function _parseBoolOption($value)
417
    {
418
        return StringTyped::parseBoolOption($value);
419
    }
420
421
    /**
422
     * @param string $value
423
     * @return string
424
     */
425
    protected function formatActive($value)
426
    {
427
        return StringTyped::formatActive($value);
428
    }
429
430
    /**
431
     * @param InputInterface  $input
432
     * @param OutputInterface $output
433
     *
434
     * @return int
435
     */
436
    public function run(InputInterface $input, OutputInterface $output)
437
    {
438
        $this->getHelperSet()->setCommand($this);
439
440
        return parent::run($input, $output);
441
    }
442
443
    /**
444
     * @param InputInterface $input
445
     * @param OutputInterface $output
446
     */
447
    protected function chooseInstallationFolder(InputInterface $input, OutputInterface $output)
448
    {
449
        /**
450
         * @param string $folderName
451
         *
452
         * @return string
453
         */
454
        $validateInstallationFolder = function ($folderName) use ($input) {
455
456
            $folderName = rtrim(trim($folderName, ' '), '/');
457
            // resolve folder-name to current working directory if relative
458
            if (substr($folderName, 0, 1) == '.') {
459
                $cwd = OperatingSystem::getCwd();
460
                $folderName = $cwd . substr($folderName, 1);
461
            }
462
463
            if (empty($folderName)) {
464
                throw new InvalidArgumentException('Installation folder cannot be empty');
465
            }
466
467
            if (!is_dir($folderName)) {
468
                if (!@mkdir($folderName, 0777, true)) {
469
                    throw new InvalidArgumentException('Cannot create folder.');
470
                }
471
472
                return $folderName;
473
            }
474
475
            if ($input->hasOption('noDownload') && $input->getOption('noDownload')) {
476
                /** @var MagentoHelper $magentoHelper */
477
                $magentoHelper = new MagentoHelper();
478
                $magentoHelper->detect($folderName);
479
                if ($magentoHelper->getRootFolder() !== $folderName) {
480
                    throw new InvalidArgumentException(
481
                        sprintf(
482
                            'Folder %s is not a Magento working copy.',
483
                            $folderName
484
                        )
485
                    );
486
                }
487
488
                $localXml = $folderName . '/app/etc/local.xml';
489
                if (file_exists($localXml)) {
490
                    throw new InvalidArgumentException(
491
                        sprintf(
492
                            'Magento working copy in %s seems already installed. Please remove %s and retry.',
493
                            $folderName,
494
                            $localXml
495
                        )
496
                    );
497
                }
498
            }
499
500
            return $folderName;
501
        };
502
503
        if (($installationFolder = $input->getOption('installationFolder')) == null) {
504
            $defaultFolder = './magento';
505
            $question[] = "<question>Enter installation folder:</question> [<comment>" . $defaultFolder . "</comment>]";
0 ignored issues
show
Coding Style Comprehensibility introduced by
$question was never initialized. Although not strictly required by PHP, it is generally a good practice to add $question = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
506
507
            $installationFolder = $this->getHelper('dialog')->askAndValidate(
508
                $output,
509
                $question,
510
                $validateInstallationFolder,
511
                false,
512
                $defaultFolder
513
            );
514
        } else {
515
            // @Todo improve validation and bring it to 1 single function
516
            $installationFolder = $validateInstallationFolder($installationFolder);
517
        }
518
519
        $this->config['installationFolder'] = realpath($installationFolder);
0 ignored issues
show
Bug introduced by
The property config does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
520
        \chdir($this->config['installationFolder']);
521
    }
522
523
    /**
524
     * @param string $type
525
     *
526
     * @return bool
527
     */
528
    protected function isSourceTypeRepository($type)
529
    {
530
        return in_array($type, array('git', 'hg'));
531
    }
532
533
    /**
534
     * @param string $argument
535
     * @param InputInterface $input
536
     * @param OutputInterface $output
537
     * @param string $message
538
     * @return string
539
     */
540
    protected function getOrAskForArgument($argument, InputInterface $input, OutputInterface $output, $message = null)
541
    {
542
        $inputArgument = $input->getArgument($argument);
543
        if ($inputArgument === null) {
544
            $message = $this->getArgumentMessage($argument, $message);
545
546
            /** @var  $dialog  \Symfony\Component\Console\Helper\DialogHelper */
547
            $dialog = $this->getHelper('dialog');
548
            return $dialog->ask($output, $message);
549
        }
550
551
        return $inputArgument;
552
    }
553
554
    /**
555
     * @param array           $entries zero-indexed array of entries (represented by strings) to select from
556
     * @param OutputInterface $output
557
     * @param string          $question
558
     */
559
    protected function askForArrayEntry(array $entries, OutputInterface $output, $question)
560
    {
561
        $dialog = '';
562
        foreach ($entries as $key => $entry) {
563
            $dialog .= '<comment>[' . ($key + 1) . ']</comment> ' . $entry . "\n";
564
        }
565
        $dialog .= "<question>{$question}</question> ";
566
567
        $selected = $this->getHelper('dialog')->askAndValidate($output, $dialog, function ($typeInput) use ($entries) {
568
            if (!in_array($typeInput, range(1, count($entries)))) {
569
                throw new InvalidArgumentException('Invalid type');
570
            }
571
572
            return $typeInput;
573
        });
574
575
        return $entries[$selected - 1];
576
    }
577
578
    /**
579
     * @param string $argument
580
     * @param string $message [optional]
581
     * @return string
582
     */
583
    protected function getArgumentMessage($argument, $message = null)
584
    {
585
        if (null === $message) {
586
            $message = ucfirst($argument);
587
        }
588
589
        return sprintf('<question>%s:</question>', $message);
590
    }
591
}
592