RunCommand::getBasePath()   A
last analyzed

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\BrokerImpl;
13
use Churn\Event\Event\AfterAnalysisEvent;
14
use Churn\Event\Event\BeforeAnalysisEvent;
15
use Churn\Event\HookLoader;
16
use Churn\File\FileFinder;
17
use Churn\File\FileHelper;
18
use Churn\Process\CacheProcessFactory;
19
use Churn\Process\ConcreteProcessFactory;
20
use Churn\Process\ProcessFactory;
21
use Churn\Process\ProcessHandlerFactory;
22
use Churn\Result\ResultAccumulator;
23
use Churn\Result\ResultsRendererFactory;
24
use InvalidArgumentException;
25
use Symfony\Component\Console\Command\Command;
26
use Symfony\Component\Console\Input\InputArgument;
27
use Symfony\Component\Console\Input\InputInterface;
28
use Symfony\Component\Console\Input\InputOption;
29
use Symfony\Component\Console\Output\ConsoleOutputInterface;
30
use Symfony\Component\Console\Output\NullOutput;
31
use Symfony\Component\Console\Output\OutputInterface;
32
use Symfony\Component\Console\Output\StreamOutput;
33
use Webmozart\Assert\Assert;
34
35
/**
36
 * @internal
37
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
38
 */
39
final class RunCommand extends Command
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
    #[\Override]
88
    protected function configure(): void
89
    {
90
        $this->setName('run')
91
            ->addArgument('paths', InputArgument::IS_ARRAY, 'Path to source to check.')
92
            ->addOption('configuration', 'c', InputOption::VALUE_REQUIRED, 'Path to the configuration file', 'churn.yml') // @codingStandardsIgnoreLine
93
            ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format to use', 'text')
94
            ->addOption('output', 'o', InputOption::VALUE_REQUIRED, 'The path where to write the result')
95
            ->addOption('parallel', null, InputOption::VALUE_REQUIRED, 'Number of parallel jobs')
96
            ->addOption('progress', 'p', InputOption::VALUE_NONE, 'Show progress bar')
97
            ->addOption('quiet', 'q', InputOption::VALUE_NONE, 'Suppress all normal output')
98
            ->setDescription('Check files')
99
            ->setHelp('Checks the churn on the provided path argument(s).');
100
    }
101
102
    /**
103
     * Execute the command.
104
     *
105
     * @param InputInterface $input Input.
106
     * @param OutputInterface $output Output.
107
     */
108
    #[\Override]
109
    protected function execute(InputInterface $input, OutputInterface $output): int
110
    {
111
        if (true === $input->getOption('quiet')) {
112
            $output = new NullOutput();
113
        }
114
        $this->printLogo($input, $output);
115
        $config = $this->getConfiguration($input, $output);
116
        $broker = new BrokerImpl();
117
        (new HookLoader($config->getDirPath()))->attachHooks($config->getHooks(), $broker);
118
        if (true === $input->getOption('progress')) {
119
            $broker->subscribe(new ProgressBarSubscriber($output));
120
        }
121
        $report = $this->analyze($input, $config, $broker);
122
        $this->writeResult($input, $output, $report);
123
124
        return (int) (new MaxScoreChecker($config->getMaxScoreThreshold()))->isOverThreshold($input, $output, $report);
125
    }
126
127
    /**
128
     * @param InputInterface $input Input.
129
     * @param OutputInterface $output Output.
130
     */
131
    private function getConfiguration(InputInterface $input, OutputInterface $output): Config
132
    {
133
        $isDefaultValue = !$input->hasParameterOption('--configuration') && !$input->hasParameterOption('-c');
134
        $config = Loader::fromPath((string) $input->getOption('configuration'), $isDefaultValue);
135
        if ([] !== $input->getArgument('paths')) {
136
            $config->setDirectoriesToScan((array) $input->getArgument('paths'));
137
        }
138
139
        $this->checkConfiguration($config, $output);
140
141
        if (null !== $input->getOption('parallel')) {
142
            Assert::integerish($input->getOption('parallel'), 'Amount of parallel jobs should be an integer');
143
            $config->setParallelJobs((int) $input->getOption('parallel'));
144
        }
145
146
        return $config;
147
    }
148
149
    /**
150
     * @param Config $config The configuration object.
151
     * @param OutputInterface $output Output.
152
     * @throws InvalidArgumentException If paths argument invalid.
153
     */
154
    private function checkConfiguration(Config $config, OutputInterface $output): void
155
    {
156
        $unrecognizedKeys = $config->getUnrecognizedKeys();
157
        if ([] !== $unrecognizedKeys) {
158
            $output = $output instanceof ConsoleOutputInterface
159
                ? $output->getErrorOutput()
160
                : $output;
161
            $output->writeln('<error>Unrecognized configuration keys: ' . \implode(', ', $unrecognizedKeys) . "</>\n");
162
        }
163
164
        if ([] === $config->getDirectoriesToScan()) {
165
            throw new InvalidArgumentException(
166
                'Provide the directories you want to scan as arguments, ' .
167
                'or configure them under "directoriesToScan" in your churn.yml file.'
168
            );
169
        }
170
    }
171
172
    /**
173
     * Run the actual analysis.
174
     *
175
     * @param InputInterface $input Input.
176
     * @param Config $config The configuration object.
177
     * @param Broker $broker The event broker.
178
     */
179
    private function analyze(InputInterface $input, Config $config, Broker $broker): ResultAccumulator
180
    {
181
        $broker->subscribe($report = new ResultAccumulator($config->getFilesToShow(), $config->getMinScoreToShow()));
182
        $broker->subscribe($processFactory = $this->getProcessFactory($config));
183
        $broker->notify(new BeforeAnalysisEvent());
184
        $basePath = $this->getBasePath($input, $config);
185
        $filesFinder = (new FileFinder($config->getFileExtensions(), $config->getFilesToIgnore(), $basePath))
186
            ->getPhpFiles($config->getDirectoriesToScan());
187
        $this->processHandlerFactory->getProcessHandler($config, $broker)->process($filesFinder, $processFactory);
188
        $broker->notify(new AfterAnalysisEvent($report));
189
190
        return $report;
191
    }
192
193
    /**
194
     * @param InputInterface $input Input.
195
     * @param Config $config The configuration object.
196
     * @return string The base path.
197
     */
198
    private function getBasePath(InputInterface $input, Config $config): string
199
    {
200
        return [] === $input->getArgument('paths')
201
            ? $config->getDirPath()
202
            : (string) \getcwd();
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') && '' === (string) $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
        $outputPath = $input->getOption('output');
246
        if (\is_string($outputPath) && '' !== $outputPath) {
247
            $handler = \fopen($outputPath, 'w+');
248
            Assert::notFalse($handler, 'Cannot write into: ' . $outputPath);
249
            $output = new StreamOutput($handler, OutputInterface::VERBOSITY_NORMAL, false);
250
        }
251
252
        $renderer = $this->renderFactory->getRenderer($input->getOption('format'));
253
        $renderer->render($output, $report->toArray());
254
    }
255
}
256