Passed
Push — master ( 945062...632d59 )
by Fabien
02:13
created

RunCommand::getBasePath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 2
dl 0
loc 5
rs 10
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Churn\Command;
6
7
use Churn\Command\Helper\MaxScoreChecker;
8
use Churn\Command\Helper\ProgressBarSubscriber;
9
use Churn\Configuration\Config;
10
use Churn\Configuration\Loader;
11
use Churn\Event\Broker;
12
use Churn\Event\Event\AfterAnalysisEvent;
13
use Churn\Event\Event\BeforeAnalysisEvent;
14
use Churn\Event\HookLoader;
15
use Churn\File\FileFinder;
16
use Churn\File\FileHelper;
17
use Churn\Process\CacheProcessFactory;
18
use Churn\Process\ConcreteProcessFactory;
19
use Churn\Process\ProcessFactory;
20
use Churn\Process\ProcessHandlerFactory;
21
use Churn\Result\ResultAccumulator;
22
use Churn\Result\ResultsRendererFactory;
23
use InvalidArgumentException;
24
use Symfony\Component\Console\Command\Command;
25
use Symfony\Component\Console\Input\InputArgument;
26
use Symfony\Component\Console\Input\InputInterface;
27
use Symfony\Component\Console\Input\InputOption;
28
use Symfony\Component\Console\Output\ConsoleOutputInterface;
29
use Symfony\Component\Console\Output\NullOutput;
30
use Symfony\Component\Console\Output\OutputInterface;
31
use Symfony\Component\Console\Output\StreamOutput;
32
use Webmozart\Assert\Assert;
33
34
/**
35
 * @internal
36
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
37
 */
38
class RunCommand extends Command
39
{
40
41
    public const LOGO = "
42
    ___  _   _  __  __  ____  _  _     ____  _   _  ____
43
   / __)( )_( )(  )(  )(  _ \( \( )___(  _ \( )_( )(  _ \
44
  ( (__  ) _ (  )(__)(  )   / )  ((___))___/ ) _ (  )___/
45
   \___)(_) (_)(______)(_)\_)(_)\_)   (__)  (_) (_)(__)
46
";
47
48
    /**
49
     * The process handler factory.
50
     *
51
     * @var ProcessHandlerFactory
52
     */
53
    private $processHandlerFactory;
54
55
    /**
56
     * The renderer factory.
57
     *
58
     * @var ResultsRendererFactory
59
     */
60
    private $renderFactory;
61
62
    /**
63
     * Class constructor.
64
     *
65
     * @param ProcessHandlerFactory $processHandlerFactory The process handler factory.
66
     * @param ResultsRendererFactory $renderFactory The Results Renderer Factory.
67
     */
68
    public function __construct(ProcessHandlerFactory $processHandlerFactory, ResultsRendererFactory $renderFactory)
69
    {
70
        parent::__construct();
71
72
        $this->processHandlerFactory = $processHandlerFactory;
73
        $this->renderFactory = $renderFactory;
74
    }
75
76
    /**
77
     * Returns a new instance of the command.
78
     */
79
    public static function newInstance(): self
80
    {
81
        return new self(new ProcessHandlerFactory(), new ResultsRendererFactory());
82
    }
83
84
    /**
85
     * Configure the command
86
     */
87
    protected function configure(): void
88
    {
89
        $this->setName('run')
90
            ->addArgument('paths', InputArgument::IS_ARRAY, 'Path to source to check.')
91
            ->addOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Path to the configuration file', 'churn.yml') // @codingStandardsIgnoreLine
92
            ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format to use', 'text')
93
            ->addOption('output', 'o', InputOption::VALUE_REQUIRED, 'The path where to write the result')
94
            ->addOption('parallel', null, InputOption::VALUE_REQUIRED, 'Number of parallel jobs')
95
            ->addOption('progress', 'p', InputOption::VALUE_NONE, 'Show progress bar')
96
            ->addOption('quiet', 'q', InputOption::VALUE_NONE, 'Suppress all normal output')
97
            ->setDescription('Check files')
98
            ->setHelp('Checks the churn on the provided path argument(s).');
99
    }
100
101
    /**
102
     * Execute the command.
103
     *
104
     * @param InputInterface $input Input.
105
     * @param OutputInterface $output Output.
106
     */
107
    protected function execute(InputInterface $input, OutputInterface $output): int
108
    {
109
        if (true === $input->getOption('quiet')) {
110
            $output = new NullOutput();
111
        }
112
        $this->printLogo($input, $output);
113
        $config = $this->getConfiguration($input, $output);
114
        $broker = new Broker();
115
        (new HookLoader($config->getDirPath()))->attachHooks($config->getHooks(), $broker);
116
        if (true === $input->getOption('progress')) {
117
            $broker->subscribe(new ProgressBarSubscriber($output));
118
        }
119
        $report = $this->analyze($input, $config, $broker);
120
        $this->writeResult($input, $output, $report);
121
122
        return (int) (new MaxScoreChecker($config->getMaxScoreThreshold()))->isOverThreshold($input, $output, $report);
123
    }
124
125
    /**
126
     * @param InputInterface $input Input.
127
     * @param OutputInterface $output Output.
128
     */
129
    private function getConfiguration(InputInterface $input, OutputInterface $output): Config
130
    {
131
        $isDefaultValue = !$input->hasParameterOption('--configuration') && !$input->hasParameterOption('-c');
132
        $config = Loader::fromPath((string) $input->getOption('configuration'), $isDefaultValue);
133
        if ([] !== $input->getArgument('paths')) {
134
            $config->setDirectoriesToScan((array) $input->getArgument('paths'));
135
        }
136
137
        $this->checkConfiguration($config, $output);
138
139
        if (null !== $input->getOption('parallel')) {
140
            Assert::integerish($input->getOption('parallel'), 'Amount of parallel jobs should be an integer');
141
            $config->setParallelJobs((int) $input->getOption('parallel'));
142
        }
143
144
        return $config;
145
    }
146
147
    /**
148
     * @param Config $config The configuration object.
149
     * @param OutputInterface $output Output.
150
     * @throws InvalidArgumentException If paths argument invalid.
151
     */
152
    private function checkConfiguration(Config $config, OutputInterface $output): void
153
    {
154
        $unrecognizedKeys = $config->getUnrecognizedKeys();
155
        if ([] !== $unrecognizedKeys) {
156
            $output = $output instanceof ConsoleOutputInterface
157
                ? $output->getErrorOutput()
158
                : $output;
159
            $output->writeln('<error>Unrecognized configuration keys: ' . \implode(', ', $unrecognizedKeys) . "</>\n");
160
        }
161
162
        if ([] === $config->getDirectoriesToScan()) {
163
            throw new InvalidArgumentException(
164
                'Provide the directories you want to scan as arguments, ' .
165
                'or configure them under "directoriesToScan" in your churn.yml file.'
166
            );
167
        }
168
    }
169
170
    /**
171
     * Run the actual analysis.
172
     *
173
     * @param InputInterface $input Input.
174
     * @param Config $config The configuration object.
175
     * @param Broker $broker The event broker.
176
     */
177
    private function analyze(InputInterface $input, Config $config, Broker $broker): ResultAccumulator
178
    {
179
        $broker->subscribe($report = new ResultAccumulator($config->getFilesToShow(), $config->getMinScoreToShow()));
180
        $broker->subscribe($processFactory = $this->getProcessFactory($config));
181
        $broker->notify(new BeforeAnalysisEvent());
182
        $basePath = $this->getBasePath($input, $config);
183
        $filesFinder = (new FileFinder($config->getFileExtensions(), $config->getFilesToIgnore(), $basePath))
184
            ->getPhpFiles($config->getDirectoriesToScan());
185
        $this->processHandlerFactory->getProcessHandler($config, $broker)->process($filesFinder, $processFactory);
186
        $broker->notify(new AfterAnalysisEvent($report));
187
188
        return $report;
189
    }
190
191
    /**
192
     * @param InputInterface $input Input.
193
     * @param Config $config The configuration object.
194
     * @return string The base path.
195
     */
196
    private function getBasePath(InputInterface $input, Config $config): string
197
    {
198
        return [] === $input->getArgument('paths')
199
            ? $config->getDirPath()
200
            : \getcwd();
201
    }
202
203
    /**
204
     * @param Config $config The configuration object.
205
     */
206
    private function getProcessFactory(Config $config): ProcessFactory
207
    {
208
        $factory = new ConcreteProcessFactory($config->getVCS(), $config->getCommitsSince());
209
210
        $path = $config->getCachePath();
211
        if (null !== $path) {
212
            $basePath = $config->getDirPath();
213
            $factory = new CacheProcessFactory(FileHelper::toAbsolutePath($path, $basePath), $factory);
214
        }
215
216
        return $factory;
217
    }
218
219
    /**
220
     * @param InputInterface $input Input.
221
     * @param OutputInterface $output Output.
222
     */
223
    private function printLogo(InputInterface $input, OutputInterface $output): void
224
    {
225
        if ('text' !== $input->getOption('format') && empty($input->getOption('output'))) {
226
            return;
227
        }
228
229
        $output->writeln(self::LOGO);
230
    }
231
232
    /**
233
     * @param InputInterface $input Input.
234
     * @param OutputInterface $output Output.
235
     * @param ResultAccumulator $report The report to write.
236
     */
237
    private function writeResult(InputInterface $input, OutputInterface $output, ResultAccumulator $report): void
238
    {
239
        if (true === $input->getOption('progress')) {
240
            $output->writeln("\n");
241
        }
242
243
        if (!empty($input->getOption('output'))) {
244
            $output = new StreamOutput(
245
                \fopen((string) $input->getOption('output'), 'w+'),
246
                OutputInterface::VERBOSITY_NORMAL,
247
                false
248
            );
249
        }
250
251
        $renderer = $this->renderFactory->getRenderer($input->getOption('format'));
252
        $renderer->render($output, $report->toArray());
253
    }
254
}
255