Passed
Push — master ( 78c9b9...e61edd )
by Fabien
02:08
created

RunCommand::checkConfiguration()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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