StashPackageRepository::resolveComponent()   F
last analyzed

Complexity

Conditions 12
Paths 258

Size

Total Lines 81
Code Lines 46

Duplication

Lines 30
Ratio 37.04 %

Code Coverage

Tests 0
CRAP Score 156

Importance

Changes 0
Metric Value
dl 30
loc 81
ccs 0
cts 44
cp 0
rs 3.7956
c 0
b 0
f 0
cc 12
eloc 46
nc 258
nop 2
crap 156

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * Moodle component manager.
5
 *
6
 * @author Luke Carrier <[email protected]>
7
 * @copyright 2016 Luke Carrier
8
 * @license GPL-3.0+
9
 */
10
11
namespace ComponentManager\PackageRepository;
12
13
use ComponentManager\Component;
14
use ComponentManager\ComponentSource\ComponentSource;
15
use ComponentManager\ComponentSource\GitComponentSource;
16
use ComponentManager\ComponentSpecification;
17
use ComponentManager\ComponentVersion;
18
use ComponentManager\Exception\InvalidProjectException;
19
use DateTime;
20
use OutOfBoundsException;
21
use Psr\Log\LoggerInterface;
22
use stdClass;
23
use Symfony\Component\HttpFoundation\Request;
24
25
/**
26
 * Atlassian Stash project package repository.
27
 *
28
 * Requires the following configuration keys to be set for each relevant
29
 * packageRepository stanza in the project file:
30
 *
31
 * -> uri - the root URL of the Stash web UI, e.g. "http://stash.atlassian.com".
32
 * -> project - the name of the Stash project to search, e.g. "MDL".
33
 * -> authentication - the Base64-encoded representation of the user's
34
 *    "username:password" combination. You're advised to use a read only user
35
 *    with access to only the specific project, as Base64 encoded text is
36
 *    *trivial* to decode.
37
 *
38
 * The following optional configuration keys can also be configured for optimal
39
 * performance within your environment:
40
 *
41
 * -> linkOrder - the order in which different link types provided by the Stash
42
 *    REST API should be fetched. If specified and a link type is excluded from
43
 *    this list, no attempt will be made to fetch it.
44
 *
45
 * To use multiple projects, add one stanza to the configuration file for each
46
 * Stash project.
47
 */
48
class StashPackageRepository extends AbstractCachingPackageRepository
49
        implements CachingPackageRepository, PackageRepository {
50
    /**
51
     * Metadata cache filename.
52
     *
53
     * @var string
54
     */
55
    const METADATA_CACHE_FILENAME = '%s.json';
56
57
    /**
58
     * Path to the list of repositories within a project.
59
     *
60
     * @var string
61
     */
62
    const PROJECT_REPOSITORY_LIST_PATH = '/rest/api/1.0/projects/%s/repos';
63
64
    /**
65
     * Path to the list of branches within a repository.
66
     *
67
     * @var string
68
     */
69
    const REPOSITORY_BRANCHES_PATH = '/rest/api/1.0/projects/%s/repos/%s/branches';
70
71
    /**
72
     * Path to the list of tags within a repository.
73
     *
74
     * @var string
75
     */
76
    const REPOSITORY_TAGS_PATH = '/rest/api/1.0/projects/%s/repos/%s/tags';
77
78
    /**
79
     * Package cache.
80
     *
81
     * @var stdClass
82
     */
83
    protected $packageCache;
84
85
    /**
86
     * @inheritdoc PackageRepository
87
     */
88
    public function getId() {
89
        return 'Stash';
90
    }
91
92
    /**
93
     * @inheritdoc PackageRepository
94
     */
95
    public function getName() {
96
        return 'Atlassian Stash plugin repository';
97
    }
98
99
    /**
100
     * @inheritdoc PackageRepository
101
     */
102
    public function resolveComponent(ComponentSpecification $componentSpecification,
103
                                     LoggerInterface $logger) {
104
        $this->maybeLoadPackageCache();
105
106
        $componentName = $componentSpecification->getName();
107
108
        try {
109
            $packageName = $componentSpecification->getExtra('repository');
110
        } catch (OutOfBoundsException $e) {
111
            $packageName = $componentName;
112
        }
113
114
        if (!property_exists($this->packageCache, $packageName)) {
115
            throw new InvalidProjectException(
116
                    "No component named \"{$componentName}\"; seeking repository \"{$packageName}\"",
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $componentName instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $packageName instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
117
                    InvalidProjectException::CODE_MISSING_COMPONENT);
118
        }
119
        $package = $this->packageCache->{$packageName};
120
121
        /* Unfortunately Stash doesn't allow us to retrieve a list of
122
         * repositories with branches/tags included, so we'll have to
123
         * incrementally retrieve them for each component as they're
124
         * requested. */
125
126
        $packageCacheDirty = false;
127
128
        if (!property_exists($package, 'branches')) {
129
            $path = $this->getRepositoryBranchesPath($packageName);
130
131
            $this->packageCache->{$packageName}->branches
132
                    = $this->getAllPages($path);
133
            $packageCacheDirty = true;
134
        }
135
136
        if (!property_exists($package, 'tags')) {
137
            $path = $this->getRepositoryTagsPath($packageName);
138
139
            $this->packageCache->{$packageName}->tags
140
                    = $this->getAllPages($path);
141
            $packageCacheDirty = true;
142
        }
143
144
        if ($packageCacheDirty) {
145
            $this->writeMetadataCache($this->packageCache);
146
        }
147
148
        $versions = [];
149 View Code Duplication
        foreach ($package->tags as $tag) {
150
            $sources = [];
151
152
            foreach ($package->links->clone as $cloneSource) {
153
                if ($this->shouldAddComponentSource($cloneSource)) {
154
                    $sources[$cloneSource->name] = new GitComponentSource(
155
                            $cloneSource->href, $tag->displayId);
156
                }
157
            }
158
159
            $sources = $this->sortComponentSources($sources);
160
161
            $versions[] = new ComponentVersion(
162
                    null, $tag->displayId, null, $sources);
163
        }
164
165 View Code Duplication
        foreach ($package->branches as $branch) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
166
            $sources = [];
167
168
            foreach ($package->links->clone as $cloneSource) {
169
                if ($this->shouldAddComponentSource($cloneSource)) {
170
                    $sources[$cloneSource->name] = new GitComponentSource(
171
                            $cloneSource->href, $branch->displayId);
172
                }
173
            }
174
175
            $sources = $this->sortComponentSources($sources);
176
177
            $versions[] = new ComponentVersion(
178
                    null, $branch->displayId, null, $sources);
179
        }
180
181
        return new Component($componentName, $versions, $this);
182
    }
183
184
    /**
185
     * @inheritdoc PackageRepository
186
     */
187
    public function satisfiesVersion($versionSpecification, ComponentVersion $version) {
188
        return $versionSpecification === $version->getRelease();
189
    }
190
191
    /**
192
     * Determine whether to add a component source for the given clone link.
193
     *
194
     * @param stdClass $cloneSource Clone link from the Stash REST API.
195
     *
196
     * @return boolean
197
     */
198
    protected function shouldAddComponentSource(stdClass $cloneSource) {
199
        return !property_exists($this->options, 'linkOrder')
200
                || in_array($cloneSource->name, $this->options->linkOrder);
201
    }
202
203
    /**
204
     * Sort component sources by link order and strip keys.
205
     *
206
     * @param ComponentSource[] $componentSources
207
     *
208
     * @return ComponentSource[]
209
     */
210
    protected function sortComponentSources(array $componentSources) {
211
        if (property_exists($this->options, 'linkOrder')) {
212
            /* Most concise method of sorting an array by order of entries in
213
             * another array *ever*:
214
             *
215
             * http://stackoverflow.com/a/9098675 */
216
            $orderKeys        = array_flip($this->options->linkOrder);
217
            $componentSources = array_merge($orderKeys, $componentSources);
218
        }
219
220
        return array_values($componentSources);
221
    }
222
223
    /**
224
     * @inheritdoc CachingPackageRepository
225
     */
226 View Code Duplication
    public function metadataCacheLastRefreshed() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

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

Loading history...
227
        $filename = $this->getMetadataCacheFilename();
228
229
        if (!$this->filesystem->exists($filename)) {
230
            return null;
231
        }
232
233
        $time = new DateTime();
234
        $time->setTimestamp(filemtime($filename));
235
236
        return $time;
237
    }
238
239
    /**
240
     * @inheritdoc CachingPackageRepository
241
     */
242
    public function refreshMetadataCache(LoggerInterface $logger) {
243
        $path = $this->getProjectRepositoryListUrl();
244
245
        $logger->debug('Fetching metadata', [
246
            'path' => $path,
247
        ]);
248
        $rawComponents = $this->getAllPages($path);
249
250
        $logger->debug('Indexing component data');
251
        $components = new stdClass();
252
        foreach ($rawComponents as $component) {
253
            $components->{$component->slug} = $component;
254
        }
255
256
        $logger->info('Storing metadata', [
257
            'filename' => $this->getMetadataCacheFilename(),
258
        ]);
259
        $this->writeMetadataCache($components);
260
    }
261
262
    /**
263
     * Get the component metadata cache filename.
264
     *
265
     * @return string
266
     */
267
    protected function getMetadataCacheFilename() {
268
        $urlHash = parse_url($this->options->uri, PHP_URL_HOST) . '-'
269
                 . $this->options->project;
270
271
        return $this->platform->joinPaths([
272
            parent::getMetadataCacheDirectory(),
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (getMetadataCacheDirectory() instead of getMetadataCacheFilename()). Are you sure this is correct? If so, you might want to change this to $this->getMetadataCacheDirectory().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
273
            sprintf(static::METADATA_CACHE_FILENAME, $urlHash),
274
        ]);
275
    }
276
277
    /**
278
     * Load the package cache.
279
     *
280
     * @return void
281
     */
282
    protected function loadPackageCache() {
283
        $this->packageCache = json_decode(file_get_contents(
284
                $this->getMetadataCacheFilename()));
285
    }
286
287
    /**
288
     * Load the package cache (if not already loaded).
289
     *
290
     * @return void
291
     */
292
    protected function maybeLoadPackageCache() {
293
        if ($this->packageCache === null) {
294
            $this->loadPackageCache();
295
        }
296
    }
297
298
    /**
299
     * Write the metadata cache to the disk.
300
     *
301
     * @param stdClass $components
302
     *
303
     * @return void
304
     */
305
    protected function writeMetadataCache($components) {
306
        $file = $this->getMetadataCacheFilename();
307
        $this->filesystem->dumpFile($file, json_encode($components));
308
    }
309
310
    /**
311
     * Perform a GET request on a Stash path.
312
     *
313
     * @param string  $path
314
     * @param mixed[] $queryParams
315
     *
316
     * @return mixed The JSON-decoded representation of the response body.
317
     */
318
    protected function get($path, array $queryParams=[]) {
319
        $uri = $this->options->uri . $path;
320
        $uri = $this->httpClient->createUri($uri)
321
            ->withQuery(http_build_query($queryParams));
322
        $message = $this->httpClient->createRequest(Request::METHOD_GET, $uri)
323
            ->withHeader('Authorization', "Basic {$this->options->authentication}");
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $this instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
324
        $response = $this->httpClient->sendRequest($message);
325
326
        return json_decode($response->getBody());
327
    }
328
329
    /**
330
     * Get a complete result for a paged Stash REST API resource.
331
     *
332
     * @param string  $path
333
     * @param mixed[] $queryParams
334
     *
335
     * @return mixed The value attribute of the JSON-decoded response body.
336
     */
337
    protected function getAllPages($path, array $queryParams=[]) {
338
        $values = [];
339
340
        $responseBody = (object) [
341
            'limit' => 25,
342
            'start' => 0,
343
            'size'  => 0,
344
        ];
345
346
        do {
347
            $queryParams['limit'] = $responseBody->limit;
348
            $queryParams['start'] = $responseBody->start + $responseBody->size;
349
350
            $responseBody = $this->get($path, $queryParams);
351
352
            $values = array_merge($values, $responseBody->values);
353
        } while (!$responseBody->isLastPage);
354
355
        return $values;
356
    }
357
358
    /**
359
     * Get the repository list path for this Stash project.
360
     *
361
     * @return string
362
     */
363
    protected function getProjectRepositoryListUrl() {
364
        return sprintf(static::PROJECT_REPOSITORY_LIST_PATH,
365
                       $this->options->project);
366
    }
367
368
    /**
369
     * Get the branch list path for the specified repository within the project.
370
     *
371
     * @param string $componentName
372
     *
373
     * @return string
374
     */
375
    protected function getRepositoryBranchesPath($componentName) {
376
        return sprintf(static::REPOSITORY_BRANCHES_PATH, $this->options->project,
377
                       $componentName);
378
    }
379
380
    /**
381
     * Get the tag list path for the specified repository within this project.
382
     *
383
     * @param string $componentName
384
     *
385
     * @return string
386
     */
387
    protected function getRepositoryTagsPath($componentName) {
388
        return sprintf(static::REPOSITORY_TAGS_PATH, $this->options->project,
389
                       $componentName);
390
    }
391
}
392