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

eliashaeussler / composer-update-check / 20742334666

06 Jan 2026 08:15AM UTC coverage: 21.587%. Remained the same
20742334666

Pull #211

github

eliashaeussler
[TASK] Migrate codebase to PHP 8.2
Pull Request #211: [TASK] Migrate codebase to PHP 8.2

408 of 1890 relevant lines covered (21.59%)

1.17 hits per line

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

0.0
/src/Entity/Report/TeamsReport.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\Entity\Report;
25

26
use EliasHaeussler\ComposerUpdateCheck\Entity;
27
use JsonSerializable;
28

29
use function sprintf;
30

31
/**
32
 * TeamsReport.
33
 *
34
 * @author Elias Häußler <e.haeussler@familie-redlich.de>
35
 * @license GPL-3.0-or-later
36
 */
37
final readonly class TeamsReport implements JsonSerializable
38
{
39
    private const SECURITY_ADVISORIES_CONTAINER_ID = 'securityAdvisories';
40

41
    /**
42
     * @param list<Dto\TeamsAttachment> $attachments
43
     */
44
    private function __construct(
×
45
        public string $type,
46
        public array $attachments,
47
    ) {}
×
48

49
    public static function create(Entity\Result\UpdateCheckResult $result, string $additionalData = ''): self
×
50
    {
51
        return new self(
×
52
            'message',
×
53
            self::createAttachments($result, $additionalData),
×
54
        );
×
55
    }
56

57
    /**
58
     * @return list<Dto\TeamsAttachment>
59
     */
60
    private static function createAttachments(Entity\Result\UpdateCheckResult $result, string $additionalData = ''): array
×
61
    {
62
        $attachment = Dto\TeamsAttachment::adaptiveCard(
×
63
            self::createCardBody($result),
×
64
            self::createFallbackText($result),
×
65
            self::createCardActions($result),
×
66
            $additionalData,
×
67
        );
×
68

69
        return [$attachment];
×
70
    }
71

72
    /**
73
     * @return list<Dto\TeamsContent>
74
     */
75
    private static function createCardBody(Entity\Result\UpdateCheckResult $result): array
×
76
    {
77
        $rootPackageName = $result->getRootPackage()?->getName();
×
78
        $numberOfOutdatedPackages = 0;
×
79
        $numberOfInsecurePackages = 0;
×
80
        $highestSeverityLevels = [];
×
81

82
        // Count outdated and insecure packages
83
        foreach ($result->getOutdatedPackages() as $outdatedPackage) {
×
84
            ++$numberOfOutdatedPackages;
×
85

86
            if ($outdatedPackage->isInsecure()) {
×
87
                ++$numberOfInsecurePackages;
×
88
                $highestSeverityLevels[] = $outdatedPackage->getHighestSeverityLevel();
×
89
            }
90
        }
91

92
        // Add title
93
        $contents = [
×
94
            Dto\TeamsContent::textBlock(
×
95
                text: sprintf(
×
96
                    '%d package%s%s %s outdated',
×
97
                    $numberOfOutdatedPackages,
×
98
                    1 !== $numberOfOutdatedPackages ? 's' : '',
×
99
                    $numberOfInsecurePackages > 0 ? sprintf(' (%d insecure)', $numberOfInsecurePackages) : '',
×
100
                    1 !== $numberOfOutdatedPackages ? 'are' : 'is',
×
101
                ),
×
102
                wrap: true,
×
103
                size: Enum\TeamsFontSize::Large,
×
104
                weight: Enum\TeamsFontWeight::Bolder,
×
105
            ),
×
106
        ];
×
107

108
        // Add summary
109
        if (null !== $rootPackageName) {
×
110
            $contents[] = Dto\TeamsContent::textBlock(
×
111
                text: sprintf(
×
112
                    'Project **%s** has **%d outdated package%s**.',
×
113
                    $rootPackageName,
×
114
                    $numberOfOutdatedPackages,
×
115
                    1 !== $numberOfOutdatedPackages ? 's' : '',
×
116
                ),
×
117
                wrap: true,
×
118
            );
×
119

120
            if ([] !== $highestSeverityLevels) {
×
121
                $highestSeverityLevel = Entity\Security\SeverityLevel::getHighestSeverityLevel(...$highestSeverityLevels);
×
122
                $contents[] = Dto\TeamsContent::textBlock(
×
123
                    text: sprintf(
×
124
                        '%s marked as **insecure** with a highest severity of **%s %s**.',
×
125
                        $numberOfInsecurePackages === $numberOfOutdatedPackages
×
126
                            ? ($numberOfInsecurePackages > 1 ? 'They are' : 'It is')
×
127
                            : sprintf(
×
128
                                '%d of them %s',
×
129
                                $numberOfInsecurePackages,
×
130
                                $numberOfInsecurePackages > 1 ? 'are' : 'is',
×
131
                            ),
×
132
                        self::getEmojiForSeverityLevel($highestSeverityLevel),
×
133
                        $highestSeverityLevel->value,
×
134
                    ),
×
135
                    wrap: true,
×
136
                    spacing: Enum\TeamsSpacing::None,
×
137
                );
×
138
            }
139
        }
140

141
        // Add table with outdated packages
142
        $contents[] = self::createTableWithOutdatedPackages($result);
×
143

144
        // Add container with security advisories
145
        if ($numberOfInsecurePackages > 0) {
×
146
            $contents[] = self::createSecurityAdvisoriesContainer($result);
×
147
        }
148

149
        return $contents;
×
150
    }
151

152
    private static function createTableWithOutdatedPackages(Entity\Result\UpdateCheckResult $result): Dto\TeamsContent
×
153
    {
154
        $hasInsecureOutdatedPackages = $result->hasInsecureOutdatedPackages();
×
155
        $columns = [];
×
156
        $rowCells = [];
×
157
        $rowHeaders = [
×
158
            'Package' => 2,
×
159
            'Current version' => 1,
×
160
            'New version' => 1,
×
161
        ];
×
162

163
        if ($hasInsecureOutdatedPackages) {
×
164
            $rowHeaders['Security advisory'] = 1;
×
165
        }
166

167
        foreach ($rowHeaders as $rowHeader => $columnWidth) {
×
168
            $columns[] = new Dto\TeamsTableColumn($columnWidth);
×
169
            $rowCells[] = new Dto\TeamsTableCell(
×
170
                [
×
171
                    Dto\TeamsContent::textBlock(
×
172
                        text: $rowHeader,
×
173
                        wrap: true,
×
174
                    ),
×
175
                ],
×
176
            );
×
177
        }
178

179
        $rows = [
×
180
            new Dto\TeamsTableRow(
×
181
                cells: $rowCells,
×
182
                spacing: 'None',
×
183
                horizontalCellContentAlignment: 'Left',
×
184
                verticalCellContentAlignment: 'Top',
×
185
            ),
×
186
        ];
×
187

188
        // Add table rows
189
        foreach ($result->getOutdatedPackages() as $outdatedPackage) {
×
190
            $cells = [
×
191
                new Dto\TeamsTableCell(
×
192
                    [
×
193
                        Dto\TeamsContent::textBlock(
×
194
                            text: sprintf('[%s](%s)', $outdatedPackage->getName(), $outdatedPackage->getProviderLink()),
×
195
                            wrap: true,
×
196
                        ),
×
197
                    ],
×
198
                ),
×
199
                new Dto\TeamsTableCell(
×
200
                    [
×
201
                        Dto\TeamsContent::textBlock(
×
202
                            text: $outdatedPackage->getOutdatedVersion()->toString(),
×
203
                            wrap: true,
×
204
                        ),
×
205
                    ],
×
206
                ),
×
207
                new Dto\TeamsTableCell(
×
208
                    [
×
209
                        Dto\TeamsContent::textBlock(
×
210
                            text: $outdatedPackage->getNewVersion()->toString(),
×
211
                            wrap: true,
×
212
                            weight: Enum\TeamsFontWeight::Bolder,
×
213
                        ),
×
214
                    ],
×
215
                ),
×
216
            ];
×
217

218
            if ($outdatedPackage->isInsecure()) {
×
219
                $severityLevel = $outdatedPackage->getHighestSeverityLevel();
×
220
                $cells[] = new Dto\TeamsTableCell(
×
221
                    [
×
222
                        Dto\TeamsContent::textBlock(
×
223
                            text: sprintf(
×
224
                                '%s %s',
×
225
                                self::getEmojiForSeverityLevel($severityLevel),
×
226
                                $severityLevel->value,
×
227
                            ),
×
228
                            wrap: true,
×
229
                        ),
×
230
                    ],
×
231
                );
×
232
            } elseif ($hasInsecureOutdatedPackages) {
×
233
                $cells[] = new Dto\TeamsTableCell(
×
234
                    [
×
235
                        Dto\TeamsContent::textBlock(''),
×
236
                    ],
×
237
                );
×
238
            }
239

240
            $rows[] = new Dto\TeamsTableRow($cells);
×
241
        }
242

243
        return Dto\TeamsContent::table(
×
244
            columns: $columns,
×
245
            rows: $rows,
×
246
            firstRowAsHeader: true,
×
247
            gridStyle: 'default',
×
248
        );
×
249
    }
250

251
    private static function createSecurityAdvisoriesContainer(Entity\Result\UpdateCheckResult $result): Dto\TeamsContent
×
252
    {
253
        $items = [
×
254
            Dto\TeamsContent::textBlock(
×
255
                text: 'Security advisories',
×
256
                wrap: true,
×
257
                size: Enum\TeamsFontSize::Large,
×
258
                weight: Enum\TeamsFontWeight::Bolder,
×
259
            ),
×
260
        ];
×
261

262
        foreach ($result->getInsecureOutdatedPackages() as $insecurePackage) {
×
263
            foreach ($insecurePackage->getSecurityAdvisories() as $securityAdvisory) {
×
264
                $facts = [
×
265
                    new Dto\TeamsFact('Package', $securityAdvisory->getPackageName()),
×
266
                    new Dto\TeamsFact('Reported at', $securityAdvisory->getReportedAt()->format('Y-m-d')),
×
267
                ];
×
268

269
                if (null !== $securityAdvisory->getCVE()) {
×
270
                    $facts[] = new Dto\TeamsFact('CVE', $securityAdvisory->getCVE());
×
271
                }
272

273
                $containerItems = [
×
274
                    Dto\TeamsContent::textBlock(
×
275
                        text: sprintf(
×
276
                            '%s %s',
×
277
                            self::getEmojiForSeverityLevel($securityAdvisory->getSeverity()),
×
278
                            $securityAdvisory->getSanitizedTitle(),
×
279
                        ),
×
280
                        wrap: true,
×
281
                        weight: Enum\TeamsFontWeight::Bolder,
×
282
                    ),
×
283
                    Dto\TeamsContent::factSet(
×
284
                        facts: $facts,
×
285
                        spacing: Enum\TeamsSpacing::Small,
×
286
                    ),
×
287
                ];
×
288

289
                if (null !== $securityAdvisory->getLink()) {
×
290
                    $containerItems[] = Dto\TeamsContent::textBlock(
×
291
                        text: sprintf('[Read more](%s)', $securityAdvisory->getLink()),
×
292
                        wrap: true,
×
293
                    );
×
294
                }
295

296
                $items[] = Dto\TeamsContent::container($containerItems);
×
297
            }
298
        }
299

300
        return Dto\TeamsContent::container(
×
301
            items: $items,
×
302
            isVisible: false,
×
303
            id: self::SECURITY_ADVISORIES_CONTAINER_ID,
×
304
            spacing: Enum\TeamsSpacing::Medium,
×
305
        );
×
306
    }
307

308
    private static function createFallbackText(Entity\Result\UpdateCheckResult $result): string
×
309
    {
310
        $rootPackageName = $result->getRootPackage()?->getName();
×
311
        $numberOfOutdatedPackages = 0;
×
312
        $numberOfInsecurePackages = 0;
×
313
        $highestSeverityLevels = [];
×
314
        $addition = '';
×
315

316
        // Count outdated and insecure packages
317
        foreach ($result->getOutdatedPackages() as $outdatedPackage) {
×
318
            ++$numberOfOutdatedPackages;
×
319

320
            if ($outdatedPackage->isInsecure()) {
×
321
                ++$numberOfInsecurePackages;
×
322
                $highestSeverityLevels[] = $outdatedPackage->getHighestSeverityLevel();
×
323
            }
324
        }
325

326
        if ([] !== $highestSeverityLevels) {
×
327
            $highestSeverityLevel = Entity\Security\SeverityLevel::getHighestSeverityLevel(...$highestSeverityLevels);
×
328
            $addition = sprintf(
×
329
                ' %s marked as insecure with a highest severity of "%s".',
×
330
                $numberOfInsecurePackages === $numberOfOutdatedPackages
×
331
                    ? ($numberOfInsecurePackages > 1 ? 'They are' : 'It is')
×
332
                    : sprintf(
×
333
                        '%d of them %s',
×
334
                        $numberOfInsecurePackages,
×
335
                        $numberOfInsecurePackages > 1 ? 'are' : 'is',
×
336
                    ),
×
337
                $highestSeverityLevel->value,
×
338
            );
×
339
        }
340

341
        if (null !== $rootPackageName) {
×
342
            return sprintf(
×
343
                'Project "%s" has %d outdated package%s.%s',
×
344
                $rootPackageName,
×
345
                $numberOfOutdatedPackages,
×
346
                1 !== $numberOfOutdatedPackages ? 's' : '',
×
347
                $addition,
×
348
            );
×
349
        }
350

351
        return sprintf(
×
352
            '%d package%s are currently outdated.%s',
×
353
            $numberOfOutdatedPackages,
×
354
            1 !== $numberOfOutdatedPackages ? 's' : '',
×
355
            $addition,
×
356
        );
×
357
    }
358

359
    /**
360
     * @return list<Dto\TeamsAction>
361
     */
362
    private static function createCardActions(Entity\Result\UpdateCheckResult $result): array
×
363
    {
364
        if (!$result->hasInsecureOutdatedPackages()) {
×
365
            return [];
×
366
        }
367

368
        $actionId = 'showSecurityAdvisories';
×
369

370
        return [
×
371
            Dto\TeamsAction::toggleVisibility(
×
372
                $actionId,
×
373
                'Show security advisories',
×
374
                [
×
375
                    self::SECURITY_ADVISORIES_CONTAINER_ID,
×
376
                    $actionId,
×
377
                ],
×
378
            ),
×
379
        ];
×
380
    }
381

382
    private static function getEmojiForSeverityLevel(Entity\Security\SeverityLevel $severityLevel): string
×
383
    {
384
        return match ($severityLevel) {
×
385
            Entity\Security\SeverityLevel::Low => '⚪',
×
386
            Entity\Security\SeverityLevel::Medium => '🟡',
×
387
            Entity\Security\SeverityLevel::High => '🟠',
×
388
            Entity\Security\SeverityLevel::Critical => '🔴',
×
389
        };
×
390
    }
391

392
    /**
393
     * @return array{
394
     *     type: string,
395
     *     attachments: list<Dto\TeamsAttachment>,
396
     * }
397
     */
398
    public function jsonSerialize(): array
×
399
    {
400
        return [
×
401
            'type' => $this->type,
×
402
            'attachments' => $this->attachments,
×
403
        ];
×
404
    }
405
}
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