Completed
Push — master ( 414f33...2cf915 )
by Tarmo
18s queued 12s
created

CheckDependencies::setTableColumnWidths()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 9
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 13
rs 9.9666
1
<?php
2
declare(strict_types = 1);
3
/**
4
 * /src/Command/Utils/CheckDependencies.php
5
 *
6
 * @author TLe, Tarmo Leppänen <[email protected]>
7
 */
8
9
namespace App\Command\Utils;
10
11
use App\Command\Traits\SymfonyStyleTrait;
12
use InvalidArgumentException;
13
use JsonException;
14
use LogicException;
15
use SplFileInfo;
16
use stdClass;
17
use Symfony\Component\Console\Command\Command;
18
use Symfony\Component\Console\Exception\RuntimeException;
19
use Symfony\Component\Console\Helper\ProgressBar;
20
use Symfony\Component\Console\Helper\Table;
21
use Symfony\Component\Console\Helper\TableSeparator;
22
use Symfony\Component\Console\Input\InputInterface;
23
use Symfony\Component\Console\Output\OutputInterface;
24
use Symfony\Component\Console\Style\SymfonyStyle;
25
use Symfony\Component\Finder\Finder;
26
use Symfony\Component\Process\Process;
27
use Throwable;
28
use Traversable;
29
use function array_filter;
30
use function array_map;
31
use function array_unshift;
32
use function count;
33
use function dirname;
34
use function implode;
35
use function is_array;
36
use function iterator_to_array;
37
use function sort;
38
use function sprintf;
39
use function str_replace;
40
use function strlen;
41
42
/**
43
 * Class CheckDependencies
44
 *
45
 * @package App\Command\Utils
46
 * @author TLe, Tarmo Leppänen <[email protected]>
47
 */
48
class CheckDependencies extends Command
49
{
50
    use SymfonyStyleTrait;
51
52
    public function __construct(
53
        private string $projectDir,
54
    ) {
55
        parent::__construct('check-dependencies');
56
57
        $this->setDescription('Console command to check which vendor dependencies has updates');
58
    }
59
60
    /**
61
     * @noinspection PhpMissingParentCallCommonInspection
62
     *
63
     * @throws Throwable
64
     */
65
    protected function execute(InputInterface $input, OutputInterface $output): int
66
    {
67
        $io = $this->getSymfonyStyle($input, $output);
68
        $io->info('Starting to check dependencies...');
69
70
        $directories = $this->getNamespaceDirectories();
71
72
        array_unshift($directories, $this->projectDir);
73
74
        $rows = $this->determineTableRows($io, $directories);
75
76
        /**
77
         * @psalm-suppress RedundantCastGivenDocblockType
78
         * @psalm-suppress ArgumentTypeCoercion
79
         */
80
        $packageNameLength = (int)max(
81
            array_map(
82
                static fn (array $row): int => isset($row[1]) ? strlen($row[1]) : 0,
83
                array_filter($rows, static fn (mixed $row): bool => !$row instanceof TableSeparator)
84
            ) + [0]
85
        );
86
87
        $style = clone Table::getStyleDefinition('box');
88
        $style->setCellHeaderFormat('<info>%s</info>');
89
90
        $table = new Table($output);
91
        $table->setHeaders($this->getHeaders());
92
        $table->setRows($rows);
93
        $table->setStyle($style);
94
95
        $this->setTableColumnWidths($packageNameLength, $table);
96
97
        $rows === []
98
            ? $io->success('Good news, there is not any vendor dependency to update at this time!')
99
            : $table->render();
100
101
        return 0;
102
    }
103
104
    /**
105
     * Method to determine all namespace directories under 'tools' directory.
106
     *
107
     * @return array<int, string>
108
     *
109
     * @throws LogicException
110
     * @throws InvalidArgumentException
111
     */
112
    private function getNamespaceDirectories(): array
113
    {
114
        // Find all main namespace directories under 'tools' directory
115
        $finder = (new Finder())
116
            ->depth(1)
117
            ->ignoreDotFiles(true)
118
            ->directories()
119
            ->in($this->projectDir . DIRECTORY_SEPARATOR . 'tools/');
120
121
        $closure = static fn (SplFileInfo $fileInfo): string => $fileInfo->getPath();
122
123
        /** @var Traversable<SplFileInfo> $iterator */
124
        $iterator = $finder->getIterator();
125
126
        // Determine namespace directories
127
        $directories = array_map($closure, iterator_to_array($iterator));
128
129
        sort($directories);
130
131
        return $directories;
132
    }
133
134
    /**
135
     * Method to determine table rows.
136
     *
137
     * @param array<int, string> $directories
138
     *
139
     * @psalm-return array<int, array<int, string>|TableSeparator>
140
     *
141
     * @throws JsonException
142
     */
143
    private function determineTableRows(SymfonyStyle $io, array $directories): array
144
    {
145
        // Initialize progress bar for process
146
        $progressBar = $this->getProgressBar($io, count($directories), 'Checking all vendor dependencies');
147
148
        // Initialize output rows
149
        $rows = [];
150
151
        $iterator = function (string $directory) use ($progressBar, &$rows): void {
152
            foreach ($this->processNamespacePath($directory) as $row => $data) {
153
                $relativePath = '';
154
155
                // First row of current library
156
                if ($row === 0) {
157
                    // We want to add table separator between different libraries
158
                    if ($rows !== 0) {
159
                        $rows[] = new TableSeparator();
160
                    }
161
162
                    $relativePath = str_replace($this->projectDir, '', $directory) . '/composer.json';
163
                } else {
164
                    $rows[] = [''];
165
                }
166
167
                $rows[] = [dirname($relativePath), $data->name, $data->description, $data->version, $data->latest];
168
169
                if (isset($data->warning)) {
170
                    $rows[] = [''];
171
                    $rows[] = ['', '', '<fg=red>' . $data->warning . '</>'];
172
                }
173
            }
174
175
            $progressBar->advance();
176
        };
177
178
        array_map($iterator, $directories);
179
180
        return $rows;
181
    }
182
183
    /**
184
     * Method to process namespace inside 'tools' directory.
185
     *
186
     * @return array<int, stdClass>
187
     *
188
     * @throws JsonException
189
     */
190
    private function processNamespacePath(string $path): array
191
    {
192
        $command = [
193
            'composer',
194
            'outdated',
195
            '-D',
196
            '-f',
197
            'json',
198
        ];
199
200
        $process = new Process($command, $path);
201
        $process->enableOutput();
202
        $process->run();
203
204
        if ($process->getErrorOutput() !== '' && !($process->getExitCode() === 0 || $process->getExitCode() === null)) {
205
            $message = sprintf(
206
                "Running command '%s' failed with error message:\n%s",
207
                implode(' ', $command),
208
                $process->getErrorOutput()
209
            );
210
211
            throw new RuntimeException($message);
212
        }
213
214
        /** @var stdClass $decoded */
215
        $decoded = json_decode($process->getOutput(), flags: JSON_THROW_ON_ERROR);
216
217
        /** @var array<int, stdClass>|string|null $installed */
218
        $installed = $decoded->installed;
219
220
        return is_array($installed) ? $installed : [];
221
    }
222
223
    /**
224
     * Helper method to get progress bar for console.
225
     */
226
    private function getProgressBar(SymfonyStyle $io, int $steps, string $message): ProgressBar
227
    {
228
        $format = '
229
 %message%
230
 %current%/%max% [%bar%] %percent:3s%%
231
 Time elapsed:   %elapsed:-6s%
232
 Time remaining: %remaining:-6s%
233
 Time estimated: %estimated:-6s%
234
 Memory usage:   %memory:-6s%
235
';
236
237
        $progress = $io->createProgressBar($steps);
238
        $progress->setFormat($format);
239
        $progress->setMessage($message);
240
241
        return $progress;
242
    }
243
244
    /**
245
     * @return array<int, string>
246
     */
247
    private function getHeaders(): array
248
    {
249
        return [
250
            'Path',
251
            'Dependency',
252
            'Description',
253
            'Version',
254
            'New version',
255
        ];
256
    }
257
258
    private function setTableColumnWidths(int $packageNameLength, Table $table): void
259
    {
260
        $widths = [
261
            23,
262
            $packageNameLength,
263
            95 - $packageNameLength,
264
            10,
265
            11,
266
        ];
267
268
        foreach ($widths as $columnIndex => $width) {
269
            $table->setColumnWidth($columnIndex, $width);
270
            $table->setColumnMaxWidth($columnIndex, $width);
271
        }
272
    }
273
}
274