RepositorySearch   A
last analyzed

Complexity

Total Complexity 32

Size/Duplication

Total Lines 273
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 32
lcom 1
cbo 8
dl 0
loc 273
rs 9.6
c 3
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 3
A enableSearchTypes() 0 8 2
A searchFully() 0 13 2
A search() 0 18 3
A searchAndDecorate() 0 16 3
B filter() 0 26 6
A getRepository() 0 4 1
A enableSearchType() 0 5 1
A disableSearchTypes() 0 8 2
A disableSearchType() 0 8 2
A decorate() 0 14 2
B decorateWithPackagistStats() 0 30 5
1
<?php
2
3
/**
4
 * This file is part of tenside/core.
5
 *
6
 * (c) Christian Schiffler <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 *
11
 * This project is provided in good faith and hope to be usable by anyone.
12
 *
13
 * @package    tenside/core
14
 * @author     Christian Schiffler <[email protected]>
15
 * @author     Nico Schneider <[email protected]>
16
 * @copyright  2015 Christian Schiffler <[email protected]>
17
 * @license    https://github.com/tenside/core/blob/master/LICENSE MIT
18
 * @link       https://github.com/tenside/core
19
 * @filesource
20
 */
21
22
namespace Tenside\Core\Composer\Search;
23
24
use Composer\IO\BufferIO;
25
use Composer\Package\PackageInterface;
26
use Composer\Repository\ComposerRepository;
27
use Composer\Repository\RepositoryInterface;
28
use Composer\Util\RemoteFilesystem;
29
use Tenside\Core\Composer\Package\VersionedPackage;
30
use Tenside\Core\Util\JsonArray;
31
32
/**
33
 * Class RepositorySearch
34
 *
35
 * @package Tenside\Composer
36
 */
37
class RepositorySearch extends AbstractSearch
38
{
39
    /**
40
     * The list of enabled search types.
41
     *
42
     * @var array
43
     */
44
    protected $enabledSearchTypes = [
45
        RepositoryInterface::SEARCH_NAME,
46
        RepositoryInterface::SEARCH_FULLTEXT
47
    ];
48
49
    /**
50
     * The repository to search on.
51
     *
52
     * @var RepositoryInterface
53
     */
54
    protected $repository;
55
56
    /**
57
     * Base url for obtaining meta data (i.e. "https://packagist.org/packages/").
58
     *
59
     * @var string|null
60
     */
61
    private $decorateBaseUrl;
62
63
    /**
64
     * Create a new instance.
65
     *
66
     * @param RepositoryInterface $repository
67
     */
68
    public function __construct(RepositoryInterface $repository)
69
    {
70
        $this->repository      = $repository;
71
        $this->decorateBaseUrl = null;
72
        if ($this->repository instanceof ComposerRepository) {
73
            $repoConfig = $this->repository->getRepoConfig();
74
            if (!preg_match('{^[\w.]+\??://}', $repoConfig['url'])) {
75
                // assume https as the default protocol
76
                $repoConfig['url'] = 'https://' . $repoConfig['url'];
77
            }
78
            $this->decorateBaseUrl = rtrim($repoConfig['url'], '/') . '/packages/%1$s.json';
79
        }
80
    }
81
82
    /**
83
     * {@inheritDoc}
84
     */
85
    public function searchFully($keywords, $filters = [])
86
    {
87
        $results = [];
88
89
        foreach ($this->enabledSearchTypes as $searchType) {
90
            $results = array_merge(
91
                $results,
92
                $this->repository->search($keywords, $searchType)
93
            );
94
        }
95
96
        return $this->filter($this->normalizeResultSet($results), $filters);
97
    }
98
99
    /**
100
     * {@inheritDoc}
101
     */
102
    public function search($keywords, $filters = [])
103
    {
104
        $results = [];
105
106
        foreach ($this->enabledSearchTypes as $searchType) {
107
            $results = array_merge(
108
                $results,
109
                $this->filter($this->normalizeResultSet($this->repository->search($keywords, $searchType)), $filters)
110
            );
111
112
            if (count($results) >= $this->getSatisfactionThreshold()) {
113
                $results = array_slice($results, 0, $this->getSatisfactionThreshold());
114
                break;
115
            }
116
        }
117
118
        return array_values($results);
119
    }
120
121
    /**
122
     * {@inheritDoc}
123
     */
124
    public function searchAndDecorate($keywords, $filters = [])
125
    {
126
        $results = $this->search($keywords, $filters);
127
128
        $decorated = [];
129
130
        foreach ($results as $packageName) {
131
            try {
132
                $decorated[] = $this->decorate($packageName);
133
            } catch (\InvalidArgumentException $exception) {
134
                // Ignore the exception as some repositories return names they do not contain (i.e. replaced packages).
135
            }
136
        }
137
138
        return $decorated;
139
    }
140
141
    /**
142
     * Filter the passed list of package names.
143
     *
144
     * @param string[]   $packageNames The package names.
145
     *
146
     * @param \Closure[] $filters      The filters to apply.
147
     *
148
     * @return string[]
149
     */
150
    protected function filter($packageNames, $filters)
151
    {
152
        if (empty($filters)) {
153
            return $packageNames;
154
        }
155
156
        $packages = [];
157
        foreach ($packageNames as $packageName) {
158
            if (count($package = $this->repository->findPackages($packageName)) > 0) {
159
                foreach ($filters as $filter) {
160
                    $package = array_filter($package, $filter);
161
                }
162
                if ($package = current($package)) {
163
                    $packages[$packageName] = $package;
164
                }
165
            }
166
        }
167
168
        return array_map(
169
            function ($package) {
170
                /** @var PackageInterface $package */
171
                return $package->getName();
172
            },
173
            $packages
174
        );
175
    }
176
177
    /**
178
     * Decorate a package.
179
     *
180
     * @param string $packageName The name of the package to decorate.
181
     *
182
     * @return VersionedPackage
183
     *
184
     * @throws \InvalidArgumentException When the package could not be found.
185
     */
186
    protected function decorate($packageName)
187
    {
188
        $results = $this->repository->findPackages($packageName);
189
190
        if (!count($results)) {
191
            throw new \InvalidArgumentException('Could not find package with specified name ' . $packageName);
192
        }
193
194
        $latest   = array_slice($results, 0, 1)[0];
195
        $versions = array_slice($results, 1);
196
        $package  = new VersionedPackage($latest, $versions);
197
198
        return $this->decorateWithPackagistStats($package);
199
    }
200
201
    /**
202
     * Decorate the package with stats from packagist.
203
     *
204
     * @param VersionedPackage $package The package version.
205
     *
206
     * @return VersionedPackage
207
     */
208
    protected function decorateWithPackagistStats(VersionedPackage $package)
209
    {
210
        if (null === $this->decorateBaseUrl) {
211
            return $package;
212
        }
213
214
        $rfs        = new RemoteFilesystem(new BufferIO());
215
        $requestUrl = sprintf($this->decorateBaseUrl, $package->getName());
216
        if (!($jsonData = $rfs->getContents($requestUrl, $requestUrl))) {
217
            $this->decorateBaseUrl = null;
218
            return $package;
219
        }
220
        try {
221
            $data = new JsonArray($jsonData);
222
        } catch (\RuntimeException $exception) {
223
            $this->decorateBaseUrl = null;
224
            return  $package;
225
        }
226
227
        $metaPaths = [
228
            'downloads' => 'package/downloads/total',
229
            'favers'    => 'package/favers'
230
        ];
231
232
        foreach ($metaPaths as $metaKey => $metaPath) {
233
            $package->addMetaData($metaKey, $data->get($metaPath));
234
        }
235
236
        return $package;
237
    }
238
239
    /**
240
     * Retrieve the composite repository.
241
     *
242
     * @return RepositoryInterface
243
     */
244
    public function getRepository()
245
    {
246
        return $this->repository;
247
    }
248
249
    /**
250
     * Set the enabled search types.
251
     *
252
     * @param int[] $searchTypes The list of search types to enable.
253
     *
254
     * @return $this
255
     */
256
    public function enableSearchTypes($searchTypes)
257
    {
258
        foreach ((array) $searchTypes as $searchType) {
259
            $this->enableSearchType($searchType);
260
        }
261
262
        return $this;
263
    }
264
265
    /**
266
     * Enable a search type.
267
     *
268
     * @param int $searchType The search type to enable.
269
     *
270
     * @return $this
271
     */
272
    public function enableSearchType($searchType)
273
    {
274
        $this->enabledSearchTypes[] = $searchType;
275
        return $this;
276
    }
277
278
    /**
279
     * Disable the passed search types.
280
     *
281
     * @param int[] $searchTypes The search types to disable.
282
     *
283
     * @return $this
284
     */
285
    public function disableSearchTypes($searchTypes)
286
    {
287
        foreach ((array) $searchTypes as $searchType) {
288
            $this->disableSearchType($searchType);
289
        }
290
291
        return $this;
292
    }
293
294
    /**
295
     * Disable a search type.
296
     *
297
     * @param int $searchType The search type to disable.
298
     *
299
     * @return $this
300
     */
301
    public function disableSearchType($searchType)
302
    {
303
        if (($key = array_search($searchType, $this->enabledSearchTypes)) !== false) {
304
            unset($this->enabledSearchTypes[$key]);
305
        }
306
307
        return $this;
308
    }
309
}
310