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

eliashaeussler / composer-update-check / 25476843784

07 May 2026 02:04AM UTC coverage: 21.406%. First build
25476843784

Pull #130

github

web-flow
[TASK] Update all dependencies
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

97.12
/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-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;
25

26
use Composer\IO;
27
use EliasHaeussler\TaskRunner;
28
use Symfony\Component\Console;
29

30
use function array_fill_keys;
31
use function array_keys;
32
use function array_map;
33
use function array_merge;
34
use function array_values;
35

36
/**
37
 * UpdateChecker.
38
 *
39
 * @author Elias Häußler <elias@haeussler.dev>
40
 * @license GPL-3.0-or-later
41
 */
42
final readonly class UpdateChecker
43
{
44
    private TaskRunner\TaskRunner $taskRunner;
45

46
    public function __construct(
12✔
47
        private \Composer\Composer $composer,
48
        private Composer\Installer $installer,
49
        IO\IOInterface $io,
50
        private Security\SecurityScanner $securityScanner,
51
        private Reporter\ReporterFactory $reporterFactory,
52
    ) {
53
        $this->taskRunner = new TaskRunner\TaskRunner($io);
12✔
54
    }
55

56
    /**
57
     * @throws Exception\ComposerInstallFailed
58
     * @throws Exception\ComposerUpdateFailed
59
     * @throws Exception\PackagistResponseHasErrors
60
     * @throws Exception\ReporterIsNotSupported
61
     * @throws Exception\ReporterOptionsAreInvalid
62
     * @throws Exception\UnableToFetchSecurityAdvisories
63
     */
64
    public function run(Configuration\ComposerUpdateCheckConfig $config): Entity\Result\UpdateCheckResult
12✔
65
    {
66
        $this->validateReporters($config->getReporters());
12✔
67

68
        // Run update check
69
        [$packages, $excludedPackages] = $this->resolvePackagesForUpdateCheck($config);
10✔
70
        $result = $this->runUpdateCheck($packages, $excludedPackages);
10✔
71

72
        // Overlay security scan
73
        if ($config->shouldPerformSecurityScan() && [] !== $result->getOutdatedPackages()) {
9✔
74
            $this->taskRunner->run(
3✔
75
                '🚨 Looking up security advisories',
3✔
76
                fn () => $this->securityScanner->scanAndOverlayResult($result),
3✔
77
            );
3✔
78
        }
79

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

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

89
        return $result;
8✔
90
    }
91

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

106
        return $this->taskRunner->run(
8✔
107
            '⏳ Checking for outdated packages',
8✔
108
            function (TaskRunner\RunnerContext $context) use ($packages, $excludedPackages) {
8✔
109
                $io = new IO\BufferIO();
8✔
110

111
                // Ensure dependencies are installed
112
                $exitCode = $this->installer->runInstall($io);
8✔
113

114
                // Handle install failures
115
                if ($exitCode > 0) {
8✔
116
                    $context->output->write($io->getOutput());
1✔
117

118
                    throw new Exception\ComposerInstallFailed($exitCode);
1✔
119
                }
120

121
                // Run Composer installer
122
                $io = new IO\BufferIO();
7✔
123
                $result = $this->installer->runUpdate($packages, $io);
7✔
124

125
                // Handle update failures
126
                if (!$result->isSuccessful()) {
7✔
NEW
127
                    $context->output->write($io->getOutput());
×
128

NEW
129
                    throw new Exception\ComposerUpdateFailed($result->getExitCode());
×
130
                }
131

132
                return new Entity\Result\UpdateCheckResult(
7✔
133
                    $result->getOutdatedPackages(),
7✔
134
                    $excludedPackages,
7✔
135
                    $this->lookupRootPackage(),
7✔
136
                );
7✔
137
            },
8✔
138
        );
8✔
139
    }
140

141
    /**
142
     * @return array{list<Entity\Package\Package>, list<Entity\Package\ExcludedPackage>}
143
     */
144
    private function resolvePackagesForUpdateCheck(Configuration\ComposerUpdateCheckConfig $config): array
10✔
145
    {
146
        return $this->taskRunner->run(
10✔
147
            '📦 Resolving packages',
10✔
148
            function (TaskRunner\RunnerContext $context) use ($config) {
10✔
149
                $rootPackage = $this->composer->getPackage();
10✔
150
                /** @var array<non-empty-string> $requiredPackages */
151
                $requiredPackages = array_keys($rootPackage->getRequires());
10✔
152
                /** @var array<non-empty-string> $requiredDevPackages */
153
                $requiredDevPackages = array_keys($rootPackage->getDevRequires());
10✔
154
                $excludedPackages = [];
10✔
155

156
                // Handle dev-packages
157
                if ($config->areDevPackagesIncluded()) {
10✔
158
                    $requiredPackages = array_merge($requiredPackages, $requiredDevPackages);
8✔
159
                } else {
160
                    $excludedPackages = array_fill_keys($requiredDevPackages, null);
2✔
161

162
                    $context->output->writeln('🚫 Skipped dev-requirements', Console\Output\OutputInterface::VERBOSITY_VERBOSE);
2✔
163
                }
164

165
                // Remove packages by exclude patterns
166
                $excludedPackages = array_merge(
10✔
167
                    $excludedPackages,
10✔
168
                    $this->removeByExcludePatterns($requiredPackages, $config->getExcludePatterns(), $context->output),
10✔
169
                );
10✔
170

171
                return [
10✔
172
                    array_values($this->mapPackageNamesToPackage($requiredPackages)),
10✔
173
                    $this->mapExcludedPackages($excludedPackages),
10✔
174
                ];
10✔
175
            },
10✔
176
        );
10✔
177
    }
178

179
    /**
180
     * @param array<non-empty-string>                           $packages
181
     * @param list<Configuration\Options\PackageExcludePattern> $excludePatterns
182
     *
183
     * @return array<non-empty-string, Configuration\Options\PackageExcludePattern>
184
     */
185
    private function removeByExcludePatterns(
10✔
186
        array &$packages,
187
        array $excludePatterns,
188
        Console\Output\OutputInterface $output,
189
    ): array {
190
        $excludedPackages = [];
10✔
191

192
        $packages = array_filter(
10✔
193
            $packages,
10✔
194
            static function (string $package) use (&$excludedPackages, $excludePatterns, $output) {
10✔
195
                foreach ($excludePatterns as $excludePattern) {
9✔
196
                    if ($excludePattern->matches($package)) {
3✔
197
                        $excludedPackages[$package] = $excludePattern;
3✔
198

199
                        $output->writeln(
3✔
200
                            sprintf('🚫 Skipped <info>%s</info>', $package),
3✔
201
                            Console\Output\OutputInterface::VERBOSITY_VERBOSE,
3✔
202
                        );
3✔
203

204
                        return false;
3✔
205
                    }
206
                }
207

208
                return true;
8✔
209
            },
10✔
210
        );
10✔
211

212
        return $excludedPackages;
10✔
213
    }
214

215
    /**
216
     * @param array<string, array<string, mixed>> $reporters
217
     *
218
     * @throws Exception\ReporterIsNotSupported
219
     */
220
    private function validateReporters(array $reporters): void
12✔
221
    {
222
        foreach ($reporters as $name => $options) {
12✔
223
            // Will throw an exception if reporter is not supported
224
            $reporter = $this->reporterFactory->make($name);
3✔
225
            // Will throw an exception if reporter options are invalid
226
            $reporter->validateOptions($options);
2✔
227
        }
228
    }
229

230
    /**
231
     * @param array<non-empty-string> $packageNames
232
     *
233
     * @return array<Entity\Package\Package>
234
     */
235
    private function mapPackageNamesToPackage(array $packageNames): array
10✔
236
    {
237
        return array_map(
10✔
238
            static fn (string $packageName) => new Entity\Package\InstalledPackage($packageName),
10✔
239
            $packageNames,
10✔
240
        );
10✔
241
    }
242

243
    /**
244
     * @param array<non-empty-string, Configuration\Options\PackageExcludePattern|null> $excludedPackages
245
     *
246
     * @return list<Entity\Package\ExcludedPackage>
247
     */
248
    private function mapExcludedPackages(array $excludedPackages): array
10✔
249
    {
250
        $packages = [];
10✔
251

252
        foreach ($excludedPackages as $packageName => $excludePattern) {
10✔
253
            $excludeReason = null === $excludePattern
4✔
254
                ? Entity\Package\ExcludeReason::NoDev
2✔
255
                : Entity\Package\ExcludeReason::Pattern
3✔
256
            ;
4✔
257

258
            $packages[] = new Entity\Package\ExcludedPackage($packageName, $excludeReason, $excludePattern);
4✔
259
        }
260

261
        return $packages;
10✔
262
    }
263

264
    private function dispatchPostUpdateCheckEvent(Entity\Result\UpdateCheckResult $result): void
8✔
265
    {
266
        $event = new Event\PostUpdateCheckEvent($result);
8✔
267

268
        $this->composer->getEventDispatcher()->dispatch($event->getName(), $event);
8✔
269
    }
270

271
    private function lookupRootPackage(): ?Entity\Package\InstalledPackage
9✔
272
    {
273
        $rootPackageName = $this->composer->getPackage()->getName();
9✔
274

275
        if ('__root__' === $rootPackageName || '' === $rootPackageName) {
9✔
276
            return null;
9✔
277
        }
278

NEW
279
        return new Entity\Package\InstalledPackage($rootPackageName);
×
280
    }
281
}
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