Passed
Push — master ( 369dd9...0c3679 )
by Fabien
01:59
created

RunCommand::attachHooks()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 6
c 0
b 0
f 0
nc 4
nop 2
dl 0
loc 11
rs 10
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
        $config = $this->getConfiguration($input);
112
        $broker = new Broker();
113
        (new HookLoader($config->getDirPath()))->attachHooks($config->getHooks(), $broker);
114
        $this->printLogo($input, $output);
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
     * @throws InvalidArgumentException If paths argument invalid.
127
     */
128
    private function getConfiguration(InputInterface $input): 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
        if ([] === $config->getDirectoriesToScan()) {
137
            throw new InvalidArgumentException(
138
                'Provide the directories you want to scan as arguments, ' .
139
                'or configure them under "directoriesToScan" in your churn.yml file.'
140
            );
141
        }
142
143
        if (null !== $input->getOption('parallel')) {
144
            Assert::integerish($input->getOption('parallel'), 'Amount of parallel jobs should be an integer');
145
            $config->setParallelJobs((int) $input->getOption('parallel'));
146
        }
147
148
        return $config;
149
    }
150
151
    /**
152
     * Run the actual analysis.
153
     *
154
     * @param InputInterface $input Input.
155
     * @param Config $config The configuration object.
156
     * @param Broker $broker The event broker.
157
     */
158
    private function analyze(InputInterface $input, Config $config, Broker $broker): ResultAccumulator
159
    {
160
        $broker->subscribe($report = new ResultAccumulator($config->getFilesToShow(), $config->getMinScoreToShow()));
161
        $broker->subscribe($processFactory = $this->getProcessFactory($config));
162
        $broker->notify(new BeforeAnalysisEvent());
163
        $filesFinder = (new FileFinder($config->getFileExtensions(), $config->getFilesToIgnore()))
164
            ->getPhpFiles($this->getDirectoriesToScan($input, $config));
165
        $this->processHandlerFactory->getProcessHandler($config, $broker)->process($filesFinder, $processFactory);
166
        $broker->notify(new AfterAnalysisEvent($report));
167
168
        return $report;
169
    }
170
171
    /**
172
     * @param InputInterface $input Input.
173
     * @param Config $config The configuration object.
174
     * @return array<string> Array of absolute paths.
175
     */
176
    private function getDirectoriesToScan(InputInterface $input, Config $config): array
177
    {
178
        $basePath = [] === $input->getArgument('paths')
179
            ? $config->getDirPath()
180
            : \getcwd();
181
        $paths = [];
182
183
        foreach ($config->getDirectoriesToScan() as $path) {
184
            $paths[] = FileHelper::toAbsolutePath($path, $basePath);
185
        }
186
187
        return $paths;
188
    }
189
190
    /**
191
     * @param Config $config The configuration object.
192
     */
193
    private function getProcessFactory(Config $config): ProcessFactory
194
    {
195
        $factory = new ConcreteProcessFactory($config->getVCS(), $config->getCommitsSince());
196
197
        if (null !== $config->getCachePath()) {
198
            $basePath = $config->getDirPath();
199
            $path = $config->getCachePath();
200
            $factory = new CacheProcessFactory(FileHelper::toAbsolutePath($path, $basePath), $factory);
201
        }
202
203
        return $factory;
204
    }
205
206
    /**
207
     * @param InputInterface $input Input.
208
     * @param OutputInterface $output Output.
209
     */
210
    private function printLogo(InputInterface $input, OutputInterface $output): void
211
    {
212
        if ('text' !== $input->getOption('format') && empty($input->getOption('output'))) {
213
            return;
214
        }
215
216
        $output->writeln(self::LOGO);
217
    }
218
219
    /**
220
     * @param InputInterface $input Input.
221
     * @param OutputInterface $output Output.
222
     * @param ResultAccumulator $report The report to write.
223
     */
224
    private function writeResult(InputInterface $input, OutputInterface $output, ResultAccumulator $report): void
225
    {
226
        if (true === $input->getOption('progress')) {
227
            $output->writeln("\n");
228
        }
229
230
        if (!empty($input->getOption('output'))) {
231
            $output = new StreamOutput(
232
                \fopen((string) $input->getOption('output'), 'w+'),
233
                OutputInterface::VERBOSITY_NORMAL,
234
                false
235
            );
236
        }
237
238
        $renderer = $this->renderFactory->getRenderer($input->getOption('format'));
239
        $renderer->render($output, $report->toArray());
240
    }
241
}
242