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

eliashaeussler / composer-update-check / 14274559891

04 Apr 2025 07:27PM UTC coverage: 20.538%. First build
14274559891

Pull #130

github

web-flow
[TASK] Update composer/composer to v2.8.8

| datasource | package           | from  | to    |
| ---------- | ----------------- | ----- | ----- |
| packagist  | composer/composer | 2.8.7 | 2.8.8 |
Pull Request #130: [!!!][FEATURE] Modernize plugin

356 of 1832 new or added lines in 57 files covered. (19.43%)

382 of 1860 relevant lines covered (20.54%)

1.11 hits per line

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

96.3
/src/UpdateChecker.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-2025 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;
25

26
use Composer\IO;
27

28
use function array_fill_keys;
29
use function array_keys;
30
use function array_map;
31
use function array_merge;
32
use function array_values;
33

34
/**
35
 * UpdateChecker.
36
 *
37
 * @author Elias Häußler <elias@haeussler.dev>
38
 * @license GPL-3.0-or-later
39
 */
40
final class UpdateChecker
41
{
42
    public function __construct(
12✔
43
        private readonly \Composer\Composer $composer,
44
        private readonly Composer\Installer $installer,
45
        private readonly IO\IOInterface $io,
46
        private readonly Security\SecurityScanner $securityScanner,
47
        private readonly Reporter\ReporterFactory $reporterFactory,
48
    ) {}
12✔
49

50
    /**
51
     * @throws Exception\ComposerInstallFailed
52
     * @throws Exception\ComposerUpdateFailed
53
     * @throws Exception\PackagistResponseHasErrors
54
     * @throws Exception\ReporterIsNotSupported
55
     * @throws Exception\ReporterOptionsAreInvalid
56
     * @throws Exception\UnableToFetchSecurityAdvisories
57
     */
58
    public function run(Configuration\ComposerUpdateCheckConfig $config): Entity\Result\UpdateCheckResult
12✔
59
    {
60
        $this->validateReporters($config->getReporters());
12✔
61

62
        // Run update check
63
        [$packages, $excludedPackages] = $this->resolvePackagesForUpdateCheck($config);
10✔
64
        $result = $this->runUpdateCheck($packages, $excludedPackages);
10✔
65

66
        // Overlay security scan
67
        if ($config->shouldPerformSecurityScan() && [] !== $result->getOutdatedPackages()) {
9✔
68
            try {
69
                $this->io->writeError('🚨 Looking up security advisories... ', false, IO\IOInterface::VERBOSE);
3✔
70
                $this->securityScanner->scanAndOverlayResult($result);
3✔
71
                $this->io->writeError('<info>Done</info>', true, IO\IOInterface::VERBOSE);
2✔
72
            } catch (Exception\PackagistResponseHasErrors|Exception\UnableToFetchSecurityAdvisories $exception) {
1✔
73
                $this->io->writeError('<error>Failed</error>', true, IO\IOInterface::VERBOSE);
1✔
74

75
                throw $exception;
1✔
76
            }
77
        }
78

79
        // Dispatch event
80
        $this->dispatchPostUpdateCheckEvent($result);
8✔
81

82
        // Report update check result
83
        foreach ($config->getReporters() as $name => $options) {
8✔
84
            $reporter = $this->reporterFactory->make($name);
1✔
85
            $reporter->report($result, $options);
1✔
86
        }
87

88
        return $result;
8✔
89
    }
90

91
    /**
92
     * @param list<Entity\Package\Package>         $packages
93
     * @param list<Entity\Package\ExcludedPackage> $excludedPackages
94
     *
95
     * @throws Exception\ComposerInstallFailed
96
     * @throws Exception\ComposerUpdateFailed
97
     */
98
    private function runUpdateCheck(array $packages, array $excludedPackages): Entity\Result\UpdateCheckResult
10✔
99
    {
100
        // Early return if no packages are listed for update check
101
        if ([] === $packages) {
10✔
102
            return new Entity\Result\UpdateCheckResult([], $excludedPackages, $this->lookupRootPackage());
2✔
103
        }
104

105
        // Ensure dependencies are installed
106
        $this->installDependencies();
8✔
107

108
        // Show progress
109
        $this->io->writeError('⏳ Checking for outdated packages... ', false, IO\IOInterface::VERBOSE);
7✔
110

111
        // Run Composer installer
112
        $io = new IO\BufferIO();
7✔
113
        $result = $this->installer->runUpdate($packages, $io);
7✔
114

115
        // Handle installer failures
116
        if (!$result->isSuccessful()) {
7✔
NEW
117
            $this->io->writeError('<error>Failed</error>', true, IO\IOInterface::VERBOSE);
×
NEW
118
            $this->io->writeError($io->getOutput());
×
119

NEW
120
            throw new Exception\ComposerUpdateFailed($result->getExitCode());
×
121
        }
122

123
        $this->io->writeError('<info>Done</info>', true, IO\IOInterface::VERBOSE);
7✔
124

125
        return new Entity\Result\UpdateCheckResult(
7✔
126
            $result->getOutdatedPackages(),
7✔
127
            $excludedPackages,
7✔
128
            $this->lookupRootPackage(),
7✔
129
        );
7✔
130
    }
131

132
    /**
133
     * @throws Exception\ComposerInstallFailed
134
     */
135
    private function installDependencies(): void
8✔
136
    {
137
        // Run Composer installer
138
        $io = new IO\BufferIO();
8✔
139
        $exitCode = $this->installer->runInstall($io);
8✔
140

141
        // Handle installer failures
142
        if ($exitCode > 0) {
8✔
143
            $this->io->writeError($io->getOutput());
1✔
144

145
            throw new Exception\ComposerInstallFailed($exitCode);
1✔
146
        }
147
    }
148

149
    /**
150
     * @return array{list<Entity\Package\Package>, list<Entity\Package\ExcludedPackage>}
151
     */
152
    private function resolvePackagesForUpdateCheck(Configuration\ComposerUpdateCheckConfig $config): array
10✔
153
    {
154
        $this->io->writeError('📦 Resolving packages... ', false, IO\IOInterface::VERBOSE);
10✔
155

156
        $outputWasWritten = false;
10✔
157
        $rootPackage = $this->composer->getPackage();
10✔
158
        /** @var array<non-empty-string> $requiredPackages */
159
        $requiredPackages = array_keys($rootPackage->getRequires());
10✔
160
        /** @var array<non-empty-string> $requiredDevPackages */
161
        $requiredDevPackages = array_keys($rootPackage->getDevRequires());
10✔
162
        $excludedPackages = [];
10✔
163

164
        // Handle dev-packages
165
        if ($config->areDevPackagesIncluded()) {
10✔
166
            $requiredPackages = array_merge($requiredPackages, $requiredDevPackages);
8✔
167
        } else {
168
            $excludedPackages = array_fill_keys($requiredDevPackages, null);
2✔
169

170
            $this->io->writeError(['', '🚫 Skipped dev-requirements'], true, IO\IOInterface::VERBOSE);
2✔
171

172
            if ($this->io->isVerbose()) {
2✔
173
                $outputWasWritten = true;
2✔
174
            }
175
        }
176

177
        // Remove packages by exclude patterns
178
        $excludedPackages = array_merge(
10✔
179
            $excludedPackages,
10✔
180
            $this->removeByExcludePatterns($requiredPackages, $config->getExcludePatterns(), $outputWasWritten),
10✔
181
        );
10✔
182

183
        if (!$outputWasWritten) {
10✔
184
            $this->io->writeError('<info>Done</info>', true, IO\IOInterface::VERBOSE);
6✔
185
        }
186

187
        return [
10✔
188
            array_values($this->mapPackageNamesToPackage($requiredPackages)),
10✔
189
            $this->mapExcludedPackages($excludedPackages),
10✔
190
        ];
10✔
191
    }
192

193
    /**
194
     * @param array<non-empty-string>                           $packages
195
     * @param list<Configuration\Options\PackageExcludePattern> $excludePatterns
196
     *
197
     * @return array<non-empty-string, Configuration\Options\PackageExcludePattern>
198
     */
199
    private function removeByExcludePatterns(
10✔
200
        array &$packages,
201
        array $excludePatterns,
202
        bool &$outputWasWritten = false,
203
    ): array {
204
        $excludedPackages = [];
10✔
205

206
        $packages = array_filter(
10✔
207
            $packages,
10✔
208
            function (string $package) use (&$excludedPackages, $excludePatterns, &$outputWasWritten) {
10✔
209
                foreach ($excludePatterns as $excludePattern) {
9✔
210
                    if ($excludePattern->matches($package)) {
3✔
211
                        $excludedPackages[$package] = $excludePattern;
3✔
212

213
                        if ($this->io->isVerbose()) {
3✔
214
                            if (!$outputWasWritten) {
3✔
215
                                $this->io->writeError('', true, IO\IOInterface::VERBOSE);
2✔
216
                            }
217

218
                            $outputWasWritten = true;
3✔
219
                        }
220

221
                        $this->io->writeError(sprintf('🚫 Skipped <info>%s</info>', $package), true, IO\IOInterface::VERBOSE);
3✔
222

223
                        return false;
3✔
224
                    }
225
                }
226

227
                return true;
8✔
228
            },
10✔
229
        );
10✔
230

231
        return $excludedPackages;
10✔
232
    }
233

234
    /**
235
     * @param array<string, array<string, mixed>> $reporters
236
     *
237
     * @throws Exception\ReporterIsNotSupported
238
     */
239
    private function validateReporters(array $reporters): void
12✔
240
    {
241
        foreach ($reporters as $name => $options) {
12✔
242
            // Will throw an exception if reporter is not supported
243
            $reporter = $this->reporterFactory->make($name);
3✔
244
            // Will throw an exception if reporter options are invalid
245
            $reporter->validateOptions($options);
2✔
246
        }
247
    }
248

249
    /**
250
     * @param array<non-empty-string> $packageNames
251
     *
252
     * @return array<Entity\Package\Package>
253
     */
254
    private function mapPackageNamesToPackage(array $packageNames): array
10✔
255
    {
256
        return array_map(
10✔
257
            static fn (string $packageName) => new Entity\Package\InstalledPackage($packageName),
10✔
258
            $packageNames,
10✔
259
        );
10✔
260
    }
261

262
    /**
263
     * @param array<non-empty-string, Configuration\Options\PackageExcludePattern|null> $excludedPackages
264
     *
265
     * @return list<Entity\Package\ExcludedPackage>
266
     */
267
    private function mapExcludedPackages(array $excludedPackages): array
10✔
268
    {
269
        $packages = [];
10✔
270

271
        foreach ($excludedPackages as $packageName => $excludePattern) {
10✔
272
            $excludeReason = null === $excludePattern
4✔
273
                ? Entity\Package\ExcludeReason::NoDev
2✔
274
                : Entity\Package\ExcludeReason::Pattern
3✔
275
            ;
4✔
276

277
            $packages[] = new Entity\Package\ExcludedPackage($packageName, $excludeReason, $excludePattern);
4✔
278
        }
279

280
        return $packages;
10✔
281
    }
282

283
    private function dispatchPostUpdateCheckEvent(Entity\Result\UpdateCheckResult $result): void
8✔
284
    {
285
        $event = new Event\PostUpdateCheckEvent($result);
8✔
286

287
        $this->composer->getEventDispatcher()->dispatch($event->getName(), $event);
8✔
288
    }
289

290
    private function lookupRootPackage(): ?Entity\Package\InstalledPackage
9✔
291
    {
292
        $rootPackageName = $this->composer->getPackage()->getName();
9✔
293

294
        if ('__root__' === $rootPackageName || '' === $rootPackageName) {
9✔
295
            return null;
9✔
296
        }
297

NEW
298
        return new Entity\Package\InstalledPackage($rootPackageName);
×
299
    }
300
}
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