Packagist   B
last analyzed

Complexity

Total Complexity 50

Size/Duplication

Total Lines 418
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 50
lcom 1
cbo 11
dl 0
loc 418
rs 8.6206
c 1
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
B searchPackages() 0 54 8
A fillSearchResultObject() 0 20 3
A getPackageList() 0 19 4
A getPackage() 0 8 1
B getPackages() 0 29 4
A createPackageUrl() 0 4 1
A createPackageUrls() 0 9 2
A createPackageObject() 0 50 3
A addPackageVersions() 0 14 4
B createPackageVersion() 0 55 5
B identifyPackageVersion() 0 26 6
A validatePackageType() 0 8 3
B validateResponse() 0 13 5

How to fix   Complexity   

Complex Class

Complex classes like Packagist often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Packagist, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * This file is part of the wow-apps/symfony-packagist project
4
 * https://github.com/wow-apps/symfony-packagist
5
 *
6
 * (c) 2017 WoW-Apps
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace WowApps\PackagistBundle\Service;
13
14
use WowApps\PackagistBundle\DTO\DownloadsStat;
15
use WowApps\PackagistBundle\DTO\GitHubStat;
16
use WowApps\PackagistBundle\DTO\Package;
17
use WowApps\PackagistBundle\DTO\PackageAuthor;
18
use WowApps\PackagistBundle\DTO\PackageDependency;
19
use WowApps\PackagistBundle\DTO\PackageDist;
20
use WowApps\PackagistBundle\DTO\PackageMaintainer;
21
use WowApps\PackagistBundle\DTO\PackageSource;
22
use WowApps\PackagistBundle\DTO\PackageVersion;
23
use WowApps\PackagistBundle\Exception\PackagistException;
24
25
/**
26
 * Class Packagist
27
 *
28
 * @author Alexey Samara <[email protected]>
29
 * @package wow-apps/symfony-packagist
30
 */
31
class Packagist
32
{
33
    const API_URL = 'https://packagist.org';
34
    const API_URL_LIST = self::API_URL . '/packages/list.json';
35
    const API_URL_SEARCH = self::API_URL . '/search.json';
36
    const API_URL_PACKAGE = self::API_URL . '/packages/%s.json';
37
    const API_RESULT_PER_PAGE = 15;
38
    const SUPPORTED_PACKAGE_TYPES = [
39
        'symfony-bundle',
40
        'wordpress-plugin',
41
        'typo3-cms-extension',
42
        'library',
43
        'project',
44
        'metapackage',
45
        'composer-plugin'
46
    ];
47
    const PACKAGE_TYPE_SYMFONY = 'symfony-bundle';
48
    const PACKAGE_TYPE_WORDPRESS = 'wordpress-plugin';
49
    const PACKAGE_TYPE_TYPO3 = 'typo3-cms-extension';
50
    const PACKAGE_TYPE_LIBRARY = 'library';
51
    const PACKAGE_TYPE_PROJECT = 'project';
52
    const PACKAGE_TYPE_METAPACKAGE = 'metapackage';
53
    const PACKAGE_TYPE_COMPOSER = 'composer-plugin';
54
55
    /** @var ApiProvider */
56
    private $apiProvider;
57
58
    /**
59
     * Packagist constructor.
60
     *
61
     * @param ApiProvider $apiProvider
62
     */
63
    public function __construct(ApiProvider $apiProvider)
64
    {
65
        $this->apiProvider = $apiProvider;
66
    }
67
68
    /**
69
     * @param string $query
70
     * @param string|null $tag
71
     * @param string|null $type
72
     * @return \ArrayObject|Package[]
73
     */
74
    public function searchPackages(string $query, $tag = null, $type = null): \ArrayObject
75
    {
76
        $result = new \ArrayObject();
77
        $currentPage = 1;
78
        $request = self::API_URL_SEARCH;
79
        $attributes = [];
80
81
        if (empty($query)) {
82
            throw new PackagistException(PackagistException::E_EMPTY_SEARCH_QUERY);
83
        }
84
85
        $attributes[] = 'q=' . urlencode($query);
86
87
        if (!empty($tag)) {
88
            $attributes[] = 'tags=' . urlencode($tag);
89
        }
90
91
        if (!empty($type)) {
92
            $this->validatePackageType($type);
93
            $attributes[] = 'type=' . urlencode($type);
94
        }
95
96
        $request .= '?' . implode('&', $attributes);
97
98
        $response = $this->apiProvider->getAPIResponse($request);
99
        $this->validateResponse($response, 'results');
100
101
        if ($response['total'] == 0) {
102
            return $result;
103
        }
104
105
        $this->fillSearchResultObject($result, $response['results']);
106
107
        $totalPages = ceil((int) $response['total'] / self::API_RESULT_PER_PAGE);
108
109
        if ($totalPages === 1) {
110
            return $result;
111
        }
112
113
        do {
114
            ++$currentPage;
115
            $response = $this->apiProvider->getAPIResponse($request . '&page=' . $currentPage);
116
            $this->validateResponse($response, 'results');
117
118
            if ($response['total'] == 0) {
119
                break;
120
            }
121
122
            $this->fillSearchResultObject($result, $response['results']);
123
124
        } while ($currentPage < $totalPages);
125
126
        return $result;
127
    }
128
129
    /**
130
     * @param \ArrayObject $searchResultObject
131
     * @param array $searchResult
132
     */
133
    private function fillSearchResultObject(\ArrayObject &$searchResultObject, array $searchResult)
134
    {
135
        if (!empty($searchResult)) {
136
            foreach ($searchResult as $item) {
137
                $package = new Package();
138
                $package
139
                    ->setName($item['name'])
140
                    ->setDescription($item['description'])
141
                    ->setUrl($item['url'])
142
                    ->setRepository($item['repository'])
143
                    ->setDownloads(
144
                        new DownloadsStat($item['downloads'])
145
                    )
146
                    ->setFavers($item['favers'])
147
                ;
148
149
                $searchResultObject->offsetSet($package->getName(), $package);
150
            }
151
        }
152
    }
153
154
    /**
155
     * @param string|null $vendor
156
     * @param string|null $type
157
     * @return array
158
     */
159
    public function getPackageList($vendor = null, $type = null): array
160
    {
161
        $request = self::API_URL_LIST;
162
        $attributes = [];
163
        if (!empty($vendor)) {
164
            $attributes[] = 'vendor=' . urlencode($vendor);
165
        }
166
        if (!empty($type)) {
167
            $this->validatePackageType($type);
168
            $attributes[] = 'type=' . urlencode($type);
169
        }
170
        if (!empty($attributes)) {
171
            $request .= '?' . implode('&', $attributes);
172
        }
173
        $response = $this->apiProvider->getAPIResponse($request);
174
        $this->validateResponse($response, 'packageNames');
175
176
        return $response['packageNames'];
177
    }
178
179
    /**
180
     * @param string $packageName
181
     * @return Package
182
     */
183
    public function getPackage(string $packageName): Package
184
    {
185
        $requestUrl = $this->createPackageUrl($packageName);
186
        $response = $this->apiProvider->getAPIResponse($requestUrl);
187
        $this->validateResponse($response, 'package');
188
189
        return $this->createPackageObject($response);
190
    }
191
192
    /**
193
     * @param array $packageNames
194
     * @param bool $multiple
195
     * @param int $concurrency
196
     * @return \ArrayObject|Package[]
197
     */
198
    public function getPackages(
199
        array $packageNames,
200
        bool $multiple = true,
201
        int $concurrency = ApiProvider::POOL_CONCURRENCY
202
    ): \ArrayObject {
203
        $packages = new \ArrayObject();
204
205
        if (!$multiple) {
206
            foreach ($packageNames as $packageName) {
207
                $package = $this->getPackage($packageName);
208
                $packages->offsetSet($packageName, $package);
209
            }
210
211
            return $packages;
212
        }
213
214
        $poolResult = $this->apiProvider->getBatchAPIResponse(
215
            $this->createPackageUrls($packageNames),
216
            $concurrency
217
        );
218
219
        foreach ($poolResult as $json) {
220
            $this->validateResponse($json, 'package');
221
            $package = $this->createPackageObject($json);
222
            $packages->offsetSet($package->getName(), $package);
223
        }
224
225
        return $packages;
226
    }
227
228
    /**
229
     * @param string $packageName
230
     * @return string
231
     */
232
    private function createPackageUrl(string $packageName): string
233
    {
234
        return sprintf(self::API_URL_PACKAGE, trim($packageName));
235
    }
236
237
    /**
238
     * @param array $packageNames
239
     * @return array
240
     */
241
    private function createPackageUrls(array $packageNames): array
242
    {
243
        $urls = [];
244
        foreach ($packageNames as $packageName) {
245
            $urls[] = $this->createPackageUrl($packageName);
246
        }
247
248
        return $urls;
249
    }
250
251
    /**
252
     * @param array $packageArray
253
     * @return Package
254
     */
255
    private function createPackageObject(array $packageArray): Package
256
    {
257
        $package = new Package();
258
259
        $package
260
            ->setName($packageArray['package']['name'] ?? '')
261
            ->setDescription($packageArray['package']['description'] ?? '')
262
            ->setTime($packageArray['package']['time'] ?? '')
263
            ->setMaintainers(new \ArrayObject())
264
            ->setVersions(new \ArrayObject())
265
            ->setType($packageArray['package']['type'] ?? '')
266
            ->setRepository($packageArray['package']['repository'] ?? '')
267
            ->setGithub(
268
                new GitHubStat(
269
                    (int) $packageArray['package']['github_stars'] ?? 0,
270
                    (int) $packageArray['package']['github_watchers'] ?? 0,
271
                    (int) $packageArray['package']['github_forks'] ?? 0,
272
                    (int) $packageArray['package']['github_open_issues'] ?? 0
273
                )
274
            )
275
            ->setLanguage($packageArray['package']['language'] ?? '')
276
            ->setDependents((int) $packageArray['package']['dependents'] ?? 0)
277
            ->setSuggesters((int) $packageArray['package']['suggesters'] ?? 0)
278
            ->setDownloads(
279
                new DownloadsStat(
280
                    (int) $packageArray['package']['downloads']['total'] ?? 0,
281
                    (int) $packageArray['package']['downloads']['monthly'] ?? 0,
282
                    (int) $packageArray['package']['downloads']['daily'] ?? 0
283
                )
284
            )
285
            ->setFavers((int) $packageArray['package']['favers'] ?? 0)
286
        ;
287
288
        if (!empty($packageArray['package']['maintainers'])) {
289
            foreach ($packageArray['package']['maintainers'] as $maintainer) {
290
                $package->getMaintainers()->append(
291
                    new PackageMaintainer(
292
                        $maintainer['name'] ?? '',
293
                        $maintainer['avatar_url'] ?? ''
294
                    )
295
                );
296
            }
297
        }
298
299
        $this->addPackageVersions($package, $packageArray);
300
301
        $package->setVersion($this->identifyPackageVersion($package));
302
303
        return $package;
304
    }
305
306
    /**
307
     * @param Package $package
308
     * @param array $packageArray
309
     */
310
    private function addPackageVersions(Package &$package, array $packageArray)
311
    {
312
        if (!empty($packageArray['package']['versions'])) {
313
            foreach ($packageArray['package']['versions'] as $version) {
314
                if (empty($version['version'])) {
315
                    continue;
316
                }
317
318
                $packageVersion = $this->createPackageVersion($version);
319
320
                $package->getVersions()->offsetSet($packageVersion->getVersion(), $packageVersion);
321
            }
322
        }
323
    }
324
325
    /**
326
     * @param array $version
327
     * @return PackageVersion
328
     */
329
    private function createPackageVersion(array  $version): PackageVersion
330
    {
331
        $packageVersion = new PackageVersion();
332
333
        $packageVersion
334
            ->setName($version['name'] ?? '')
335
            ->setDescription($version['description'] ?? '')
336
            ->setKeywords($version['keywords'] ?? [])
337
            ->setHomepage($version['homepage'] ?? '')
338
            ->setVersion($version['version'])
339
            ->setVersionNormalized($version['version_normalized'] ?? '')
340
            ->setLicense($version['license'][0] ?? '')
341
            ->setAuthors(new \ArrayObject())
342
            ->setSource(
343
                new PackageSource(
344
                    $version['source']['type'] ?? '',
345
                    $version['source']['url'] ?? '',
346
                    $version['source']['reference'] ?? ''
347
                )
348
            )
349
            ->setDist(
350
                new PackageDist(
351
                    $version['dist']['type'] ?? '',
352
                    $version['dist']['url'] ?? '',
353
                    $version['dist']['reference'] ?? '',
354
                    $version['dist']['shasum'] ?? ''
355
                )
356
            )
357
            ->setType($version['type'] ?? '')
358
            ->setTime($version['time'] ?? '')
359
            ->setAutoload($version['autoload'] ?? [])
360
            ->setRequire(new \ArrayObject())
361
        ;
362
363
        if (!empty($version['authors'])) {
364
            foreach ($version['authors'] as $author) {
365
                $packageVersion->getAuthors()->append(
366
                    new PackageAuthor(
367
                        $author['name'] ?? '',
368
                        $author['email'] ?? '',
369
                        $author['homepage'] ?? '',
370
                        $author['role'] ?? ''
371
                    )
372
                );
373
            }
374
        }
375
376
        if (!empty($version['require'])) {
377
            foreach ($version['require'] as $name => $ver) {
378
                $packageVersion->getRequire()->append(new PackageDependency($name, $ver));
379
            }
380
        }
381
382
        return $packageVersion;
383
    }
384
385
    /**
386
     * @param Package $package
387
     * @return string
388
     */
389
    private function identifyPackageVersion(Package $package): string
390
    {
391
        if (empty($package->getVersions())) {
392
            return '';
393
        }
394
395
        $currentVersion = '';
396
397
        foreach ($package->getVersions() as $version) {
398
            if (preg_match('/(dev)/i', $version->getVersion())) {
399
                continue;
400
            }
401
402
            if (preg_match('/(master)/i', $version->getVersion())) {
403
                continue;
404
            }
405
406
            if ((int) str_replace('.', '', $version->getVersion()) < (int) str_replace('.', '', $currentVersion)) {
407
                continue;
408
            }
409
410
            $currentVersion = $version->getVersion();
411
        }
412
413
        return $currentVersion;
414
    }
415
416
    /**
417
     * @param string $packageType
418
     * @throws PackagistException
419
     */
420
    private function validatePackageType(string $packageType)
421
    {
422
        if (empty($packageType) || !in_array($packageType, self::SUPPORTED_PACKAGE_TYPES)) {
423
            throw new PackagistException(
424
                PackagistException::E_UNSUPPORTED_PACKAGE_TYPE
425
            );
426
        }
427
    }
428
429
    /**
430
     * @param array $response
431
     * @param string
432
     * @return void
433
     * @throws PackagistException
434
     */
435
    private function validateResponse(array $response, string $searchKey = null)
436
    {
437
        if (isset($response['status']) && $response['status'] == 'error') {
438
            throw new PackagistException($response['message'] ?? PackagistException::E_UNKNOWN);
439
        }
440
441
        if (!empty($searchKey) && !isset($response[$searchKey])) {
442
            throw new PackagistException(
443
                PackagistException::E_RESPONSE_WITHOUT_NEEDED_KEY,
444
                ['needed_key' => $searchKey]
445
            );
446
        }
447
    }
448
}
449