• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

eliashaeussler / composer-update-check / 22548892422

01 Mar 2026 05:42PM UTC coverage: 21.406%. First build
22548892422

Pull #130

github

web-flow
Merge pull request #219 from eliashaeussler/renovate/lock-file-maintenance
Pull Request #130: [!!!][FEATURE] Modernize plugin

385 of 1870 new or added lines in 57 files covered. (20.59%)

405 of 1892 relevant lines covered (21.41%)

1.18 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/IO/Formatter/JsonFormatter.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the Composer package "eliashaeussler/composer-update-check".
7
 *
8
 * Copyright (C) 2020-2026 Elias Häußler <elias@haeussler.dev>
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, either version 3 of the License, or
13
 * (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
 * GNU General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU General Public License
21
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22
 */
23

24
namespace EliasHaeussler\ComposerUpdateCheck\IO\Formatter;
25

26
use EliasHaeussler\ComposerUpdateCheck\Entity;
27
use Symfony\Component\Console;
28

29
use function count;
30
use function json_encode;
31
use function sprintf;
32

33
/**
34
 * JsonFormatter.
35
 *
36
 * @author Elias Häußler <elias@haeussler.dev>
37
 * @license GPL-3.0-or-later
38
 */
39
final class JsonFormatter implements Formatter
40
{
41
    public const FORMAT = 'json';
42

NEW
43
    public function __construct(
×
44
        private ?Console\Style\SymfonyStyle $io = null,
NEW
45
    ) {}
×
46

NEW
47
    public function formatResult(Entity\Result\UpdateCheckResult $result): void
×
48
    {
49
        // Early return if IO is missing
NEW
50
        if (null === $this->io) {
×
NEW
51
            return;
×
52
        }
53

NEW
54
        $outdatedPackages = $result->getOutdatedPackages();
×
NEW
55
        $excludedPackages = $result->getExcludedPackages();
×
56

57
        // Print message if no packages are outdated
NEW
58
        if ([] === $outdatedPackages) {
×
NEW
59
            $this->renderUpToDateMessage($excludedPackages);
×
60

NEW
61
            return;
×
62
        }
63

NEW
64
        $json = $this->renderPackages($outdatedPackages, $excludedPackages);
×
NEW
65
        $securityAdvisories = $this->renderSecurityAdvisories($outdatedPackages);
×
66

NEW
67
        if ([] !== $securityAdvisories) {
×
NEW
68
            $json['securityAdvisories'] = $securityAdvisories;
×
69
        }
70

NEW
71
        $this->renderJson($json);
×
72
    }
73

74
    /**
75
     * @param list<Entity\Package\Package> $excludedPackages
76
     */
NEW
77
    private function renderUpToDateMessage(array $excludedPackages): void
×
78
    {
NEW
79
        $numberOfExcludedPackages = count($excludedPackages);
×
80

NEW
81
        if ($numberOfExcludedPackages > 0) {
×
NEW
82
            $additionalInformation = sprintf(
×
NEW
83
                ' (skipped %d package%s)',
×
NEW
84
                $numberOfExcludedPackages,
×
NEW
85
                1 !== $numberOfExcludedPackages ? 's' : '',
×
NEW
86
            );
×
87
        } else {
NEW
88
            $additionalInformation = '';
×
89
        }
90

NEW
91
        $json = [
×
NEW
92
            'status' => sprintf('All packages are up to date%s.', $additionalInformation),
×
NEW
93
        ];
×
94

NEW
95
        if ([] !== $excludedPackages) {
×
NEW
96
            $json['excludedPackages'] = $excludedPackages;
×
97
        }
98

NEW
99
        $this->renderJson($json);
×
100
    }
101

102
    /**
103
     * @param list<Entity\Package\OutdatedPackage> $outdatedPackages
104
     * @param list<Entity\Package\Package>         $excludedPackages
105
     *
106
     * @return array{
107
     *     status: string,
108
     *     outdatedPackages: list<array{
109
     *         name: non-empty-string,
110
     *         outdatedVersion: string,
111
     *         newVersion: string,
112
     *         insecure?: bool,
113
     *     }>,
114
     *     excludedPackages?: list<Entity\Package\Package>,
115
     * }
116
     */
NEW
117
    private function renderPackages(array $outdatedPackages, array $excludedPackages): array
×
118
    {
NEW
119
        $numberOfOutdatedPackages = count($outdatedPackages);
×
NEW
120
        $numberOfExcludedPackages = count($excludedPackages);
×
NEW
121
        $hasInsecurePackages = false;
×
122

NEW
123
        if ($numberOfExcludedPackages > 0) {
×
NEW
124
            $additionalInformation = sprintf(
×
NEW
125
                ' (skipped %d package%s)',
×
NEW
126
                $numberOfExcludedPackages,
×
NEW
127
                1 !== $numberOfExcludedPackages ? 's' : '',
×
NEW
128
            );
×
129
        } else {
NEW
130
            $additionalInformation = '';
×
131
        }
132

133
        // Create status label
NEW
134
        if (1 === $numberOfOutdatedPackages) {
×
NEW
135
            $statusLabel = sprintf('1 package is outdated%s.', $additionalInformation);
×
136
        } else {
NEW
137
            $statusLabel = sprintf('%d packages are outdated%s.', $numberOfOutdatedPackages, $additionalInformation);
×
138
        }
139

140
        // Print table
NEW
141
        $rows = $this->parsePackages($outdatedPackages, $hasInsecurePackages);
×
NEW
142
        $result = [];
×
143

NEW
144
        foreach ($rows as $row) {
×
145
            // Assure even secure packages have "insecure" value set
NEW
146
            if ($hasInsecurePackages) {
×
NEW
147
                $row['insecure'] ??= false;
×
148
            }
149

NEW
150
            $result[] = $row;
×
151
        }
152

NEW
153
        $json = [
×
NEW
154
            'status' => $statusLabel,
×
NEW
155
            'outdatedPackages' => $result,
×
NEW
156
        ];
×
157

NEW
158
        if ([] !== $excludedPackages) {
×
NEW
159
            $json['excludedPackages'] = $excludedPackages;
×
160
        }
161

NEW
162
        return $json;
×
163
    }
164

165
    /**
166
     * @param list<Entity\Package\OutdatedPackage> $outdatedPackages
167
     *
168
     * @return list<array{name: non-empty-string, outdatedVersion: string, newVersion: string, insecure?: bool}>
169
     */
NEW
170
    private function parsePackages(array $outdatedPackages, bool &$hasInsecurePackages = false): array
×
171
    {
NEW
172
        $tableRows = [];
×
173

NEW
174
        foreach ($outdatedPackages as $outdatedPackage) {
×
NEW
175
            $report = [
×
NEW
176
                'name' => $outdatedPackage->getName(),
×
NEW
177
                'outdatedVersion' => $outdatedPackage->getOutdatedVersion()->toString(),
×
NEW
178
                'newVersion' => $outdatedPackage->getNewVersion()->toString(),
×
NEW
179
            ];
×
180

NEW
181
            if ($outdatedPackage->isInsecure()) {
×
NEW
182
                $report['insecure'] = $outdatedPackage->isInsecure();
×
NEW
183
                $hasInsecurePackages = true;
×
184
            }
185

NEW
186
            $tableRows[] = $report;
×
187
        }
188

NEW
189
        return $tableRows;
×
190
    }
191

192
    /**
193
     * @param list<Entity\Package\OutdatedPackage> $outdatedPackages
194
     *
195
     * @return array<string, list<array{
196
     *     title: string,
197
     *     advisoryId: string,
198
     *     reportedAt: string,
199
     *     severity: string,
200
     *     cve: string|null,
201
     *     link?: string,
202
     * }>>
203
     */
NEW
204
    private function renderSecurityAdvisories(array $outdatedPackages): array
×
205
    {
NEW
206
        if (true !== $this->io?->isVerbose()) {
×
NEW
207
            return [];
×
208
        }
209

NEW
210
        $securityAdvisories = [];
×
211

NEW
212
        foreach ($outdatedPackages as $outdatedPackage) {
×
NEW
213
            if (!$outdatedPackage->isInsecure()) {
×
NEW
214
                continue;
×
215
            }
216

NEW
217
            $securityAdvisories[$outdatedPackage->getName()] = [];
×
218

NEW
219
            foreach ($outdatedPackage->getSecurityAdvisories() as $securityAdvisory) {
×
NEW
220
                $link = $securityAdvisory->getLink();
×
221

NEW
222
                $json = [
×
NEW
223
                    'title' => $securityAdvisory->getTitle(),
×
NEW
224
                    'advisoryId' => $securityAdvisory->getAdvisoryId(),
×
NEW
225
                    'reportedAt' => $securityAdvisory->getReportedAt()->format('Y-m-d H:i:s'),
×
NEW
226
                    'severity' => $securityAdvisory->getSeverity()->value,
×
NEW
227
                    'cve' => $securityAdvisory->getCVE(),
×
NEW
228
                ];
×
229

NEW
230
                if (null !== $link) {
×
NEW
231
                    $json['link'] = (string) $link;
×
232
                }
233

NEW
234
                $securityAdvisories[$outdatedPackage->getName()][] = $json;
×
235
            }
236
        }
237

NEW
238
        return $securityAdvisories;
×
239
    }
240

241
    /**
242
     * @param array<string, mixed> $json
243
     */
NEW
244
    private function renderJson(array $json): void
×
245
    {
246
        // Early return if output is quiet
NEW
247
        if (false !== $this->io?->isQuiet()) {
×
NEW
248
            return;
×
249
        }
250

NEW
251
        $flags = JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR;
×
252

253
        // Pretty-print JSON on verbose output
NEW
254
        if ($this->io->isVerbose()) {
×
NEW
255
            $flags |= JSON_PRETTY_PRINT;
×
256
        }
257

NEW
258
        $this->io->writeln(json_encode($json, $flags));
×
259
    }
260

NEW
261
    public function setIO(Console\Style\SymfonyStyle $io): void
×
262
    {
NEW
263
        $this->io = $io;
×
264
    }
265

NEW
266
    public static function getFormat(): string
×
267
    {
NEW
268
        return self::FORMAT;
×
269
    }
270
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc