Completed
Push — master ( ad6910...d77fda )
by Tarmo
18s queued 12s
created

CheckDependencies::determineTableRows()   A

Complexity

Conditions 5
Paths 1

Size

Total Lines 43
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 18
c 0
b 0
f 0
nc 1
nop 2
dl 0
loc 43
rs 9.3554
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_map;
30
use function array_unshift;
31
use function count;
32
use function dirname;
33
use function implode;
34
use function is_array;
35
use function iterator_to_array;
36
use function sort;
37
use function sprintf;
38
use function str_replace;
39
40
/**
41
 * Class CheckDependencies
42
 *
43
 * @package App\Command\Utils
44
 * @author TLe, Tarmo Leppänen <[email protected]>
45
 */
46
class CheckDependencies extends Command
47
{
48
    use SymfonyStyleTrait;
49
50
    public function __construct(
51
        private string $projectDir,
52
    ) {
53
        parent::__construct('check-dependencies');
54
55
        $this->setDescription('Console command to check which vendor dependencies has updates');
56
    }
57
58
    /**
59
     * @noinspection PhpMissingParentCallCommonInspection
60
     *
61
     * @throws Throwable
62
     */
63
    protected function execute(InputInterface $input, OutputInterface $output): int
64
    {
65
        $io = $this->getSymfonyStyle($input, $output);
66
67
        $directories = $this->getNamespaceDirectories();
68
69
        array_unshift($directories, $this->projectDir);
70
71
        $rows = $this->determineTableRows($io, $directories);
72
73
        $headers = [
74
            'Path',
75
            'Dependency',
76
            'Description',
77
            'Version',
78
            'New version',
79
        ];
80
81
        $style = clone Table::getStyleDefinition('box');
82
        $style->setCellHeaderFormat('<info>%s</info>');
83
84
        $table = new Table($output);
85
        $table->setHeaders($headers);
86
        $table->setRows($rows);
87
        $table->setStyle($style);
88
        $table->setColumnMaxWidth(2, 80);
89
        $table->setColumnMaxWidth(3, 10);
90
        $table->setColumnMaxWidth(4, 11);
91
92
        count($rows)
93
            ? $table->render()
94
            : $io->success('Good news, there is not any vendor dependency to update at this time!');
95
96
        return 0;
97
    }
98
99
    /**
100
     * Method to determine all namespace directories under 'tools' directory.
101
     *
102
     * @return array<int, string>
103
     *
104
     * @throws LogicException
105
     * @throws InvalidArgumentException
106
     */
107
    private function getNamespaceDirectories(): array
108
    {
109
        // Find all main namespace directories under 'tools' directory
110
        $finder = (new Finder())
111
            ->depth(1)
112
            ->ignoreDotFiles(true)
113
            ->directories()
114
            ->in($this->projectDir . DIRECTORY_SEPARATOR . 'tools/');
115
116
        $closure = static fn (SplFileInfo $fileInfo): string => $fileInfo->getPath();
117
118
        /** @var Traversable<SplFileInfo> $iterator */
119
        $iterator = $finder->getIterator();
120
121
        // Determine namespace directories
122
        $directories = array_map($closure, iterator_to_array($iterator));
123
124
        sort($directories);
125
126
        return $directories;
127
    }
128
129
    /**
130
     * Method to determine table rows.
131
     *
132
     * @param array<int, string> $directories
133
     *
134
     * @return array<int, array<int, string>|TableSeparator>
135
     *
136
     * @throws JsonException
137
     */
138
    private function determineTableRows(SymfonyStyle $io, array $directories): array
139
    {
140
        // Initialize progress bar for process
141
        $progressBar = $this->getProgressBar($io, count($directories), 'Checking all vendor dependencies');
142
143
        // Initialize output rows
144
        $rows = [];
145
146
        /**
147
         * Closure to process each vendor directory and check if there is libraries to be updated.
148
         *
149
         * @param string $directory
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 (!empty($rows)) {
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);
0 ignored issues
show
Bug introduced by
App\Command\Utils\JSON_THROW_ON_ERROR of type integer is incompatible with the type boolean|null expected by parameter $associative of json_decode(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

215
        $decoded = json_decode($process->getOutput(), /** @scrutinizer ignore-type */ flags: JSON_THROW_ON_ERROR);
Loading history...
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