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

eliashaeussler / composer-update-check / 20745988060

06 Jan 2026 10:47AM UTC coverage: 21.62% (+0.03%) from 21.587%
20745988060

Pull #212

github

eliashaeussler
[TASK] Use TaskRunner for long running processes
Pull Request #212: [TASK] Use TaskRunner for long running processes

42 of 86 new or added lines in 4 files covered. (48.84%)

411 of 1901 relevant lines covered (21.62%)

1.19 hits per line

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

97.27
/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
        private IO\IOInterface $io,
50
        private Security\SecurityScanner $securityScanner,
51
        private Reporter\ReporterFactory $reporterFactory,
52
    ) {
53
        $this->taskRunner = new TaskRunner\TaskRunner($this->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
                Console\Output\OutputInterface::VERBOSITY_VERBOSE,
3✔
78
            );
3✔
79
        }
80

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

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

90
        return $result;
8✔
91
    }
92

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

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

110
        $result = $this->taskRunner->run(
7✔
111
            '⏳ Checking for outdated packages',
7✔
112
            function (TaskRunner\RunnerContext $context) use ($packages) {
7✔
113
                $io = new IO\BufferIO();
7✔
114

115
                // Run Composer installer
116
                $result = $this->installer->runUpdate($packages, $io);
7✔
117

118
                // Handle installer failures
119
                if (!$result->isSuccessful()) {
7✔
NEW
120
                    $context->output->write($io->getOutput());
×
121

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

125
                return $result;
7✔
126
            },
7✔
127
            Console\Output\OutputInterface::VERBOSITY_VERBOSE,
7✔
128
        );
7✔
129

130
        return new Entity\Result\UpdateCheckResult(
7✔
131
            $result->getOutdatedPackages(),
7✔
132
            $excludedPackages,
7✔
133
            $this->lookupRootPackage(),
7✔
134
        );
7✔
135
    }
136

137
    /**
138
     * @throws Exception\ComposerInstallFailed
139
     */
140
    private function installDependencies(): void
8✔
141
    {
142
        // Run Composer installer
143
        $io = new IO\BufferIO();
8✔
144
        $exitCode = $this->installer->runInstall($io);
8✔
145

146
        // Handle installer failures
147
        if ($exitCode > 0) {
8✔
148
            $this->io->writeError($io->getOutput());
1✔
149

150
            throw new Exception\ComposerInstallFailed($exitCode);
1✔
151
        }
152
    }
153

154
    /**
155
     * @return array{list<Entity\Package\Package>, list<Entity\Package\ExcludedPackage>}
156
     */
157
    private function resolvePackagesForUpdateCheck(Configuration\ComposerUpdateCheckConfig $config): array
10✔
158
    {
159
        return $this->taskRunner->run(
10✔
160
            '📦 Resolving packages',
10✔
161
            function (TaskRunner\RunnerContext $context) use ($config) {
10✔
162
                $rootPackage = $this->composer->getPackage();
10✔
163
                /** @var array<non-empty-string> $requiredPackages */
164
                $requiredPackages = array_keys($rootPackage->getRequires());
10✔
165
                /** @var array<non-empty-string> $requiredDevPackages */
166
                $requiredDevPackages = array_keys($rootPackage->getDevRequires());
10✔
167
                $excludedPackages = [];
10✔
168

169
                // Handle dev-packages
170
                if ($config->areDevPackagesIncluded()) {
10✔
171
                    $requiredPackages = array_merge($requiredPackages, $requiredDevPackages);
8✔
172
                } else {
173
                    $excludedPackages = array_fill_keys($requiredDevPackages, null);
2✔
174

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

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

184
                return [
10✔
185
                    array_values($this->mapPackageNamesToPackage($requiredPackages)),
10✔
186
                    $this->mapExcludedPackages($excludedPackages),
10✔
187
                ];
10✔
188
            },
10✔
189
            Console\Output\OutputInterface::VERBOSITY_VERBOSE,
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
        Console\Output\OutputInterface $output,
203
    ): array {
204
        $excludedPackages = [];
10✔
205

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

213
                        $output->writeln(
3✔
214
                            sprintf('🚫 Skipped <info>%s</info>', $package),
3✔
215
                            Console\Output\OutputInterface::VERBOSITY_VERBOSE,
3✔
216
                        );
3✔
217

218
                        return false;
3✔
219
                    }
220
                }
221

222
                return true;
8✔
223
            },
10✔
224
        );
10✔
225

226
        return $excludedPackages;
10✔
227
    }
228

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

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

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

266
        foreach ($excludedPackages as $packageName => $excludePattern) {
10✔
267
            $excludeReason = null === $excludePattern
4✔
268
                ? Entity\Package\ExcludeReason::NoDev
2✔
269
                : Entity\Package\ExcludeReason::Pattern
3✔
270
            ;
4✔
271

272
            $packages[] = new Entity\Package\ExcludedPackage($packageName, $excludeReason, $excludePattern);
4✔
273
        }
274

275
        return $packages;
10✔
276
    }
277

278
    private function dispatchPostUpdateCheckEvent(Entity\Result\UpdateCheckResult $result): void
8✔
279
    {
280
        $event = new Event\PostUpdateCheckEvent($result);
8✔
281

282
        $this->composer->getEventDispatcher()->dispatch($event->getName(), $event);
8✔
283
    }
284

285
    private function lookupRootPackage(): ?Entity\Package\InstalledPackage
9✔
286
    {
287
        $rootPackageName = $this->composer->getPackage()->getName();
9✔
288

289
        if ('__root__' === $rootPackageName || '' === $rootPackageName) {
9✔
290
            return null;
9✔
291
        }
292

293
        return new Entity\Package\InstalledPackage($rootPackageName);
×
294
    }
295
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc