GithubRepository::getDepBowerJson()   B
last analyzed

Complexity

Conditions 7
Paths 19

Size

Total Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 32
rs 8.4746
c 0
b 0
f 0
cc 7
nc 19
nop 1
1
<?php
2
3
namespace Bowerphp\Repository;
4
5
use Github\Client;
6
use Github\ResultPager;
7
use RuntimeException;
8
use vierbergenlars\SemVer\expression;
9
use vierbergenlars\SemVer\SemVerException;
10
use vierbergenlars\SemVer\version;
11
12
/**
13
 * GithubRepository
14
 */
15
class GithubRepository implements RepositoryInterface
16
{
17
    /**
18
     * @var string
19
     */
20
    protected $url;
21
22
    /**
23
     * @var array
24
     */
25
    protected $tag = ['name' => null];
26
27
    /**
28
     * @var Client
29
     */
30
    protected $githubClient;
31
32
    /**
33
     * {@inheritdoc}
34
     *
35
     * @return GithubRepository
36
     */
37
    public function setUrl($url, $raw = true)
38
    {
39
        $url = preg_replace('/\.git$/', '', str_replace('git://', 'https://' . ($raw ? 'raw.' : ''), $url));
40
        $this->url = str_replace('raw.github.com', 'raw.githubusercontent.com', $url);
41
42
        return $this;
43
    }
44
45
    /**
46
     * {@inheritdoc}
47
     */
48
    public function getUrl()
49
    {
50
        return $this->url;
51
    }
52
53
    /**
54
     * @param  Client           $githubClient
55
     * @return GithubRepository
56
     */
57
    public function setHttpClient(Client $githubClient)
58
    {
59
        $this->githubClient = $githubClient;
60
        // see https://developer.github.com/changes/2015-04-17-preview-repository-redirects/
61
        $this->githubClient->getHttpClient()->setHeaders(['Accept' => 'application/vnd.github.quicksilver-preview+json']);
62
63
        return $this;
64
    }
65
66
    /**
67
     * {@inheritdoc}
68
     */
69
    public function getBower($version = 'master', $includeHomepage = false, $url = '')
70
    {
71
        if ('*' == $version) {
72
            $version = 'master';
73
        }
74
        if (!empty($url)) {
75
            // we need to save current $this->url
76
            $oldUrl = $this->url;
77
            // then, we call setUrl(), to get the http url
78
            $this->setUrl($url);
79
        }
80
        $json = $this->getDepBowerJson($version);
81
        if ($includeHomepage) {
82
            $array = json_decode($json, true);
83
            if (!empty($url)) {
84
                // here, we set again original $this->url, to pass it in bower.json
85
                $this->setUrl($oldUrl);
0 ignored issues
show
Bug introduced by
The variable $oldUrl does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
86
            }
87
            $array['homepage'] = $this->url;
88
            $json = json_encode($array, JSON_PRETTY_PRINT);
89
        }
90
91
        return $json;
92
    }
93
94
    /**
95
     * {@inheritdoc}
96
     */
97
    public function findPackage($rawCriteria = '*')
0 ignored issues
show
Complexity introduced by
This operation has 480 execution paths which exceeds the configured maximum of 200.

A high number of execution paths generally suggests many nested conditional statements and make the code less readible. This can usually be fixed by splitting the method into several smaller methods.

You can also find more information in the “Code” section of your repository.

Loading history...
98
    {
99
        list($repoUser, $repoName) = explode('/', $this->clearGitURL($this->url));
100
        $paginator = new ResultPager($this->githubClient);
101
        $tags = $paginator->fetchAll($this->githubClient->api('repo'), 'tags', [$repoUser, $repoName]);
102
103
        // edge case: package has no tags
104
        if (0 === count($tags)) {
105
            $this->tag['name'] = 'master';
106
107
            return $this->tag['name'];
108
        }
109
110
        // edge case: user asked for latest package
111
        if ('latest' == $rawCriteria || '*' == $rawCriteria || empty($rawCriteria)) {
112
            $sortedTags = $this->sortTags($tags);
113
            $this->tag = end($sortedTags);
114
115
            return $this->tag['name'];
116
        }
117
118
        // edge case for versions with slash (like ckeditor). See also issue #120
119
        if (strpos($rawCriteria, '/') > 0) {
120
            $tagNames = array_column($tags, 'name');
121
            if (false !== $tag = array_search($rawCriteria, $tagNames, true)) {
122
                $this->tag = $tag;
0 ignored issues
show
Documentation Bug introduced by
It seems like $tag of type * is incompatible with the declared type array of property $tag.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
123
124
                return $rawCriteria;
125
            }
126
        }
127
128
        try {
129
            $criteria = new expression($rawCriteria);
130
        } catch (SemVerException $sve) {
131
            throw new RuntimeException(sprintf('Criteria %s is not valid.', $rawCriteria), self::INVALID_CRITERIA, $sve);
132
        }
133
        $sortedTags = $this->sortTags($tags);
134
135
        // Yes, the php-semver lib does offer a maxSatisfying() method similar the code below.
136
        // We're not using it because it will throw an exception on what it considers to be an
137
        // "invalid" candidate version, and not continue checking the rest of the candidates.
138
        // So, even if it's faster than this code, it's not a complete solution.
139
        $matches = array_filter($sortedTags, function ($tag) use ($criteria) {
140
            $candidate = $tag['parsed_version'];
141
142
            return $criteria->satisfiedBy($candidate) ? $tag : false;
143
        });
144
145
        // If the array has elements, the LAST element is the best (highest numbered) version.
146
        if (count($matches) > 0) {
147
            // @todo Get rid of this side effect?
148
            $this->tag = array_pop($matches);
149
150
            return $this->tag['name'];
151
        }
152
153
        throw new RuntimeException(sprintf('%s: No suitable version for %s was found.', $repoName, $rawCriteria), self::VERSION_NOT_FOUND);
154
    }
155
156
    /**
157
     * {@inheritdoc}
158
     */
159
    public function getRelease($type = 'zip')
160
    {
161
        list($repoUser, $repoName) = explode('/', $this->clearGitURL($this->url));
162
163
        return $this->githubClient->api('repo')->contents()->archive($repoUser, $repoName, $type . 'ball', $this->tag['name']);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Github\Api\ApiInterface as the method contents() does only exist in the following implementations of said interface: Github\Api\Repo.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
164
    }
165
166
    /**
167
     * {@inheritdoc}
168
     */
169
    public function getTags()
170
    {
171
        list($repoUser, $repoName) = explode('/', $this->clearGitURL($this->url));
172
        $paginator = new ResultPager($this->githubClient);
173
        $tags = $paginator->fetchAll($this->githubClient->api('repo'), 'tags', [$repoUser, $repoName]);
174
        // edge case: no tags
175
        if (0 === count($tags)) {
176
            return [];
177
        }
178
179
        $sortedTags = $this->sortTags($tags);  // Filters out bad tag specs
180
181
        return array_keys($sortedTags);
182
    }
183
184
    /**
185
     * Get remote bower.json file (or package.json file)
186
     *
187
     * @param  string $version
188
     * @return string
189
     */
190
    private function getDepBowerJson($version)
191
    {
192
        list($repoUser, $repoName) = explode('/', $this->clearGitURL($this->url));
193
        $contents = $this->githubClient->api('repo')->contents();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Github\Api\ApiInterface as the method contents() does only exist in the following implementations of said interface: Github\Api\Repo.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
194
        if ($contents->exists($repoUser, $repoName, 'bower.json', $version)) {
195
            $json = $contents->download($repoUser, $repoName, 'bower.json', $version);
196
        } else {
197
            $isPackageJson = true;
198
            if ($contents->exists($repoUser, $repoName, 'package.json', $version)) {
199
                $json = $contents->download($repoUser, $repoName, 'package.json', $version);
200
            } elseif ('master' != $version) {
201
                return $this->getDepBowerJson('master');
202
            }
203
            // try anyway. E.g. exists() return false for Modernizr, but then it downloads :-|
204
            $json = $contents->download($repoUser, $repoName, 'package.json', $version);
205
        }
206
207
        if ("\xef\xbb\xbf" == substr($json, 0, 3)) {
208
            $json = substr($json, 3);
209
        }
210
211
        // for package.json, remove dependencies (see the case of Modernizr)
212
        if (isset($isPackageJson)) {
213
            $array = json_decode($json, true);
214
            if (isset($array['dependencies'])) {
215
                unset($array['dependencies']);
216
            }
217
            $json = json_encode($array, JSON_PRETTY_PRINT);
218
        }
219
220
        return $json;
221
    }
222
223
    /**
224
     * @param string $url
225
     *
226
     * @return string
227
     */
228
    private function clearGitURL($url)
229
    {
230
        $partsToClean = [
231
            'git://',
232
            '[email protected]:',
233
            'https://',
234
            'github.com/',
235
            'raw.githubusercontent.com/',
236
        ];
237
        foreach ($partsToClean as $part) {
238
            $url = str_replace($part, '', $url);
239
        }
240
241
        if ('.git' == substr($url, -4)) {
242
            $url = substr($url, 0, -4);
243
        }
244
245
        return $url;
246
    }
247
248
    /**
249
     * Why do we have to do this? Your guess is as good as mine.
250
     * The only flaw I've seen in the semver lib we're using,
251
     * and the regex's in there are too complicated to mess with.
252
     *
253
     * @param string $rawValue
254
     *
255
     * @return string
256
     */
257
    private function fixupRawTag($rawValue)
258
    {
259
        if (0 === strpos($rawValue, 'v')) {
260
            $rawValue = substr($rawValue, 1);
261
        }
262
        // WHY NOT SCRUB OUT PLUS SIGNS, RIGHT?
263
        $foundIt = strpos($rawValue, '+');
264
        if (false !== $foundIt) {
265
            $rawValue = substr($rawValue, 0, $foundIt);
266
        }
267
        $rawValue = strtr($rawValue, ['.alpha' => '-alpha', '.beta' => '-beta', '.dev' => '-dev']);
268
        $pieces = explode('.', $rawValue);
269
        $count = count($pieces);
270
        if (0 == $count) {
271
            $pieces[] = '0';
272
            $count = 1;
273
        }
274
        for ($add = $count; $add < 3; ++$add) {
275
            $pieces[] = '0';
276
        }
277
        $return = implode('.', array_slice($pieces, 0, 3));
278
279
        return $return;
280
    }
281
282
    /**
283
     * @param array $tags
284
     * @param bool  $excludeUnstables
285
     *
286
     * @return array
287
     */
288
    private function sortTags(array $tags, $excludeUnstables = true)
289
    {
290
        $return = [];
291
292
        // Don't include invalid tags
293
        foreach ($tags as $tag) {
294
            try {
295
                $fixedName = $this->fixupRawTag($tag['name']);
296
                $v = new version($fixedName);
297
                if ($v->valid()) {
298
                    $version = $v->getVersion();
299
                    if ($excludeUnstables && $this->isNotStable($v)) {
300
                        continue;
301
                    }
302
                    $tag['parsed_version'] = $v;
303
                    $return[$version] = $tag;
304
                }
305
            } catch (\Exception $ex) {
306
                // Skip
307
            }
308
        }
309
310
        uasort($return, function ($a, $b) {
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $a. Configured minimum length is 2.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
Comprehensibility introduced by
Avoid variables with short names like $b. Configured minimum length is 2.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
311
            return version::compare($a['parsed_version'], $b['parsed_version']);
312
        });
313
314
        return $return;
315
    }
316
317
    /**
318
     * @param version $version
319
     *
320
     * @return bool
321
     */
322
    private function isNotStable(version $version)
323
    {
324
        return count($version->getPrerelease()) > 0;
325
    }
326
}
327