Completed
Pull Request — master (#21)
by Robbie
01:41
created

CheckComposerUpdatesTask::getDependencies()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 8
rs 9.4285
cc 2
eloc 5
nc 2
nop 0
1
<?php
2
3
use BringYourOwnIdeas\UpdateChecker\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
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
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 = [
0 ignored issues
show
Unused Code introduced by
The property $dependencies is not used and could be removed.

This check marks private properties in classes that are never used. Those properties can be removed.

Loading history...
27
        'PackagistClient' => '%$Packagist\\Api\\Client',
28
        'ComposerLoader' => '%$BringYourOwnIdeas\\UpdateChecker\\ComposerLoader',
29
    ];
30
31
    /**
32
     * Minimum required stability defined in the site composer.json
33
     *
34
     * @var string
35
     */
36
    private $minimumStability;
37
38
    /**
39
     * Whether or not to prefer stable packages
40
     *
41
     * @var bool
42
     */
43
    private $preferStable;
44
45
    /**
46
     * Known stability values
47
     *
48
     * @var array
49
     */
50
    private $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
    private 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
    private 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
Documentation introduced by
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
    private 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
    private 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
    private function getLocalPackage($packageName)
248
    {
249
        foreach ($this->getComposerLock()->packages as $package) {
0 ignored issues
show
Documentation Bug introduced by
The method getComposerLock does not exist on object<CheckComposerUpdatesTask>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
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
    private 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
    private 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
Documentation introduced by
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
    private 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
    private 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
    private function recordUpdate($package, $installed, $latest)
341
    {
342
        // Is there a record already for the package? If so find it.
343
        $packages = ComposerUpdate::get()->filter(['Name' => $package]);
344
345
        // if there is already one use it otherwise create a new data object
346
        if ($packages->count() > 0) {
347
            $update = $packages->first();
348
        } else {
349
            $update = ComposerUpdate::create();
350
            $update->Name = $package;
0 ignored issues
show
Documentation introduced by
The property Name does not exist on object<ComposerUpdate>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
351
        }
352
353
        // If installed is dev-master get the hash
354
        if ($installed === 'dev-master') {
355
            $localPackage = $this->getLocalPackage($package);
356
            $installed = $localPackage->source->reference;
357
        }
358
359
        // Set the new details and save it
360
        $update->Installed = $installed;
361
        $update->Available = $latest;
362
        $update->write();
363
    }
364
365
    /**
366
     * runs the actual steps to verify if there are updates available
367
     *
368
     * @param SS_HTTPRequest $request
369
     */
370
    public function run($request)
371
    {
372
        // Retrieve the packages
373
        $packages = $this->getPackages();
374
        $dependencies = $this->getDependencies();
375
376
        // run through the packages and check each for updates
377
        foreach ($packages as $package) {
378
            // verify that we need to check this package.
379
            if (!isset($dependencies[$package])) {
380
                continue;
381
            } else {
382
                // get information about this package from packagist.
383
                try {
384
                    $latest = $this->getPackagistClient()->get($package);
385
                } catch (Guzzle\Http\Exception\ClientErrorResponseException $e) {
0 ignored issues
show
Bug introduced by
The class Guzzle\Http\Exception\ClientErrorResponseException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
386
                    SS_Log::log($e->getMessage(), SS_Log::WARN);
387
                    continue;
388
                }
389
390
                // Check if there is a newer version
391
                $currentVersion = $dependencies[$package];
392
                $result = $this->hasUpdate($currentVersion, $latest->getVersions());
393
394
                // Check if there is a newer version and if so record the update
395
                if ($result !== false) {
396
                    $this->recordUpdate($package, $currentVersion, $result);
397
                }
398
            }
399
        }
400
401
        // finished message
402
        $this->message('The task finished running. You can find the updated information in the database now.');
403
    }
404
405
    /**
406
     * prints a message during the run of the task
407
     *
408
     * @param string $text
409
     */
410
    protected function message($text)
411
    {
412
        if (PHP_SAPI !== 'cli') {
413
            $text = '<p>' . $text . '</p>' . PHP_EOL;
414
        }
415
416
        echo $text . PHP_EOL;
417
    }
418
419
    /**
420
     * @param string $minimumStability
421
     * @return $this
422
     */
423
    public function setMinimumStability($minimumStability)
424
    {
425
        $this->minimumStability = $minimumStability;
426
        return $this;
427
    }
428
429
    /**
430
     * @return string
431
     */
432
    public function getMinimumStability()
433
    {
434
        return $this->minimumStability;
435
    }
436
437
    /**
438
     * @param bool $preferStable
439
     * @return $this
440
     */
441
    public function setPreferStable($preferStable)
442
    {
443
        $this->preferStable = $preferStable;
444
        return $this;
445
    }
446
447
    /**
448
     * @return bool
449
     */
450
    public function getPreferStable()
451
    {
452
        return $this->preferStable;
453
    }
454
455
    /**
456
     * @param array $stabilityOptions
457
     * @return $this
458
     */
459
    public function setStabilityOptions($stabilityOptions)
460
    {
461
        $this->stabilityOptions = $stabilityOptions;
462
        return $this;
463
    }
464
465
    /**
466
     * @return array
467
     */
468
    public function getStabilityOptions()
469
    {
470
        return $this->stabilityOptions;
471
    }
472
473
    /**
474
     * @return Client
475
     */
476
    public function getPackagistClient()
477
    {
478
        return $this->packagistClient;
479
    }
480
481
    /**
482
     * @param Client $packagistClient
483
     */
484
    public function setPackagistClient(Client $packagistClient)
485
    {
486
        $this->packagistClient = $packagistClient;
487
        return $this;
488
    }
489
490
    /**
491
     * @param ComposerLoader $composerLoader
492
     * @return $this
493
     */
494
    public function setComposerLoader(ComposerLoader $composerLoader)
495
    {
496
        $this->composerLoader = $composerLoader;
497
        return $this;
498
    }
499
500
    /**
501
     * @return ComposerLoader
502
     */
503
    public function getComposerLoader()
504
    {
505
        return $this->composerLoader;
506
    }
507
}
508