Completed
Push — develop ( 5d5326...be0e9e )
by Christian
02:27
created

src/N98/Magento/Application.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace N98\Magento;
4
5
use BadMethodCallException;
6
use Composer\Autoload\ClassLoader;
7
use Exception;
8
use Magento\Framework\ObjectManagerInterface;
9
use N98\Magento\Application\ApplicationAwareInterface;
10
use N98\Magento\Application\Config;
11
use N98\Magento\Application\ConfigurationLoader;
12
use N98\Magento\Application\Console\Events;
13
use N98\Magento\Application\DetectionResult;
14
use N98\Magento\Application\Magento1Initializer;
15
use N98\Magento\Application\Magento2Initializer;
16
use N98\Magento\Application\MagentoDetector;
17
use N98\Util\Console\Helper\TwigHelper;
18
use Symfony\Component\Console\Application as BaseApplication;
19
use Symfony\Component\Console\Command\Command;
20
use Symfony\Component\Console\Event\ConsoleEvent;
21
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
22
use Symfony\Component\Console\Input\ArgvInput;
23
use Symfony\Component\Console\Input\InputDefinition;
24
use Symfony\Component\Console\Input\InputInterface;
25
use Symfony\Component\Console\Input\InputOption;
26
use Symfony\Component\Console\Output\ConsoleOutput;
27
use Symfony\Component\Console\Output\OutputInterface;
28
use Symfony\Component\EventDispatcher\EventDispatcher;
29
use UnexpectedValueException;
30
31
/**
32
 * Class Application
33
 * @package N98\Magento
34
 */
35
class Application extends BaseApplication
36
{
37
    /**
38
     * @var string
39
     */
40
    const APP_NAME = 'n98-magerun2';
41
42
    /**
43
     * @var string
44
     */
45
    const APP_VERSION = '4.2.0';
46
47
    /**
48
     * @var int
49
     */
50
    const MAGENTO_MAJOR_VERSION_1 = 1;
51
    const MAGENTO_MAJOR_VERSION_2 = 2;
52
53
    /**
54
     * @var string
55
     */
56
    private static $logo = "
57
     ___ ___                                       ___
58
 _ _/ _ ( _ )___ _ __  __ _ __ _ ___ _ _ _  _ _ _ |_  )
59
| ' \\_, / _ \\___| '  \\/ _` / _` / -_) '_| || | ' \\ / /
60
|_||_/_/\\___/   |_|_|_\\__,_\\__, \\___|_|  \\_,_|_||_/___|
61
                           |___/
62
";
63
    /**
64
     * @var ClassLoader
65
     */
66
    protected $autoloader;
67
68
    /**
69
     * @var Config
70
     */
71
    protected $config;
72
73
    /**
74
     * @var bool
75
     */
76
    protected $_isPharMode = false;
77
78
    /**
79
     * @var bool
80
     */
81
    protected $_isInitialized = false;
82
83
    /**
84
     * @var EventDispatcher
85
     */
86
    protected $dispatcher;
87
88
    /**
89
     * @var ObjectManagerInterface
90
     */
91
    protected $_objectManager;
92
93
    /**
94
     * @see \N98\Magento\Application::setConfigurationLoader()
95
     * @var ConfigurationLoader
96
     */
97
    private $configurationLoaderInjected;
98
99
    /**
100
     * @var string [optional] root folder not detected, but set via public setter
101
     * @see setMagentoRootFolder()
102
     */
103
    private $magentoRootFolderInjected;
104
105
    /**
106
     * @var int Magento Major Version to operate on by this Magerun application
107
     */
108
    private $magerunMajorVersion = self::MAGENTO_MAJOR_VERSION_2;
109
110
    /**
111
     * @var DetectionResult of the Magento application (e.g. v1/v2, Enterprise/Community, root-path)
112
     */
113
    private $detectionResult;
114
115
    /**
116
     * @var boolean
117
     */
118
    private $autoExit = true;
119
120
    /**
121
     * @param ClassLoader $autoloader
122
     */
123
    public function __construct($autoloader = null)
124
    {
125
        $this->autoloader = $autoloader;
126
        parent::__construct(self::APP_NAME, self::APP_VERSION);
127
128
        $this->preloadClassesBeforeMagentoCore();
129
    }
130
131
    /**
132
     * Sets whether to automatically exit after a command execution or not.
133
     *
134
     * Implemented on this level to allow early exit on configuration exceptions
135
     *
136
     * @see run()
137
     *
138
     * @param bool $boolean Whether to automatically exit after a command execution or not
139
     */
140
    public function setAutoExit($boolean)
141
    {
142
        $this->autoExit = (bool) $boolean;
143
        parent::setAutoExit($boolean);
144
    }
145
146
    /**
147
     * @param bool $mode
148
     */
149
    public function setPharMode($mode)
150
    {
151
        $this->_isPharMode = $mode;
152
    }
153
154
    /**
155
     * @return string
156
     */
157
    public function getHelp()
158
    {
159
        return self::$logo . parent::getHelp();
160
    }
161
162
    public function getLongVersion()
163
    {
164
        return parent::getLongVersion() . ' by <info>netz98 GmbH</info>';
165
    }
166
167
    /**
168
     * @return boolean
169
     */
170
    public function isMagentoEnterprise()
171
    {
172
        return $this->detectionResult->isEnterpriseEdition();
173
    }
174
175
    /**
176
     * @param string $magentoRootFolder
177
     */
178
    public function setMagentoRootFolder($magentoRootFolder)
179
    {
180
        $this->magentoRootFolderInjected = $magentoRootFolder;
181
    }
182
183
    /**
184
     * @return int|null
185
     */
186
    public function getMagentoMajorVersion()
187
    {
188
        return $this->detectionResult ? $this->detectionResult->getMajorVersion() : null;
189
    }
190
191
    /**
192
     * @return array
193
     */
194
    public function getConfig()
195
    {
196
        // TODO(TK) getter for config / getter for config array
197
        return $this->config->getConfig();
198
    }
199
200
    /**
201
     * @param array $config
202
     */
203
    public function setConfig($config)
204
    {
205
        $this->config->setConfig($config);
206
    }
207
208
    /**
209
     * Runs the current application with possible command aliases
210
     *
211
     * @param InputInterface $input An Input instance
212
     * @param OutputInterface $output An Output instance
213
     *
214
     * @return int 0 if everything went fine, or an error code
215
     * @throws \Magento\Framework\Exception\FileSystemException
216
     * @throws \Exception
217
     * @throws \Throwable
218
     */
219
    public function doRun(InputInterface $input, OutputInterface $output)
220
    {
221
        $input = $this->config->checkConfigCommandAlias($input);
222
223
        $event = new ConsoleEvent(new \N98\Magento\Command\DummyCommand(), $input, $output);
224
        $this->dispatcher->dispatch($event, Events::RUN_BEFORE);
225
226
        return parent::doRun($input, $output);
227
    }
228
229
    /**
230
     * Loads and initializes the Magento application
231
     *
232
     * @param bool $soft
233
     *
234
     * @return bool false if magento root folder is not set, true otherwise
235
     * @throws \Exception
236
     */
237
    public function initMagento($soft = false)
238
    {
239
        if ($this->getMagentoRootFolder(true) === null) {
240
            return false;
241
        }
242
243
        $isMagento2 = $this->detectionResult->getMajorVersion() === self::MAGENTO_MAJOR_VERSION_2;
244
        if ($isMagento2) {
245
            $magento2Initializer = new Magento2Initializer($this->getAutoloader());
246
            $app = $magento2Initializer->init($this->getMagentoRootFolder());
247
            $this->_objectManager = $app->getObjectManager();
248
        } else {
249
            $magento1Initializer = new Magento1Initializer($this->getHelperSet());
250
            $magento1Initializer->init();
251
        }
252
253
        return true;
254
    }
255
256
    /**
257
     * @param bool $preventException [optional] on uninitialized magento root folder (returns null then, caution!)
258
     * @return string|null
259
     */
260
    public function getMagentoRootFolder($preventException = false)
261
    {
262
        if (null !== $this->magentoRootFolderInjected) {
263
            return $this->magentoRootFolderInjected;
264
        }
265
266
        if ($preventException) {
267
            return $this->detectionResult ? $this->detectionResult->getRootFolder() : null;
268
        }
269
270
        if (!$this->detectionResult) {
271
            throw new BadMethodCallException('Magento-root-folder is not yet detected (nor set)');
272
        }
273
274
        return $this->detectionResult->getRootFolder();
275
    }
276
277
    /**
278
     * @return ClassLoader
279
     */
280
    public function getAutoloader()
281
    {
282
        return $this->autoloader;
283
    }
284
285
    /**
286
     * @param ClassLoader $autoloader
287
     */
288
    public function setAutoloader(ClassLoader $autoloader)
289
    {
290
        $this->autoloader = $autoloader;
291
    }
292
293
    /**
294
     * @param InputInterface $input [optional]
295
     * @param OutputInterface $output [optional]
296
     *
297
     * @return int
298
     * @throws \Exception
299
     */
300
    public function run(InputInterface $input = null, OutputInterface $output = null)
301
    {
302
        if (null === $input) {
303
            $input = new ArgvInput();
304
        }
305
306
        if (null === $output) {
307
            $output = new ConsoleOutput();
308
        }
309
        $this->_addOutputStyles($output);
310
        if ($output instanceof ConsoleOutput) {
311
            $this->_addOutputStyles($output->getErrorOutput());
312
        }
313
314
        $this->configureIO($input, $output);
315
316
        try {
317
            $this->init([], $input, $output);
318
        } catch (Exception $e) {
319
            $output = new ConsoleOutput();
320
            $this->renderThrowable($e, $output->getErrorOutput());
321
            $exitCode = max(1, min(255, (int) $e->getCode()));
322
            if ($this->autoExit) {
323
                die($exitCode);
324
            }
325
326
            return $exitCode;
327
        }
328
329
        $return = parent::run($input, $output);
330
331
        // Fix for no return values -> used in interactive shell to prevent error output
332
        if ($return === null) {
333
            return 0;
334
        }
335
336
        return $return;
337
    }
338
339
    /**
340
     * @param OutputInterface $output
341
     */
342
    protected function _addOutputStyles(OutputInterface $output)
343
    {
344
        $output->getFormatter()->setStyle('debug', new OutputFormatterStyle('magenta', 'white'));
345
        $output->getFormatter()->setStyle('warning', new OutputFormatterStyle('red', 'yellow', ['bold']));
346
    }
347
348
    /**
349
     * @param array $initConfig [optional]
350
     * @param InputInterface $input [optional]
351
     * @param OutputInterface $output [optional]
352
     *
353
     * @return void
354
     * @throws \Exception
355
     */
356
    public function init(array $initConfig = [], InputInterface $input = null, OutputInterface $output = null)
357
    {
358
        if ($this->_isInitialized) {
359
            return;
360
        }
361
362
        // Suppress DateTime warnings
363
        date_default_timezone_set(@date_default_timezone_get());
364
365
        // Initialize EventDispatcher early
366
        $this->dispatcher = new EventDispatcher();
367
        $this->setDispatcher($this->dispatcher);
368
369
        $input = $input ?: new ArgvInput();
370
        $output = $output ?: new ConsoleOutput();
371
372
        if (null !== $this->config) {
373
            throw new UnexpectedValueException(sprintf('Config already initialized'));
374
        }
375
376
        $loadExternalConfig = !$this->_checkSkipConfigOption($input);
377
378
        $this->config = new Config($initConfig, $this->isPharMode(), $output);
379
        if ($this->configurationLoaderInjected) {
380
            $this->config->setLoader($this->configurationLoaderInjected);
381
        }
382
        $this->config->loadPartialConfig($loadExternalConfig);
383
        $this->detectMagento($input, $output);
384
385
        $configLoader = $this->config->getLoader();
386
        $configLoader->loadStageTwo(
387
            $this->getMagentoRootFolder(true),
388
            $loadExternalConfig,
389
            $this->detectionResult->getMagerunStopFileFolder()
390
        );
391
        $this->config->load();
392
393
        if ($autoloader = $this->autoloader) {
394
            /**
395
             * Include commands shipped by Magento 2 core
396
             */
397
            if (!$this->_checkSkipMagento2CoreCommandsOption($input)) {
398
                $this->registerMagentoCoreCommands($output);
399
            }
400
            $this->config->registerCustomAutoloaders($autoloader);
401
            $this->registerEventSubscribers($input, $output);
402
            $this->config->registerCustomCommands($this);
403
        }
404
405
        $this->registerHelpers();
406
407
        $this->_isInitialized = true;
408
    }
409
410
    /**
411
     * @param InputInterface $input
412
     * @return bool
413
     */
414
    protected function _checkSkipConfigOption(InputInterface $input)
415
    {
416
        return $input->hasParameterOption('--skip-config');
417
    }
418
419
    /**
420
     * @return bool
421
     */
422
    public function isPharMode()
423
    {
424
        return $this->_isPharMode;
425
    }
426
427
    /**
428
     * Search for magento root folder
429
     *
430
     * @param InputInterface $input [optional]
431
     * @param OutputInterface $output [optional]
432
     * @return void
433
     * @throws \Exception
434
     */
435
    public function detectMagento(InputInterface $input = null, OutputInterface $output = null)
436
    {
437
        if ($this->detectionResult) {
438
            return;
439
        }
440
441
        $magentoRootDirectory = $this->getMagentoRootFolder(true);
442
443
        $detector = new MagentoDetector();
444
        $this->detectionResult = $detector->detect(
445
            $input,
0 ignored issues
show
It seems like $input defined by parameter $input on line 435 can be null; however, N98\Magento\Application\MagentoDetector::detect() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
446
            $output,
0 ignored issues
show
It seems like $output defined by parameter $output on line 435 can be null; however, N98\Magento\Application\MagentoDetector::detect() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
447
            $this->config,
448
            $this->getHelperSet(),
449
            $magentoRootDirectory
450
        );
451
452
        if ($this->detectionResult->isDetected()) {
453
            $magentoMajorVersion = $this->detectionResult->getMajorVersion();
454
            if ($magentoMajorVersion !== $this->magerunMajorVersion) {
455
                $magento1Initialiter = new Magento1Initializer($this->getHelperSet());
456
                $magento1Initialiter->init();
457
            }
458
        }
459
    }
460
461
    /**
462
     * @return bool
463
     */
464
    protected function _checkSkipMagento2CoreCommandsOption(InputInterface $input)
465
    {
466
        return $input->hasParameterOption('--skip-core-commands') || getenv('MAGERUN_SKIP_CORE_COMMANDS');
467
    }
468
469
    /**
470
     * Try to bootstrap magento 2 and load cli application
471
     *
472
     * @param OutputInterface $output
473
     */
474
    protected function registerMagentoCoreCommands(OutputInterface $output)
475
    {
476
        $magentoRootFolder = $this->getMagentoRootFolder();
477
        if (0 === strlen($magentoRootFolder)) {
478
            return;
479
        }
480
481
        // Magento was found -> register core cli commands
482
        try {
483
            $this->requireOnce($magentoRootFolder . '/app/bootstrap.php');
484
485
            // Magento 2.3.1 removes phar stream wrapper.
486
            if (!in_array('phar', \stream_get_wrappers(), true)) {
487
                \stream_wrapper_restore('phar');
488
            }
489
        } catch (\Exception $ex) {
490
            $this->renderThrowable($ex, $output);
491
            $output->writeln(
492
                '<info>Use --skip-core-commands to not require the Magento app/bootstrap.php which caused ' .
493
                'the exception.</info>'
494
            );
495
496
            return;
497
        }
498
499
        $coreCliApplication = new \Magento\Framework\Console\Cli();
500
        $coreCliApplicationCommands = $coreCliApplication->all();
501
502
        foreach ($coreCliApplicationCommands as $coreCliApplicationCommand) {
503
            if (OutputInterface::VERBOSITY_DEBUG <= $output->getVerbosity()) {
504
                $output->writeln(
505
                    sprintf(
506
                        '<debug>Add core command </debug> <info>%s</info> -> <comment>%s</comment>',
507
                        $coreCliApplicationCommand->getName(),
508
                        get_class($coreCliApplicationCommand)
509
                    )
510
                );
511
            }
512
            $this->add($coreCliApplicationCommand);
513
        }
514
    }
515
516
    /**
517
     * use require-once inside a function with it's own variable scope w/o any other variables
518
     * and $this unbound.
519
     *
520
     * @param string $path
521
     */
522 View Code Duplication
    private function requireOnce($path)
523
    {
524
        $requireOnce = function () {
525
            require_once func_get_arg(0);
526
        };
527
        if (50400 <= PHP_VERSION_ID) {
528
            $requireOnce->bindTo(null);
529
        }
530
531
        $requireOnce($path);
532
    }
533
534
    /**
535
     * Override standard command registration. We want alias support.
536
     *
537
     * @param Command $command
538
     *
539
     * @return Command
540
     */
541
    public function add(Command $command)
542
    {
543
        if ($this->config) {
544
            $this->config->registerConfigCommandAlias($command);
545
        }
546
547
        return parent::add($command);
548
    }
549
550
    /**
551
     * @return void
552
     */
553
    protected function registerEventSubscribers(InputInterface $input, OutputInterface $output)
554
    {
555
        $config = $this->config->getConfig();
556
557
        if (!isset($config['event']['subscriber'])) {
558
            return;
559
        }
560
561
        $subscriberClasses = $config['event']['subscriber'];
562
        foreach ($subscriberClasses as $subscriberClass) {
563
            $subscriber = new $subscriberClass($this, $input, $output);
564
565
            if ($subscriber instanceof ApplicationAwareInterface) {
566
                $subscriber->setApplication($this);
567
            }
568
569
            $this->dispatcher->addSubscriber($subscriber);
570
        }
571
    }
572
573
    /**
574
     * Add own helpers to helperset.
575
     *
576
     * @return void
577
     */
578
    protected function registerHelpers()
579
    {
580
        $helperSet = $this->getHelperSet();
581
        $config = $this->config->getConfig();
582
583
        if (empty($config)) {
584
            return;
585
        }
586
587
        // Twig
588
        $twigBaseDirs = [
589
            __DIR__ . '/../../../res/twig',
590
        ];
591
        if (isset($config['twig']['baseDirs']) && is_array($config['twig']['baseDirs'])) {
592
            $twigBaseDirs = array_merge(array_reverse($config['twig']['baseDirs']), $twigBaseDirs);
593
        }
594
        $helperSet->set(new TwigHelper($twigBaseDirs), 'twig');
595
596
        foreach ($config['helpers'] as $helperName => $helperClass) {
597
            if (class_exists($helperClass)) {
598
                $helperSet->set(new $helperClass(), $helperName);
599
            }
600
        }
601
    }
602
603
    /**
604
     * @param array $initConfig [optional]
605
     * @param InputInterface $input [optional]
606
     * @param OutputInterface $output [optional]
607
     * @throws \Exception
608
     */
609
    public function reinit($initConfig = [], InputInterface $input = null, OutputInterface $output = null)
610
    {
611
        $this->_isInitialized = false;
612
        $this->detectionResult = null;
613
        $this->config = null;
614
        $this->init($initConfig, $input, $output);
615
    }
616
617
    /**
618
     * @param ConfigurationLoader $configurationLoader
619
     */
620
    public function setConfigurationLoader(ConfigurationLoader $configurationLoader)
621
    {
622
        if ($this->config) {
623
            $this->config->setLoader($configurationLoader);
624
        } else {
625
            /* inject loader to be used later when config is created in */
626
            /* @see \N98\Magento\Application::init() */
627
            $this->configurationLoaderInjected = $configurationLoader;
628
        }
629
    }
630
631
    /**
632
     * @return ObjectManagerInterface
633
     */
634
    public function getObjectManager()
635
    {
636
        return $this->_objectManager;
637
    }
638
639
    /**
640
     * @return InputDefinition
641
     */
642
    protected function getDefaultInputDefinition()
643
    {
644
        $inputDefinition = parent::getDefaultInputDefinition();
645
646
        /**
647
         * Root dir
648
         */
649
        $rootDirOption = new InputOption(
650
            '--root-dir',
651
            '',
652
            InputOption::VALUE_OPTIONAL,
653
            'Force magento root dir. No auto detection'
654
        );
655
        $inputDefinition->addOption($rootDirOption);
656
657
        /**
658
         * Skip config
659
         */
660
        $skipExternalConfig = new InputOption(
661
            '--skip-config',
662
            '',
663
            InputOption::VALUE_NONE,
664
            'Do not load any custom config.'
665
        );
666
        $inputDefinition->addOption($skipExternalConfig);
667
668
        /**
669
         * Skip root check
670
         */
671
        $skipExternalConfig = new InputOption(
672
            '--skip-root-check',
673
            '',
674
            InputOption::VALUE_NONE,
675
            'Do not check if n98-magerun runs as root'
676
        );
677
        $inputDefinition->addOption($skipExternalConfig);
678
679
        /**
680
         * Skip core commands
681
         */
682
        $skipMagento2CoreCommands = new InputOption(
683
            '--skip-core-commands',
684
            '',
685
            InputOption::VALUE_OPTIONAL,
686
            'Do not include Magento 2 core commands'
687
        );
688
        $inputDefinition->addOption($skipMagento2CoreCommands);
689
690
        /**
691
         * Skip Magento compatibility check
692
         */
693
        $skipMagentoCompatibilityCheck = new InputOption(
694
            '--skip-magento-compatibility-check',
695
            '',
696
            InputOption::VALUE_NONE,
697
            'Do not check for Magento version compatibility'
698
        );
699
        $inputDefinition->addOption($skipMagentoCompatibilityCheck);
700
701
        return $inputDefinition;
702
    }
703
704
    /**
705
     * Force to load some classes before the Magento core loads the classes
706
     * in a different version
707
     */
708
    private function preloadClassesBeforeMagentoCore()
709
    {
710
        if ($this->autoloader instanceof ClassLoader) {
711
            $this->autoloader->loadClass('Symfony\Component\Console\Question\Question');
712
        }
713
    }
714
}
715