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