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

violinist-dev / composer-updater / 6574636024

19 Oct 2023 12:33PM UTC coverage: 84.653% (+0.5%) from 84.127%
6574636024

Pull #42

github

web-flow
Merge branch 'main' into feat/multi-check-pack
Pull Request #42: Make it possible to check for several packages that has updated

30 of 30 new or added lines in 1 file covered. (100.0%)

171 of 202 relevant lines covered (84.65%)

10.79 hits per line

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

87.89
/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 array
78
     */
79
    protected $packagesToCheck = [];
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;
4✔
95
    }
96

97
    /**
98
     * @deprecated This method should not be used, and instead we should use the
99
     * one that accepts an array of packages. @see ::setPackagesToCheckHasUpdated
100
     */
101
    public function setPackageToCheckHasUpdated($package)
102
    {
103
        $this->setPackagesToCheckHasUpdated([$package]);
2✔
104
    }
105

106
    public function setPackagesToCheckHasUpdated(array $packages)
107
    {
108
        $this->packagesToCheck = $packages;
24✔
109
    }
110

111
    /**
112
     * @return bool
113
     */
114
    public function shouldRunScripts(): bool
115
    {
116
        return $this->runScripts;
22✔
117
    }
118

119
    /**
120
     * @param bool $runScripts
121
     */
122
    public function setRunScripts(bool $runScripts)
123
    {
124
        $this->runScripts = $runScripts;
×
125
    }
126

127
    /**
128
     * @return array
129
     */
130
    public function getBundledPackages()
131
    {
132
        if (empty($this->bundledPackages)) {
20✔
133
            return [];
18✔
134
        }
135
        return $this->bundledPackages;
2✔
136
    }
137

138
    public function hasBundledPackages()
139
    {
140
        return (bool) count($this->getBundledPackages());
20✔
141
    }
142

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

181
    public function __construct($cwd, $package)
182
    {
183
        $this->cwd = $cwd;
24✔
184
        $this->package = $package;
24✔
185
        $this->setPackagesToCheckHasUpdated([$package]);
24✔
186
    }
187

188
    /**
189
     * @return string
190
     */
191
    public function getConstraint()
192
    {
193
        return $this->constraint;
×
194
    }
195

196
    /**
197
     * @param string $constraint
198
     */
199
    public function setConstraint($constraint)
200
    {
201
        $this->constraint = $constraint;
×
202
    }
203

204
    /**
205
     * @return ProcessFactoryInterface
206
     */
207
    public function getProcessFactory()
208
    {
209
        if (!$this->processFactory instanceof ProcessFactoryInterface) {
22✔
210
            $this->processFactory = new ProcessFactory();
16✔
211
        }
212
        return $this->processFactory;
22✔
213
    }
214

215
    /**
216
     * @param ProcessFactoryInterface $processFactory
217
     */
218
    public function setProcessFactory(ProcessFactoryInterface $processFactory)
219
    {
220
        $this->processFactory = $processFactory;
6✔
221
    }
222

223
    /**
224
     * @return LoggerInterface
225
     */
226
    public function getLogger()
227
    {
228
        if (!$this->logger instanceof LoggerInterface) {
22✔
229
            $this->logger = new DefaultLogger();
22✔
230
        }
231
        return $this->logger;
22✔
232
    }
233

234
    /**
235
     * @param LoggerInterface $logger
236
     */
237
    public function setLogger(LoggerInterface $logger)
238
    {
239
        $this->logger = $logger;
×
240
    }
241

242
    /**
243
     * @return bool
244
     */
245
    public function isWithUpdate()
246
    {
247
        return $this->withUpdate;
22✔
248
    }
249

250
    /**
251
     * @param bool $withUpdate
252
     */
253
    public function setWithUpdate($withUpdate)
254
    {
255
        $this->withUpdate = $withUpdate;
×
256
    }
257

258
    /**
259
     * @return bool
260
     */
261
    public function isDevPackage()
262
    {
263
        return $this->devPackage;
2✔
264
    }
265

266
    /**
267
     * @param bool $devPackage
268
     */
269
    public function setDevPackage($devPackage)
270
    {
271
        $this->devPackage = $devPackage;
×
272
    }
273

274
    protected function getPreUpdateData() : array
275
    {
276
        $pre_update_lock = ComposerLockData::createFromFile($this->cwd . '/composer.lock');
24✔
277
        return array_map(function ($package) use ($pre_update_lock) {
22✔
278
            return $pre_update_lock->getPackageData($package);
22✔
279
        }, $this->packagesToCheck);
22✔
280
    }
281

282
    public function executeRequire($new_version)
283
    {
284
        $pre_update_data = $this->getPreUpdateData();
2✔
285
        $commands = $this->getRequireRecipes($new_version);
2✔
286
        $exception = null;
2✔
287
        $success = false;
2✔
288
        $e = null;
2✔
289
        foreach ($commands as $command) {
2✔
290
            if ($success) {
2✔
291
                continue;
×
292
            }
293
            try {
294
                $full_command = array_merge(
2✔
295
                    $command,
2✔
296
                    array_filter([
2✔
297
                        ($this->isWithUpdate() ? '--update-with-dependencies' : ''),
2✔
298
                        (!$this->shouldRunScripts() ? '--no-scripts' : ''),
2✔
299
                    ])
2✔
300
                );
2✔
301
                $log_command = implode(' ', $full_command);
2✔
302
                $this->log("Creating command $log_command", [
2✔
303
                    'command' => $log_command,
2✔
304
                ]);
2✔
305
                $process = $this->getProcessFactory()->getProcess($full_command, $this->cwd, $this->getEnv(), null, $this->timeout);
2✔
306
                $process->run();
2✔
307
                $this->handlePostComposerCommand($pre_update_data, $process);
2✔
308
                $success = true;
×
309
            } catch (\Throwable $e) {
2✔
310
                continue;
2✔
311
            }
312
        }
313
        if (!$success) {
2✔
314
            // Re-throw the last exception.
315
            if ($e) {
2✔
316
                throw $e;
2✔
317
            }
318
            throw new \Exception('The result was not successful, but we are not sure what failed');
×
319
        }
320
    }
321

322

323
    /**
324
     * @throws \Throwable
325
     * @throws ComposerUpdateProcessFailedException
326
     * @throws NotUpdatedException
327
     */
328
    public function executeUpdate()
329
    {
330
        $pre_update_data = $this->getPreUpdateData();
22✔
331
        $commands = $this->getUpdateRecipies();
20✔
332
        $exception = null;
20✔
333
        $success = false;
20✔
334
        $e = null;
20✔
335
        foreach ($commands as $command) {
20✔
336
            try {
337
                $full_command = array_merge(
20✔
338
                    $command,
20✔
339
                    array_filter([
20✔
340
                        ($this->isWithUpdate() ? '--with-dependencies' : ''),
20✔
341
                        ($this->shouldRunScripts() ? '' : '--no-scripts'),
20✔
342
                    ])
20✔
343
                );
20✔
344
                $log_command = implode(' ', $full_command);
20✔
345
                $this->log("Creating command $log_command", [
20✔
346
                    'command' => $log_command,
20✔
347
                ]);
20✔
348
                $process = $this->getProcessFactory()->getProcess($full_command, $this->cwd, $this->getEnv(), null, $this->timeout);
20✔
349
                $process->run();
20✔
350
                if ($process->getExitCode()) {
20✔
351
                    $exception = new ComposerUpdateProcessFailedException('Composer update exited with exit code ' . $process->getExitCode());
2✔
352
                    $exception->setErrorOutput($process->getErrorOutput());
2✔
353
                    throw $exception;
2✔
354
                }
355
                $this->handlePostComposerCommand($pre_update_data, $process);
18✔
356
                $success = true;
14✔
357
            } catch (\Throwable $e) {
9✔
358
                continue;
9✔
359
            }
360
        }
361
        if (!$success) {
20✔
362
            // Re-throw the last exception.
363
            if ($e) {
6✔
364
                throw $e;
6✔
365
            }
366
            throw new \Exception('The result was not successful, but we are not sure what failed');
×
367
        }
368
    }
369

370
    protected function handlePostComposerCommand(array $pre_update_data_array, Process $process)
371
    {
372
        $new_lock_data = @json_decode(@file_get_contents(sprintf('%s/composer.lock', $this->cwd)));
20✔
373
        if (!$new_lock_data) {
20✔
374
            $message = sprintf('No composer.lock found after updating %s', $this->package);
×
375
            $this->log($message);
×
376
            $this->log('This is the stdout:');
×
377
            $this->log($process->getOutput());
×
378
            $this->log('This is the stderr:');
×
379
            $this->log($process->getErrorOutput());
×
380
            throw new \Exception($message);
×
381
        }
382
        $has_updated_at_least_one_package = false;
20✔
383
        foreach ($this->packagesToCheck as $package) {
20✔
384
            $pre_update_data = $this->getPreUpdataDataForPackageFromArray($pre_update_data_array, $package);
20✔
385
            $post_update_data = ComposerLockData::createFromString(json_encode($new_lock_data))->getPackageData($package);
20✔
386
            $version_to = $post_update_data->version;
20✔
387
            $version_from = $pre_update_data->version;
20✔
388
            if (isset($post_update_data->source) && $post_update_data->source->type == 'git' && isset($pre_update_data->source)) {
20✔
389
                $version_from = $pre_update_data->source->reference;
18✔
390
                $version_to = $post_update_data->source->reference;
18✔
391
            }
392
            if ($version_from === $version_to) {
20✔
393
                // In theory though, the reference sources can be the same (the same commit), but the
394
                // version is different. In which case it does not really matter much to update, but it
395
                // can be frustrating to get an error. So let's not give an error.
396
                if ($post_update_data->version === $pre_update_data->version) {
11✔
397
                    $this->log($process->getErrorOutput(), [
11✔
398
                        'package' => $this->package,
11✔
399
                    ]);
11✔
400
                } else {
401
                    $has_updated_at_least_one_package = true;
11✔
402
                }
403
            } else {
404
                $has_updated_at_least_one_package = true;
12✔
405
            }
406
        }
407
        if ($this->shouldThrowOnUnupdated && !$has_updated_at_least_one_package) {
20✔
408
            // Nothing has happened here. Although that can be alright (like we
409
            // have updated some dependencies of this package), we have at least
410
            // not updated any of the expected dependencies at this point.
411
            throw new NotUpdatedException('The version installed is still the same after trying to update.');
9✔
412
        }
413
        // We still want the post update data to be from the actual package though, no matter if we were actually
414
        // checking if a dependency was updated or not.
415
        $actual_package_post_update_data = ComposerLockData::createFromString(json_encode($new_lock_data))->getPackageData($this->package);
14✔
416
        $this->postUpdateData = $actual_package_post_update_data;
14✔
417
        // And make sure we log this as well.
418
        $this->log($process->getOutput());
14✔
419
        $this->log($process->getErrorOutput());
14✔
420
    }
421

422
    protected function getPreUpdataDataForPackageFromArray(array $pre_update_data_array, $package) : ?\stdClass
423
    {
424
        foreach ($pre_update_data_array as $item) {
20✔
425
            if ($item->name === $package) {
20✔
426
                return $item;
20✔
427
            }
428
        }
429
        return null;
×
430
    }
431

432
    /**
433
     * @return \stdClass
434
     */
435
    public function getPostUpdateData()
436
    {
437
        return $this->postUpdateData;
2✔
438
    }
439

440
    protected function log($message, $context = [])
441
    {
442
        $this->getLogger()->log('info', $message, $context);
22✔
443
    }
444

445
    protected function getRequireRecipes($version)
446
    {
447
        return [
2✔
448
            array_filter([
2✔
449
                'composer',
2✔
450
                'require',
2✔
451
                $this->isDevPackage() ? '--dev' : '',
2✔
452
                '-n',
2✔
453
                '--no-ansi',
2✔
454
                sprintf('%s:%s%s', $this->package, $this->constraint, $version),
2✔
455
            ]),
2✔
456
        ];
2✔
457
    }
458

459
    protected function getUpdateRecipies()
460
    {
461
        $map = [
20✔
462
            'drupal/core' => [
20✔
463
                ['composer', 'update', 'drupal/core', 'drupal/core-*', '--with-all-dependencies'],
20✔
464
                ['composer', 'update', '-n', '--no-ansi', 'drupal/core', 'webflo/drupal-core-require-dev', 'symfony/*'],
20✔
465
            ],
20✔
466
            'drupal/core-recommended' => [
20✔
467
                ['composer', 'update', 'drupal/core', 'drupal/core-*', '--with-all-dependencies'],
20✔
468
            ],
20✔
469
            'drupal/dropzonejs' => [
20✔
470
                ['composer', 'update', '-n', '--no-ansi', 'drupal/dropzonejs', 'drupal/dropzonejs_eb_widget'],
20✔
471
            ],
20✔
472
            'drupal/commerce' => [
20✔
473
                ['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'],
20✔
474
            ],
20✔
475
            'drupal/league_oauth_login' => [
20✔
476
                ['composer', 'update', '-n', '--no-ansi', 'drupal/league_oauth_login', 'drupal/league_oauth_login_github', 'drupal/league_oauth_login_gitlab'],
20✔
477
            ],
20✔
478
        ];
20✔
479
        $return = [
20✔
480
            ['composer', 'update', '-n', '--no-ansi', $this->package],
20✔
481
        ];
20✔
482
        if ($this->hasBundledPackages()) {
20✔
483
            $return = [
2✔
484
                array_merge(['composer', 'update', '-n', '--no-ansi', $this->package], $this->getBundledPackages()),
2✔
485
            ];
2✔
486
        }
487
        if (isset($map[$this->package])) {
20✔
488
            $return = array_merge($return, $map[$this->package]);
8✔
489
        }
490
        return $return;
20✔
491
    }
492

493
    protected function getEnv()
494
    {
495
        return [
22✔
496
            // Need the path to composer.
497
            'PATH' => getenv('PATH'),
22✔
498
            // And we need a "HOME" environment.
499
            'HOME' => getenv('HOME'),
22✔
500
            'COMPOSER_ALLOW_SUPERUSER' => 1,
22✔
501
            'COMPOSER_DISCARD_CHANGES' => 'true',
22✔
502
            'COMPOSER_NO_INTERACTION' => 1,
22✔
503
        ];
22✔
504
    }
505
}
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