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

eliashaeussler / composer-update-check / 10285821446

07 Aug 2024 02:02PM UTC coverage: 25.504%. First build
10285821446

Pull #130

github

web-flow
Merge ed00778d6 into 7d4f7bd74
Pull Request #130: [!!!][FEATURE] Modernize plugin

328 of 1362 new or added lines in 47 files covered. (24.08%)

354 of 1388 relevant lines covered (25.5%)

1.34 hits per line

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

96.15
/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-2024 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_keys;
29
use function array_map;
30
use function array_merge;
31

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

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

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

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

73
                throw $exception;
1✔
74
            }
75
        }
76

77
        // Dispatch event
78
        $this->dispatchPostUpdateCheckEvent($result);
8✔
79

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

86
        return $result;
8✔
87
    }
88

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

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

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

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

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

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

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

123
        return new Entity\Result\UpdateCheckResult($result->getOutdatedPackages(), $excludedPackages);
7✔
124
    }
125

126
    /**
127
     * @throws Exception\ComposerInstallFailed
128
     */
129
    private function installDependencies(): void
8✔
130
    {
131
        // Run Composer installer
132
        $io = new IO\BufferIO();
8✔
133
        $exitCode = $this->installer->runInstall($io);
8✔
134

135
        // Handle installer failures
136
        if ($exitCode > 0) {
8✔
137
            $this->io->writeError($io->getOutput());
1✔
138

139
            throw new Exception\ComposerInstallFailed($exitCode);
1✔
140
        }
141
    }
142

143
    /**
144
     * @return array{list<Entity\Package\Package>, list<Entity\Package\Package>}
145
     */
146
    private function resolvePackagesForUpdateCheck(Configuration\ComposerUpdateCheckConfig $config): array
10✔
147
    {
148
        $this->io->writeError('📦 Resolving packages...', true, IO\IOInterface::VERBOSE);
10✔
149

150
        $rootPackage = $this->composer->getPackage();
10✔
151
        /** @var array<non-empty-string> $requiredPackages */
152
        $requiredPackages = array_keys($rootPackage->getRequires());
10✔
153
        /** @var array<non-empty-string> $requiredDevPackages */
154
        $requiredDevPackages = array_keys($rootPackage->getDevRequires());
10✔
155
        $excludedPackages = [];
10✔
156

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

163
            $this->io->writeError('🚫 Skipped dev-requirements', true, IO\IOInterface::VERBOSE);
2✔
164
        }
165

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

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

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

188
        $packages = array_filter($packages, function (string $package) use (&$excludedPackages, $excludePatterns) {
10✔
189
            foreach ($excludePatterns as $excludePattern) {
9✔
190
                if ($excludePattern->matches($package)) {
3✔
191
                    $excludedPackages[] = $package;
3✔
192

193
                    $this->io->writeError(sprintf('🚫 Skipped "%s"', $package), true, IO\IOInterface::VERBOSE);
3✔
194

195
                    return false;
3✔
196
                }
197
            }
198

199
            return true;
8✔
200
        });
10✔
201

202
        return $excludedPackages;
10✔
203
    }
204

205
    /**
206
     * @param array<string, array<string, mixed>> $reporters
207
     *
208
     * @throws Exception\ReporterIsNotSupported
209
     */
210
    private function validateReporters(array $reporters): void
12✔
211
    {
212
        foreach ($reporters as $name => $options) {
12✔
213
            // Will throw an exception if reporter is not supported
214
            $reporter = $this->reporterFactory->make($name);
3✔
215
            // Will throw an exception if reporter options are invalid
216
            $reporter->validateOptions($options);
2✔
217
        }
218
    }
219

220
    /**
221
     * @param array<non-empty-string> $packageNames
222
     *
223
     * @return array<Entity\Package\Package>
224
     */
225
    private function mapPackageNamesToPackage(array $packageNames): array
10✔
226
    {
227
        return array_map(
10✔
228
            static fn (string $packageName) => new Entity\Package\InstalledPackage($packageName),
10✔
229
            $packageNames,
10✔
230
        );
10✔
231
    }
232

233
    private function dispatchPostUpdateCheckEvent(Entity\Result\UpdateCheckResult $result): void
8✔
234
    {
235
        $event = new Event\PostUpdateCheckEvent($result);
8✔
236

237
        $this->composer->getEventDispatcher()->dispatch($event->getName(), $event);
8✔
238
    }
239
}
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

© 2025 Coveralls, Inc