Completed
Push — master ( 12405d...706141 )
by Alexey
01:27
created

Packagist::createPackageObject()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 50
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 50
rs 9.3333
c 0
b 0
f 0
cc 3
eloc 34
nc 2
nop 1
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
48
    /** @var ApiProvider */
49
    private $apiProvider;
50
51
    /**
52
     * Packagist constructor.
53
     *
54
     * @param ApiProvider $apiProvider
55
     */
56
    public function __construct(ApiProvider $apiProvider)
57
    {
58
        $this->apiProvider = $apiProvider;
59
    }
60
61
    /**
62
     * @param string|null $query
63
     * @param string|null $tag
64
     * @param string|null $type
65
     * @return \ArrayObject|Package[]
66
     */
67
    public function searchPackages($query = null, $tag = null, $type = null): \ArrayObject
68
    {
69
        $result = new \ArrayObject();
70
        $currentPage = 1;
71
        $request = self::API_URL_SEARCH;
72
        $attributes = [];
73
74
        if (empty($query)) {
75
            throw new PackagistException(PackagistException::E_EMPTY_SEARCH_QUERY);
76
        }
77
78
        $attributes[] = 'q=' . urlencode($query);
79
80
        if (!empty($tag)) {
81
            $attributes[] = 'tags=' . urlencode($tag);
82
        }
83
84
        if (!empty($type)) {
85
            $this->validatePackageType($type);
86
            $attributes[] = 'type=' . urlencode($type);
87
        }
88
89
        $request .= '?' . implode('&', $attributes);
90
91
        $response = $this->apiProvider->getAPIResponse($request);
92
        $this->validateResponse($response, 'results');
93
94
        if ($response['total'] == 0) {
95
            return $result;
96
        }
97
98
        $this->fillSearchResultObject($result, $response['results']);
99
100
        $totalPages = ceil((int) $response['total'] / self::API_RESULT_PER_PAGE);
101
102
        if ($totalPages === 1) {
103
            return $result;
104
        }
105
106
        do {
107
            ++$currentPage;
108
            $response = $this->apiProvider->getAPIResponse($request . '&page=' . $currentPage);
109
            $this->validateResponse($response, 'results');
110
111
            if ($response['total'] == 0) {
112
                break;
113
            }
114
115
            $this->fillSearchResultObject($result, $response['results']);
116
117
        } while ($currentPage < $totalPages);
118
119
        return $result;
120
    }
121
122
    /**
123
     * @param \ArrayObject $searchResultObject
124
     * @param array $searchResult
125
     */
126
    private function fillSearchResultObject(\ArrayObject &$searchResultObject, array $searchResult)
127
    {
128
        if (!empty($searchResult)) {
129
            foreach ($searchResult as $item) {
130
                $package = new Package();
131
                $package
132
                    ->setName($item['name'])
133
                    ->setDescription($item['description'])
134
                    ->setUrl($item['url'])
135
                    ->setRepository($item['repository'])
136
                    ->setDownloads(
137
                        new DownloadsStat($item['downloads'])
138
                    )
139
                    ->setFavers($item['favers'])
140
                ;
141
142
                $searchResultObject->offsetSet($package->getName(), $package);
143
            }
144
        }
145
    }
146
147
    /**
148
     * @param string|null $vendor
149
     * @param string|null $type
150
     * @return array
151
     */
152
    public function getPackageList($vendor = null, $type = null): array
153
    {
154
        $request = self::API_URL_LIST;
155
        $attributes = [];
156
        if (!empty($vendor)) {
157
            $attributes[] = 'vendor=' . urlencode($vendor);
158
        }
159
        if (!empty($type)) {
160
            $this->validatePackageType($type);
161
            $attributes[] = 'type=' . urlencode($type);
162
        }
163
        if (!empty($attributes)) {
164
            $request .= '?' . implode('&', $attributes);
165
        }
166
        $response = $this->apiProvider->getAPIResponse($request);
167
        $this->validateResponse($response, 'packageNames');
168
169
        return $response['packageNames'];
170
    }
171
172
    /**
173
     * @param string $packageName
174
     * @return Package
175
     */
176
    public function getPackage(string $packageName): Package
177
    {
178
        $request = sprintf(self::API_URL_PACKAGE, trim($packageName));
179
        $response = $this->apiProvider->getAPIResponse($request);
180
        $this->validateResponse($response, 'package');
181
182
        return $this->createPackageObject($response);
183
    }
184
185
    /**
186
     * @param array $packageNames
187
     * @return \ArrayObject|Package[]
188
     */
189
    public function getPackages(array $packageNames): \ArrayObject
190
    {
191
        $packages = new \ArrayObject();
192
193
        foreach ($packageNames as $packageName) {
194
            $package = $this->getPackage($packageName);
195
            $packages->offsetSet($packageName, $package);
196
        }
197
198
        return $packages;
199
    }
200
201
    /**
202
     * @param array $packageArray
203
     * @return Package
204
     */
205
    private function createPackageObject(array $packageArray): Package
206
    {
207
        $package = new Package();
208
209
        $package
210
            ->setName($packageArray['package']['name'] ?? '')
211
            ->setDescription($packageArray['package']['description'] ?? '')
212
            ->setTime($packageArray['package']['time'] ?? '')
213
            ->setMaintainers(new \ArrayObject())
214
            ->setVersions(new \ArrayObject())
215
            ->setType($packageArray['package']['type'] ?? '')
216
            ->setRepository($packageArray['package']['repository'] ?? '')
217
            ->setGithub(
218
                new GitHubStat(
219
                    (int) $packageArray['package']['github_stars'] ?? 0,
220
                    (int) $packageArray['package']['github_watchers'] ?? 0,
221
                    (int) $packageArray['package']['github_forks'] ?? 0,
222
                    (int) $packageArray['package']['github_open_issues'] ?? 0
223
                )
224
            )
225
            ->setLanguage($packageArray['package']['language'] ?? '')
226
            ->setDependents((int) $packageArray['package']['dependents'] ?? 0)
227
            ->setSuggesters((int) $packageArray['package']['suggesters'] ?? 0)
228
            ->setDownloads(
229
                new DownloadsStat(
230
                    (int) $packageArray['package']['downloads']['total'] ?? 0,
231
                    (int) $packageArray['package']['downloads']['monthly'] ?? 0,
232
                    (int) $packageArray['package']['downloads']['daily'] ?? 0
233
                )
234
            )
235
            ->setFavers((int) $packageArray['package']['favers'] ?? 0)
236
        ;
237
238
        if (!empty($packageArray['package']['maintainers'])) {
239
            foreach ($packageArray['package']['maintainers'] as $maintainer) {
240
                $package->getMaintainers()->append(
241
                    new PackageMaintainer(
242
                        $maintainer['name'] ?? '',
243
                        $maintainer['avatar_url'] ?? ''
244
                    )
245
                );
246
            }
247
        }
248
249
        $this->addPackageVersions($package, $packageArray);
250
251
        $package->setVersion($this->identifyPackageVersion($package));
252
253
        return $package;
254
    }
255
256
    /**
257
     * @param Package $package
258
     * @param array $packageArray
259
     */
260
    private function addPackageVersions(Package &$package, array $packageArray)
261
    {
262
        if (!empty($packageArray['package']['versions'])) {
263
            foreach ($packageArray['package']['versions'] as $version) {
264
                if (empty($version['version'])) {
265
                    continue;
266
                }
267
268
                $packageVersion = new PackageVersion();
269
270
                $packageVersion
271
                    ->setName($version['name'] ?? '')
272
                    ->setDescription($version['description'] ?? '')
273
                    ->setKeywords($version['keywords'] ?? [])
274
                    ->setHomepage($version['homepage'] ?? '')
275
                    ->setVersion($version['version'])
276
                    ->setVersionNormalized($version['version_normalized'] ?? '')
277
                    ->setLicense($version['license'][0] ?? '')
278
                    ->setAuthors(new \ArrayObject())
279
                    ->setSource(
280
                        new PackageSource(
281
                            $version['source']['type'] ?? '',
282
                            $version['source']['url'] ?? '',
283
                            $version['source']['reference'] ?? ''
284
                        )
285
                    )
286
                    ->setDist(
287
                        new PackageDist(
288
                            $version['dist']['type'] ?? '',
289
                            $version['dist']['url'] ?? '',
290
                            $version['dist']['reference'] ?? '',
291
                            $version['dist']['shasum'] ?? ''
292
                        )
293
                    )
294
                    ->setType($version['type'] ?? '')
295
                    ->setTime($version['time'] ?? '')
296
                    ->setAutoload($version['autoload'] ?? [])
297
                    ->setRequire(new \ArrayObject())
298
                ;
299
300
                if (!empty($version['authors'])) {
301
                    foreach ($version['authors'] as $author) {
302
                        $packageVersion->getAuthors()->append(
303
                            new PackageAuthor(
304
                                $author['name'] ?? '',
305
                                $author['email'] ?? '',
306
                                $author['homepage'] ?? '',
307
                                $author['role'] ?? ''
308
                            )
309
                        );
310
                    }
311
                }
312
313
                if (!empty($version['require'])) {
314
                    foreach ($version['require'] as $name => $ver) {
315
                        $packageVersion->getRequire()->append(new PackageDependency($name, $ver));
316
                    }
317
                }
318
319
                $package->getVersions()->offsetSet($packageVersion->getVersion(), $packageVersion);
320
            }
321
        }
322
    }
323
324
    /**
325
     * @param Package $package
326
     * @return string
327
     */
328
    private function identifyPackageVersion(Package $package): string
329
    {
330
        if (empty($package->getVersions())) {
331
            return '';
332
        }
333
334
        $currentVersion = '';
335
336
        foreach ($package->getVersions() as $version) {
337
            if (preg_match('/(dev)/i', $version->getVersion())) {
338
                continue;
339
            }
340
341
            if (preg_match('/(master)/i', $version->getVersion())) {
342
                continue;
343
            }
344
345
            if ((int) str_replace('.', '', $version->getVersion()) < (int) str_replace('.', '', $currentVersion)) {
346
                continue;
347
            }
348
349
            $currentVersion = $version->getVersion();
350
        }
351
352
        return $currentVersion;
353
    }
354
355
    /**
356
     * @param string $packageType
357
     * @throws PackagistException
358
     */
359
    private function validatePackageType(string $packageType)
360
    {
361
        if (empty($packageType) || !in_array($packageType, self::SUPPORTED_PACKAGE_TYPES)) {
362
            throw new PackagistException(
363
                PackagistException::E_UNSUPPORTED_PACKAGE_TYPE
364
            );
365
        }
366
    }
367
368
    /**
369
     * @param array $response
370
     * @param string
371
     * @return void
372
     * @throws PackagistException
373
     */
374
    private function validateResponse(array $response, string $searchKey = null)
375
    {
376
        if (isset($response['status']) && $response['status'] == 'error') {
377
            throw new PackagistException($response['message'] ?? PackagistException::E_UNKNOWN);
378
        }
379
380
        if (!empty($searchKey) && !isset($response[$searchKey])) {
381
            throw new PackagistException(
382
                PackagistException::E_RESPONSE_WITHOUT_NEEDED_KEY,
383
                ['needed_key' => $searchKey]
384
            );
385
        }
386
    }
387
}
388