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

violinist-dev / composer-updater / 6545699357

17 Oct 2023 10:10AM UTC coverage: 81.481% (+17.5%) from 64.021%
6545699357

Pull #44

github

web-flow
Merge 58ebd8006 into b778aa32f
Pull Request #44: Add a require test as well

154 of 189 relevant lines covered (81.48%)

5.58 hits per line

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

85.88
/src/Updater.php
1
<?php
2

3
namespace Violinist\ComposerUpdater;
4

5
use Psr\Log\LoggerInterface;
6
use Symfony\Component\Process\Process;
7
use Violinist\ComposerLockData\ComposerLockData;
8
use Violinist\ComposerUpdater\Exception\ComposerUpdateProcessFailedException;
9
use Violinist\ComposerUpdater\Exception\NotUpdatedException;
10
use Violinist\ProcessFactory\ProcessFactoryInterface;
11

12
class Updater
13
{
14
    /**
15
     * @var bool
16
     */
17
    protected $runScripts = true;
18

19
    /**
20
     * @var int
21
     */
22
    protected $timeout = 1200;
23

24
    /**
25
     * @var bool
26
     */
27
    protected $withUpdate = true;
28

29
    /**
30
     * @var bool
31
     */
32
    protected $devPackage = false;
33

34
    /**
35
     * @var ProcessFactoryInterface|null
36
     */
37
    protected $processFactory;
38

39
    /**
40
     * @var string
41
     */
42
    protected $package;
43

44
    /**
45
     * @var string
46
     */
47
    protected $cwd;
48

49
    /**
50
     * @var LoggerInterface|null
51
     */
52
    protected $logger;
53

54
    /**
55
     * @var \stdClass
56
     */
57
    protected $postUpdateData;
58

59
    /**
60
     * @var string
61
     */
62
    protected $constraint;
63

64
    /**
65
     * Bundled packages.
66
     *
67
     * @var array
68
     */
69
    protected $bundledPackages;
70

71
    /**
72
     * @var bool
73
     */
74
    protected $shouldThrowOnUnupdated = true;
75

76
    /**
77
     * @var string
78
     */
79
    protected $packageToCheck;
80

81
    /**
82
     * @return bool
83
     */
84
    public function shouldThrowOnUnupdated(): bool
85
    {
86
        return $this->shouldThrowOnUnupdated;
×
87
    }
88

89
    /**
90
     * @param bool $shouldThrowOnUnupdated
91
     */
92
    public function setShouldThrowOnUnupdated(bool $shouldThrowOnUnupdated)
93
    {
94
        $this->shouldThrowOnUnupdated = $shouldThrowOnUnupdated;
2✔
95
    }
96

97
    public function setPackageToCheckHasUpdated($package)
98
    {
99
        $this->packageToCheck = $package;
1✔
100
    }
101

102

103

104
    /**
105
     * @return bool
106
     */
107
    public function shouldRunScripts(): bool
108
    {
109
        return $this->runScripts;
12✔
110
    }
111

112
    /**
113
     * @param bool $runScripts
114
     */
115
    public function setRunScripts(bool $runScripts)
116
    {
117
        $this->runScripts = $runScripts;
×
118
    }
119

120
    /**
121
     * @return array
122
     */
123
    public function getBundledPackages()
124
    {
125
        if (empty($this->bundledPackages)) {
11✔
126
            return [];
10✔
127
        }
128
        return $this->bundledPackages;
1✔
129
    }
130

131
    public function hasBundledPackages()
132
    {
133
        return (bool) count($this->getBundledPackages());
11✔
134
    }
135

136
    /**
137
     * @param array $bundledPackages
138
     */
139
    public function setBundledPackages($bundledPackages)
140
    {
141
        $this->bundledPackages = [];
1✔
142
        // Now filter them. There should only be bundled packages that we can also find in composer.lock
143
        try {
144
            $lock = ComposerLockData::createFromFile($this->cwd . '/composer.lock');
1✔
145
            $this->bundledPackages = array_filter($bundledPackages, function ($package) use ($lock) {
1✔
146
                try {
147
                    return $lock->getPackageData($package);
1✔
148
                } catch (\Throwable $e) {
1✔
149
                    // Probably means the package is not there.
150
                    // Let's also see if we can find a match by wildcard.
151
                    $lock_data = $lock->getData();
1✔
152
                    foreach (['packages', 'packages-dev'] as $type) {
1✔
153
                        if (empty($lock_data->{$type})) {
1✔
154
                            continue;
×
155
                        }
156
                        foreach ($lock_data->{$type} as $package_data) {
1✔
157
                            if (empty($package_data->name)) {
1✔
158
                                continue;
×
159
                            }
160
                            if (fnmatch($package, $package_data->name)) {
1✔
161
                                return true;
1✔
162
                            }
163
                        }
164
                    }
165
                    return false;
×
166
                }
167
            });
1✔
168
        } catch (\Throwable $e) {
×
169
            // So no bundled packages it is. Also. This probably means no updates, but that will be an exception for
170
            // another method.
171
        }
172
    }
173

174
    public function __construct($cwd, $package)
175
    {
176
        $this->cwd = $cwd;
13✔
177
        $this->package = $package;
13✔
178
        $this->packageToCheck = $package;
13✔
179
    }
180

181
    /**
182
     * @return string
183
     */
184
    public function getConstraint()
185
    {
186
        return $this->constraint;
×
187
    }
188

189
    /**
190
     * @param string $constraint
191
     */
192
    public function setConstraint($constraint)
193
    {
194
        $this->constraint = $constraint;
×
195
    }
196

197
    /**
198
     * @return ProcessFactoryInterface
199
     */
200
    public function getProcessFactory()
201
    {
202
        if (!$this->processFactory instanceof ProcessFactoryInterface) {
12✔
203
            $this->processFactory = new ProcessFactory();
9✔
204
        }
205
        return $this->processFactory;
12✔
206
    }
207

208
    /**
209
     * @param ProcessFactoryInterface $processFactory
210
     */
211
    public function setProcessFactory(ProcessFactoryInterface $processFactory)
212
    {
213
        $this->processFactory = $processFactory;
3✔
214
    }
215

216
    /**
217
     * @return LoggerInterface
218
     */
219
    public function getLogger()
220
    {
221
        if (!$this->logger instanceof LoggerInterface) {
12✔
222
            $this->logger = new DefaultLogger();
12✔
223
        }
224
        return $this->logger;
12✔
225
    }
226

227
    /**
228
     * @param LoggerInterface $logger
229
     */
230
    public function setLogger(LoggerInterface $logger)
231
    {
232
        $this->logger = $logger;
×
233
    }
234

235
    /**
236
     * @return bool
237
     */
238
    public function isWithUpdate()
239
    {
240
        return $this->withUpdate;
12✔
241
    }
242

243
    /**
244
     * @param bool $withUpdate
245
     */
246
    public function setWithUpdate($withUpdate)
247
    {
248
        $this->withUpdate = $withUpdate;
×
249
    }
250

251
    /**
252
     * @return bool
253
     */
254
    public function isDevPackage()
255
    {
256
        return $this->devPackage;
1✔
257
    }
258

259
    /**
260
     * @param bool $devPackage
261
     */
262
    public function setDevPackage($devPackage)
263
    {
264
        $this->devPackage = $devPackage;
×
265
    }
266

267
    public function executeRequire($new_version)
268
    {
269
        $pre_update_lock = ComposerLockData::createFromFile($this->cwd . '/composer.lock');
1✔
270
        $pre_update_data = $pre_update_lock->getPackageData($this->packageToCheck);
1✔
271
        $commands = $this->getRequireRecipes($new_version);
1✔
272
        $exception = null;
1✔
273
        $success = false;
1✔
274
        $e = null;
1✔
275
        foreach ($commands as $command) {
1✔
276
            if ($success) {
1✔
277
                continue;
×
278
            }
279
            try {
280
                $full_command = array_merge(
1✔
281
                    $command,
1✔
282
                    array_filter([
1✔
283
                        ($this->isWithUpdate() ? '--update-with-dependencies' : ''),
1✔
284
                        (!$this->shouldRunScripts() ? '--no-scripts' : ''),
1✔
285
                    ])
1✔
286
                );
1✔
287
                $log_command = implode(' ', $full_command);
1✔
288
                $this->log("Creating command $log_command", [
1✔
289
                    'command' => $log_command,
1✔
290
                ]);
1✔
291
                $process = $this->getProcessFactory()->getProcess($full_command, $this->cwd, $this->getEnv(), null, $this->timeout);
1✔
292
                $process->run();
1✔
293
                $this->handlePostComposerCommand($pre_update_data, $process);
1✔
294
                $success = true;
×
295
            } catch (\Throwable $e) {
1✔
296
                continue;
1✔
297
            }
298
        }
299
        if (!$success) {
1✔
300
            // Re-throw the last exception.
301
            if ($e) {
1✔
302
                throw $e;
1✔
303
            }
304
            throw new \Exception('The result was not successful, but we are not sure what failed');
×
305
        }
306
    }
307

308

309
    /**
310
     * @throws \Throwable
311
     * @throws ComposerUpdateProcessFailedException
312
     * @throws NotUpdatedException
313
     */
314
    public function executeUpdate()
315
    {
316
        $pre_update_lock = ComposerLockData::createFromFile($this->cwd . '/composer.lock');
12✔
317
        $pre_update_data = $pre_update_lock->getPackageData($this->packageToCheck);
11✔
318
        $commands = $this->getUpdateRecipies();
11✔
319
        $exception = null;
11✔
320
        $success = false;
11✔
321
        $e = null;
11✔
322
        foreach ($commands as $command) {
11✔
323
            try {
324
                $full_command = array_merge(
11✔
325
                    $command,
11✔
326
                    array_filter([
11✔
327
                        ($this->isWithUpdate() ? '--with-dependencies' : ''),
11✔
328
                        ($this->shouldRunScripts() ? '' : '--no-scripts'),
11✔
329
                    ])
11✔
330
                );
11✔
331
                $log_command = implode(' ', $full_command);
11✔
332
                $this->log("Creating command $log_command", [
11✔
333
                    'command' => $log_command,
11✔
334
                ]);
11✔
335
                $process = $this->getProcessFactory()->getProcess($full_command, $this->cwd, $this->getEnv(), null, $this->timeout);
11✔
336
                $process->run();
11✔
337
                if ($process->getExitCode()) {
11✔
338
                    $exception = new ComposerUpdateProcessFailedException('Composer update exited with exit code ' . $process->getExitCode());
×
339
                    $exception->setErrorOutput($process->getErrorOutput());
×
340
                    throw $exception;
×
341
                }
342
                $this->handlePostComposerCommand($pre_update_data, $process);
11✔
343
                $success = true;
9✔
344
            } catch (\Throwable $e) {
4✔
345
                continue;
4✔
346
            }
347
        }
348
        if (!$success) {
11✔
349
            // Re-throw the last exception.
350
            if ($e) {
2✔
351
                throw $e;
2✔
352
            }
353
            throw new \Exception('The result was not successful, but we are not sure what failed');
×
354
        }
355
    }
356

357
    protected function handlePostComposerCommand($pre_update_data, Process $process)
358
    {
359
        $new_lock_data = @json_decode(@file_get_contents(sprintf('%s/composer.lock', $this->cwd)));
12✔
360
        if (!$new_lock_data) {
12✔
361
            $message = sprintf('No composer.lock found after updating %s', $this->package);
×
362
            $this->log($message);
×
363
            $this->log('This is the stdout:');
×
364
            $this->log($process->getOutput());
×
365
            $this->log('This is the stderr:');
×
366
            $this->log($process->getErrorOutput());
×
367
            throw new \Exception($message);
×
368
        }
369
        $post_update_data = ComposerLockData::createFromString(json_encode($new_lock_data))->getPackageData($this->packageToCheck);
12✔
370
        $version_to = $post_update_data->version;
12✔
371
        $version_from = $pre_update_data->version;
12✔
372
        if (isset($post_update_data->source) && $post_update_data->source->type == 'git' && isset($pre_update_data->source)) {
12✔
373
            $version_from = $pre_update_data->source->reference;
11✔
374
            $version_to = $post_update_data->source->reference;
11✔
375
        }
376
        if ($this->shouldThrowOnUnupdated && $version_to === $version_from) {
12✔
377
            // Nothing has happened here. Although that can be alright (like we
378
            // have updated some dependencies of this package) this is not what
379
            // this service does, currently, and also the title of the PR would be
380
            // wrong.
381
            // In theory though, the reference sources can be the same (the same commit), but the
382
            // version is different. In which case it does not really matter much to update, but it
383
            // can be frustrating to get an error. So let's not give an error.
384
            if ($post_update_data->version === $pre_update_data->version) {
5✔
385
                $this->log($process->getErrorOutput(), [
5✔
386
                    'package' => $this->package,
5✔
387
                ]);
5✔
388
                throw new NotUpdatedException('The version installed is still the same after trying to update.');
5✔
389
            }
390
        }
391
        // We still want the post update data to be from the actual package though, no matter if we were actually
392
        // checking if a dependency was updated or not.
393
        $actual_package_post_update_data = ComposerLockData::createFromString(json_encode($new_lock_data))->getPackageData($this->package);
9✔
394
        $this->postUpdateData = $actual_package_post_update_data;
9✔
395
        // And make sure we log this as well.
396
        $this->log($process->getOutput());
9✔
397
        $this->log($process->getErrorOutput());
9✔
398
    }
399

400
    /**
401
     * @return \stdClass
402
     */
403
    public function getPostUpdateData()
404
    {
405
        return $this->postUpdateData;
1✔
406
    }
407

408
    protected function log($message, $context = [])
409
    {
410
        $this->getLogger()->log('info', $message, $context);
12✔
411
    }
412

413
    protected function getRequireRecipes($version)
414
    {
415
        return [
1✔
416
            array_filter([
1✔
417
                'composer',
1✔
418
                'require',
1✔
419
                $this->isDevPackage() ? '--dev' : '',
1✔
420
                '-n',
1✔
421
                '--no-ansi',
1✔
422
                sprintf('%s:%s%s', $this->package, $this->constraint, $version),
1✔
423
            ]),
1✔
424
        ];
1✔
425
    }
426

427
    protected function getUpdateRecipies()
428
    {
429
        $map = [
11✔
430
            'drupal/core' => [
11✔
431
                ['composer', 'update', 'drupal/core', 'drupal/core-*', '--with-all-dependencies'],
11✔
432
                ['composer', 'update', '-n', '--no-ansi', 'drupal/core', 'webflo/drupal-core-require-dev', 'symfony/*'],
11✔
433
            ],
11✔
434
            'drupal/core-recommended' => [
11✔
435
                ['composer', 'update', 'drupal/core', 'drupal/core-*', '--with-all-dependencies'],
11✔
436
            ],
11✔
437
            'drupal/dropzonejs' => [
11✔
438
                ['composer', 'update', '-n', '--no-ansi', 'drupal/dropzonejs', 'drupal/dropzonejs_eb_widget'],
11✔
439
            ],
11✔
440
            'drupal/commerce' => [
11✔
441
                ['composer', 'update', '-n', '--no-ansi', 'drupal/commerce', 'drupal/commerce_price', 'drupal/commerce_product', 'drupal/commerce_order', 'drupal/commerce_payment', 'drupal/commerce_payment_example', 'drupal/commerce_checkout', 'drupal/commerce_tax', 'drupal/commerce_cart', 'drupal/commerce_log', 'drupal/commerce_store', 'drupal/commerce_promotion', 'drupal/commerce_number_pattern'],
11✔
442
            ],
11✔
443
            'drupal/league_oauth_login' => [
11✔
444
                ['composer', 'update', '-n', '--no-ansi', 'drupal/league_oauth_login', 'drupal/league_oauth_login_github', 'drupal/league_oauth_login_gitlab'],
11✔
445
            ],
11✔
446
        ];
11✔
447
        $return = [
11✔
448
            ['composer', 'update', '-n', '--no-ansi', $this->package],
11✔
449
        ];
11✔
450
        if ($this->hasBundledPackages()) {
11✔
451
            $return = [
1✔
452
                array_merge(['composer', 'update', '-n', '--no-ansi', $this->package], $this->getBundledPackages()),
1✔
453
            ];
1✔
454
        }
455
        if (isset($map[$this->package])) {
11✔
456
            $return = array_merge($return, $map[$this->package]);
5✔
457
        }
458
        return $return;
11✔
459
    }
460

461
    protected function getEnv()
462
    {
463
        return [
12✔
464
            // Need the path to composer.
465
            'PATH' => getenv('PATH'),
12✔
466
            // And we need a "HOME" environment.
467
            'HOME' => getenv('HOME'),
12✔
468
            'COMPOSER_ALLOW_SUPERUSER' => 1,
12✔
469
            'COMPOSER_DISCARD_CHANGES' => 'true',
12✔
470
            'COMPOSER_NO_INTERACTION' => 1,
12✔
471
        ];
12✔
472
    }
473
}
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