Completed
Push — master ( 580007...3ce141 )
by
unknown
9s
created

src/Tasks/CheckComposerUpdatesTask.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
use BringYourOwnIdeas\Maintenance\Util\ComposerLoader;
4
use Packagist\Api\Client;
5
6
/**
7
 * Task which does the actual checking of updates
8
 *
9
 * Originally from https://github.com/XploreNet/silverstripe-composerupdates
10
 *
11
 * @author Matt Dwen
12
 * @license MIT
13
 */
14
class CheckComposerUpdatesTask extends BuildTask
15
{
16
    /**
17
     * @var string
18
     */
19
    protected $title = 'Composer update checker';
20
21
    /**
22
     * @var string
23
     */
24
    protected $description = 'Checks if any composer dependencies can be updated.';
25
26
    private static $dependencies = [
27
        'PackagistClient' => '%$Packagist\\Api\\Client',
28
        'ComposerLoader' => '%$BringYourOwnIdeas\\Maintenance\\Util\\ComposerLoader',
29
    ];
30
31
    /**
32
     * Minimum required stability defined in the site composer.json
33
     *
34
     * @var string
35
     */
36
    protected $minimumStability;
37
38
    /**
39
     * Whether or not to prefer stable packages
40
     *
41
     * @var bool
42
     */
43
    protected $preferStable;
44
45
    /**
46
     * Known stability values
47
     *
48
     * @var array
49
     */
50
    protected $stabilityOptions = array(
51
        'dev',
52
        'alpha',
53
        'beta',
54
        'rc',
55
        'stable'
56
    );
57
58
    /**
59
     * @var Client
60
     */
61
    protected $packagistClient;
62
63
    /**
64
     * @var ComposerLoader
65
     */
66
    protected $composerLoader;
67
68
    /**
69
     * Retrieve an array of primary composer dependencies from composer.json
70
     *
71
     * @return array
72
     */
73
    protected function getPackages()
74
    {
75
        $composerJson = $this->getComposerLoader()->getJson();
76
77
        ini_set('display_errors', 1);
78
        error_reporting(E_ALL);
79
80
        // Set the stability parameters
81
        $this->setMinimumStability(
82
            isset($composerJson->{'minimum-stability'}) ? $composerJson->{'minimum-stability'} : 'stable'
83
        );
84
85
        $this->setPreferStable(
86
            isset($composerJson->{'prefer-stable'}) ? $composerJson->{'prefer-stable'} : true
87
        );
88
89
        $packages = [];
90
        foreach ($composerJson->require as $package => $version) {
91
            // Ensure there's a / in the name, probably not an addon with it
92
            if (!strpos($package, '/')) {
93
                continue;
94
            }
95
96
            $packages[] = $package;
97
        }
98
99
        return $packages;
100
    }
101
102
    /**
103
     * Return an array of all Composer dependencies from composer.lock
104
     *
105
     * Example: `['silverstripe/cms' => '3.4.1', ...]`
106
     *
107
     * @return array
108
     */
109
    protected function getDependencies()
110
    {
111
        $packages = [];
112
        foreach ($this->getComposerLoader()->getLock()->packages as $package) {
113
            $packages[$package->name] = $package->version;
114
        }
115
        return $packages;
116
    }
117
118
    /**
119
     * Check if an available version is better than the current version,
120
     * considering stability requirements
121
     *
122
     * Returns FALSE if no update is available.
123
     * Returns the best available version if an update is available.
124
     *
125
     * @param string $currentVersion
126
     * @param string[] $availableVersions
127
     * @return bool|string
0 ignored issues
show
Should the return type not be boolean|string|integer?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
128
     */
129
    protected function hasUpdate($currentVersion, $availableVersions)
130
    {
131
        $currentVersion = strtolower($currentVersion);
132
133
        // Check there are some versions
134
        if (count($availableVersions) < 1) {
135
            return false;
136
        }
137
138
        // If this is dev-master, compare the hashes
139
        if ($currentVersion === 'dev-master') {
140
            return $this->hasUpdateOnDevMaster($availableVersions);
141
        }
142
143
        // Loop through each available version
144
        $currentStability = $this->getStability($currentVersion);
145
        $bestVersion = $currentVersion;
146
        $bestStability = $currentStability;
147
        $availableVersions = array_reverse($availableVersions, true);
148
        foreach ($availableVersions as $version => $details) {
149
            // Get the stability of the version
150
            $versionStability = $this->getStability($version);
151
152
            // Does this meet minimum stability
153
            if (!$this->isStableEnough($this->getMinimumStability(), $versionStability)) {
154
                continue;
155
            }
156
157
            if ($this->getPreferStable()) {
158
                // A simple php version compare rules out the dumb stuff
159
                if (version_compare($bestVersion, $version) !== -1) {
160
                    continue;
161
                }
162
            } else {
163
                // We're doing a straight version compare
164
                $pureBestVersion = $this->getPureVersion($bestVersion);
165
                $pureVersion = $this->getPureVersion($version);
166
167
                // Checkout the version
168
                $continue = false;
169
                switch (version_compare($pureBestVersion, $pureVersion)) {
170
                    case -1:
171
                        // The version is better, take it
172
                        break;
173
174
                    case 0:
175
                        // The version is the same.
176
                        // Do another straight version compare to rule out rc1 vs rc2 etc...
177
                        if ($bestStability == $versionStability) {
178
                            if (version_compare($bestVersion, $version) !== -1) {
179
                                $continue = true;
180
                                break;
181
                            }
182
                        }
183
                        break;
184
185
                    case 1:
186
                        // The version is worse, ignore it
187
                        $continue = true;
188
                        break;
189
                }
190
191
                if ($continue) {
192
                    continue;
193
                }
194
            }
195
196
            $bestVersion = $version;
197
            $bestStability = $versionStability;
198
        }
199
200
        if ($bestVersion !== $currentVersion || $bestStability !== $currentStability) {
201
            if ($bestStability === 'stable') {
202
                return $bestVersion;
203
            }
204
205
            return $bestVersion . '-' . $bestStability;
206
        }
207
208
        return false;
209
    }
210
211
    /**
212
     * Check the latest hash on the dev-master branch, and return it if different to the local hash
213
     *
214
     * FALSE is returned if the hash is the same.
215
     *
216
     * @param $availableVersions
217
     * @return bool|string
218
     */
219
    protected function hasUpdateOnDevMaster($availableVersions)
220
    {
221
        // Get the dev-master version
222
        $devMaster = $availableVersions['dev-master'];
223
224
        // Sneak the name of the package
225
        $packageName = $devMaster->getName();
226
227
        // Get the local package details
228
        $localPackage = $this->getLocalPackage($packageName);
229
230
        // What's the current hash?
231
        $localHash = $localPackage->source->reference;
232
233
        // What's the latest hash in the available versions
234
        $remoteHash = $devMaster->getSource()->getReference();
235
236
        // return either the new hash or false
237
        return ($localHash != $remoteHash) ? $remoteHash : false;
238
    }
239
240
    /**
241
     * Return details from composer.lock for a specific package
242
     *
243
     * @param string $packageName
244
     * @return object
245
     * @throws Exception if package cannot be found in composer.lock
246
     */
247
    protected function getLocalPackage($packageName)
248
    {
249
        foreach ($this->getComposerLoader()->getLock()->packages as $package) {
250
            if ($package->name == $packageName) {
251
                return $package;
252
            }
253
        }
254
255
        throw new Exception('Cannot locate local package ' . $packageName);
256
    }
257
258
    /**
259
     * Retrieve the pure numerical version
260
     *
261
     * @param string $version
262
     * @return string|null
263
     */
264
    protected function getPureVersion($version)
265
    {
266
        $matches = [];
267
268
        preg_match("/^(\d+\\.)?(\d+\\.)?(\\*|\d+)/", $version, $matches);
269
270
        if (count($matches) > 0) {
271
            return $matches[0];
272
        }
273
274
        return null;
275
    }
276
277
    /**
278
     * Determine the stability of a given version
279
     *
280
     * @param string $version
281
     * @return string
282
     */
283
    protected function getStability($version)
284
    {
285
        $version = strtolower($version);
286
287
        foreach ($this->getStabilityOptions() as $option) {
288
            if (strpos($version, $option) !== false) {
289
                return $option;
290
            }
291
        }
292
293
        return 'stable';
294
    }
295
296
    /**
297
     * Return a numerical representation of a stability
298
     *
299
     * Higher is more stable
300
     *
301
     * @param string $stability
302
     * @return int
0 ignored issues
show
Should the return type not be integer|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
303
     * @throws Exception If the stability is unknown
304
     */
305
    protected function getStabilityIndex($stability)
306
    {
307
        $stability = strtolower($stability);
308
309
        $index = array_search($stability, $this->getStabilityOptions(), true);
310
311
        if ($index === false) {
312
            throw new Exception("Unknown stability: $stability");
313
        }
314
315
        return $index;
316
    }
317
318
    /**
319
     * Check if a stability meets a given minimum requirement
320
     *
321
     * @param $currentStability
322
     * @param $possibleStability
323
     * @return bool
324
     */
325
    protected function isStableEnough($currentStability, $possibleStability)
326
    {
327
        $minimumIndex = $this->getStabilityIndex($currentStability);
328
        $possibleIndex = $this->getStabilityIndex($possibleStability);
329
330
        return ($possibleIndex >= $minimumIndex);
331
    }
332
333
    /**
334
     * Record package details in the database
335
     *
336
     * @param string $package Name of the Composer Package
337
     * @param string $installed Currently installed version
338
     * @param string|boolean $latest The latest available version
339
     */
340
    protected function recordUpdate($package, $installed, $latest)
341
    {
342
        // Is there a record already for the package? If so find it.
343
        $packages = Package::get()->filter(['Name' => $package]);
344
345
        // Get the hash installed
346
        $localPackage = $this->getLocalPackage($package);
347
        $installedHash = $localPackage->source->reference;
348
349
        // if there is already one use it otherwise create a new data object
350
        if ($packages->count() > 0) {
351
            $update = $packages->first();
352
        } else {
353
            $update = Package::create();
354
            $update->Name = $package;
355
            $update->Version = $installed;
356
            $update->VersionHash = $installedHash;
357
        }
358
359
        // Set the new details and save it
360
        $update->LatestVersion = $latest;
361
        $update->write();
362
    }
363
364
    /**
365
     * runs the actual steps to verify if there are updates available
366
     *
367
     * @param SS_HTTPRequest $request
368
     */
369
    public function run($request)
370
    {
371
        // Retrieve the packages
372
        $packages = $this->getPackages();
373
        $dependencies = $this->getDependencies();
374
375
        // run through the packages and check each for updates
376
        foreach ($packages as $package) {
377
            // verify that we need to check this package.
378
            if (!isset($dependencies[$package])) {
379
                continue;
380
            } else {
381
                // get information about this package from packagist.
382
                try {
383
                    $latest = $this->getPackagistClient()->get($package);
384
                } catch (Guzzle\Http\Exception\ClientErrorResponseException $e) {
385
                    SS_Log::log($e->getMessage(), SS_Log::WARN);
386
                    continue;
387
                }
388
389
                // Check if there is a newer version
390
                $currentVersion = $dependencies[$package];
391
                $result = $this->hasUpdate($currentVersion, $latest->getVersions());
392
393
                // Check if there is a newer version and if so record the update
394
                if ($result !== false) {
395
                    $this->recordUpdate($package, $currentVersion, $result);
396
                }
397
            }
398
        }
399
400
        // finished message
401
        $this->message('The task finished running. You can find the updated information in the database now.');
402
    }
403
404
    /**
405
     * prints a message during the run of the task
406
     *
407
     * @param string $text
408
     */
409
    protected function message($text)
410
    {
411
        if (!Director::is_cli()) {
412
            $text = '<p>' . $text . '</p>';
413
        }
414
415
        echo $text . PHP_EOL;
416
    }
417
418
    /**
419
     * @param string $minimumStability
420
     * @return $this
421
     */
422
    public function setMinimumStability($minimumStability)
423
    {
424
        $this->minimumStability = $minimumStability;
425
        return $this;
426
    }
427
428
    /**
429
     * @return string
430
     */
431
    public function getMinimumStability()
432
    {
433
        return $this->minimumStability;
434
    }
435
436
    /**
437
     * @param bool $preferStable
438
     * @return $this
439
     */
440
    public function setPreferStable($preferStable)
441
    {
442
        $this->preferStable = $preferStable;
443
        return $this;
444
    }
445
446
    /**
447
     * @return bool
448
     */
449
    public function getPreferStable()
450
    {
451
        return $this->preferStable;
452
    }
453
454
    /**
455
     * @param array $stabilityOptions
456
     * @return $this
457
     */
458
    public function setStabilityOptions($stabilityOptions)
459
    {
460
        $this->stabilityOptions = $stabilityOptions;
461
        return $this;
462
    }
463
464
    /**
465
     * @return array
466
     */
467
    public function getStabilityOptions()
468
    {
469
        return $this->stabilityOptions;
470
    }
471
472
    /**
473
     * @return Client
474
     */
475
    public function getPackagistClient()
476
    {
477
        return $this->packagistClient;
478
    }
479
480
    /**
481
     * @param Client $packagistClient
482
     */
483
    public function setPackagistClient(Client $packagistClient)
484
    {
485
        $this->packagistClient = $packagistClient;
486
        return $this;
487
    }
488
489
    /**
490
     * @param ComposerLoader $composerLoader
491
     * @return $this
492
     */
493
    public function setComposerLoader(ComposerLoader $composerLoader)
494
    {
495
        $this->composerLoader = $composerLoader;
496
        return $this;
497
    }
498
499
    /**
500
     * @return ComposerLoader
501
     */
502
    public function getComposerLoader()
503
    {
504
        return $this->composerLoader;
505
    }
506
}
507