Completed
Push — master ( cd44b6...7febf5 )
by MusikAnimal
03:41 queued 01:27
created

ApiHelper   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 249
Duplicated Lines 15.26 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 0
Metric Value
wmc 31
lcom 1
cbo 9
dl 38
loc 249
rs 9.8
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A setUp() 0 7 2
A groups() 19 20 4
A globalGroups() 18 18 4
B displayTitles() 0 39 6
B massApi() 0 26 1
C massApiInternal() 0 64 13

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
<?php
2
/**
3
 * This file contains only the ApiHelper class.
4
 */
5
6
namespace AppBundle\Helper;
7
8
use Mediawiki\Api\MediawikiApi;
9
use Mediawiki\Api\SimpleRequest;
10
use Mediawiki\Api\FluentRequest;
11
use Psr\Cache\CacheItemPoolInterface;
12
use Symfony\Component\Config\Definition\Exception\Exception;
13
use Symfony\Component\DependencyInjection\ContainerInterface;
14
use Xtools\ProjectRepository;
15
16
/**
17
 * This is a helper for calling the MediaWiki API.
18
 */
19
class ApiHelper extends HelperBase
20
{
21
    /** @var MediawikiApi The API object. */
22
    private $api;
23
24
    /** @var CacheItemPoolInterface The cache. */
25
    protected $cache;
26
27
    /** @var ContainerInterface The DI container. */
28
    protected $container;
29
30
    /**
31
     * ApiHelper constructor.
32
     * @param ContainerInterface $container
33
     */
34
    public function __construct(ContainerInterface $container)
35
    {
36
        $this->container = $container;
37
        $this->cache = $container->get('cache.app');
38
    }
39
40
    /**
41
     * Set up the MediawikiApi object for the given project.
42
     *
43
     * @param string $project
44
     */
45
    private function setUp($project)
46
    {
47
        if (!$this->api instanceof MediawikiApi) {
48
            $project = ProjectRepository::getProject($project, $this->container);
49
            $this->api = $project->getApi();
50
        }
51
    }
52
53
    /**
54
     * Get the given user's groups on the given project.
55
     * @deprecated Use User::getGroups() instead.
56
     * @param string $project
57
     * @param string $username
58
     * @return string[]
59
     */
60 View Code Duplication
    public function groups($project, $username)
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...
61
    {
62
        $this->setUp($project);
63
        $params = [ "list"=>"users", "ususers"=>$username, "usprop"=>"groups" ];
64
        $query = new SimpleRequest('query', $params);
65
        $result = [];
66
67
        try {
68
            $res = $this->api->getRequest($query);
69
            if (isset($res["batchcomplete"])
70
                && isset($res["query"]["users"][0]["groups"])
71
            ) {
72
                $result = $res["query"]["users"][0]["groups"];
73
            }
74
        } catch (Exception $e) {
75
            // The api returned an error!  Ignore
76
        }
77
78
        return $result;
79
    }
80
81
    /**
82
     * Get the given user's globally-applicable groups.
83
     * @deprecated Use User::getGlobalGroups() instead.
84
     * @param string $project
85
     * @param string $username
86
     * @return string[]
87
     */
88 View Code Duplication
    public function globalGroups($project, $username)
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...
89
    {
90
        $this->setUp($project);
91
        $params = [ "meta"=>"globaluserinfo", "guiuser"=>$username, "guiprop"=>"groups" ];
92
        $query = new SimpleRequest('query', $params);
93
        $result = [];
94
95
        try {
96
            $res = $this->api->getRequest($query);
97
            if (isset($res["batchcomplete"]) && isset($res["query"]["globaluserinfo"]["groups"])) {
98
                $result = $res["query"]["globaluserinfo"]["groups"];
99
            }
100
        } catch (Exception $e) {
101
            // The api returned an error!  Ignore
102
        }
103
104
        return $result;
105
    }
106
107
    /**
108
     * Get HTML display titles of a set of pages (or the normal title if there's no display title).
109
     * This will send t/50 API requests where t is the number of titles supplied.
110
     * @param string $project The project.
111
     * @param string[] $pageTitles The titles to fetch.
112
     * @return string[] Keys are the original supplied title, and values are the display titles.
113
     */
114
    public function displayTitles($project, $pageTitles)
115
    {
116
        $this->setUp($project);
117
        $displayTitles = [];
118
        $numPages = count($pageTitles);
119
        for ($n = 0; $n < $numPages; $n += 50) {
120
            $titleSlice = array_slice($pageTitles, $n, 50);
121
            $params = [
122
                'prop' => 'info|pageprops',
123
                'inprop' => 'displaytitle',
124
                'titles' => join('|', $titleSlice),
125
            ];
126
            $query = new SimpleRequest('query', $params);
127
            $result = $this->api->postRequest($query);
128
129
            // Extract normalization info.
130
            $normalized = [];
131
            if (isset($result['query']['normalized'])) {
132
                array_map(
133
                    function ($e) use (&$normalized) {
134
                        $normalized[$e['to']] = $e['from'];
135
                    },
136
                    $result['query']['normalized']
137
                );
138
            }
139
140
            // Match up the normalized titles with the display titles and the original titles.
141
            foreach ($result['query']['pages'] as $pageInfo) {
142
                $displayTitle = isset($pageInfo['pageprops']['displaytitle'])
143
                    ? $pageInfo['pageprops']['displaytitle']
144
                    : $pageInfo['title'];
145
                $origTitle = isset($normalized[$pageInfo['title']])
146
                    ? $normalized[$pageInfo['title']] : $pageInfo['title'];
147
                $displayTitles[$origTitle] = $displayTitle;
148
            }
149
        }
150
151
        return $displayTitles;
152
    }
153
154
    /**
155
     * Make mass API requests to MediaWiki API
156
     * The API normally limits to 500 pages, but gives you a 'continue' value
157
     *   to finish iterating through the resource.
158
     * Adapted from https://github.com/MusikAnimal/pageviews
159
     * @param  array       $params        Associative array of params to pass to API
160
     * @param  string      $project       Project to query, e.g. en.wikipedia.org
161
     * @param  string|func $dataKey       The key for the main chunk of data, in the query hash
162
     *                                    (e.g. 'categorymembers' for API:Categorymembers).
163
     *                                    If this is a function it is given the response data,
164
     *                                    and expected to return the data we want to concatentate.
165
     * @param  string      [$continueKey] the key to look in the continue hash, if present
166
     *                                    (e.g. 'cmcontinue' for API:Categorymembers)
167
     * @param  integer     [$limit]       Max number of pages to fetch
168
     * @return array                      Associative array with data
169
     */
170
    public function massApi($params, $project, $dataKey, $continueKey = 'continue', $limit = 5000)
171
    {
172
        $this->setUp($project);
173
174
        // Passed by reference to massApiInternal so we can keep track of
175
        //   everything we need during the recursive calls
176
        // The magically essential part here is $data['promise'] which we'll
177
        //   wait to be resolved
178
        $data = [
179
            'params' => $params,
180
            'project' => $project,
181
            'continueKey' => $continueKey,
182
            'dataKey' => $dataKey,
183
            'limit' => $limit,
184
            'resolveData' => [
185
                'pages' => []
186
            ],
187
            'continueValue' => null,
188
            'promise' => new \GuzzleHttp\Promise\Promise(),
189
        ];
190
191
        // wait for all promises to complete, even if some of them fail
192
        \GuzzleHttp\Promise\settle($this->massApiInternal($data))->wait();
193
194
        return $data['resolveData'];
195
    }
196
197
    /**
198
     * Internal function used by massApi() to make recursive calls
199
     * @param  array &$data Everything we need to keep track of, as defined in massApi()
200
     * @return null         Nothing. $data['promise']->then is used to continue flow of
201
     *                      execution after all recursive calls are complete
202
     */
203
    private function massApiInternal(&$data)
204
    {
205
        $requestData = array_merge([
206
            'action' => 'query',
207
            'format' => 'json',
208
            'formatversion' => '2',
209
        ], $data['params']);
210
211
        if ($data['continueValue']) {
212
            $requestData[$data['continueKey']] = $data['continueValue'];
213
        }
214
215
        $query = FluentRequest::factory()->setAction('query')->setParams($requestData);
216
        $innerPromise = $this->api->getRequestAsync($query);
217
218
        $innerPromise->then(function ($result) use (&$data) {
219
            // some failures come back as 200s, so we still resolve and let the outer function handle it
220
            if (isset($result['error']) || !isset($result['query'])) {
221
                return $data['promise']->resolve($data);
222
            }
223
224
            $dataKey = $data['dataKey'];
225
            $isFinished = false;
0 ignored issues
show
Unused Code introduced by
$isFinished is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
226
227
            // allow custom function to parse the data we want, if provided
228
            if (is_callable($dataKey)) {
229
                $data['resolveData']['pages'] = array_merge(
230
                    $data['resolveData']['pages'],
231
                    $data['dataKey']($result['query'])
232
                );
233
                $isFinished = count($data['resolveData']['pages']) >= $data['limit'];
234
            } else {
235
                // append new data to data from last request. We might want both 'pages' and dataKey
236
                if (isset($result['query']['pages'])) {
237
                    $data['resolveData']['pages'] = array_merge(
238
                        $data['resolveData']['pages'],
239
                        $result['query']['pages']
240
                    );
241
                }
242
                if ($result['query'][$dataKey]) {
243
                    $newValues = isset($data['resolveData'][$dataKey]) ? $data['resolveData'][$dataKey] : [];
244
                    $data['resolveData'][$dataKey] = array_merge($newValues, $result['query'][$dataKey]);
245
                }
246
247
                // If pages is not the collection we want, it will be either an empty array or one entry with
248
                //   basic page info depending on what API we're hitting. So resolveData[dataKey] will hit the limit
249
                $isFinished = count($data['resolveData']['pages']) >= $data['limit'] ||
250
                    count($data['resolveData'][$dataKey]) >= $data['limit'];
251
            }
252
253
            // make recursive call if needed, waiting 100ms
254
            if (!$isFinished && isset($result['continue']) && isset($result['continue'][$data['continueKey']])) {
255
                usleep(100000);
256
                $data['continueValue'] = $result['continue'][$data['continueKey']];
257
                return $this->massApiInternal($data);
258
            } else {
259
                // indicate there were more entries than the limit
260
                if (isset($result['continue'])) {
261
                    $data['resolveData']['continue'] = true;
262
                }
263
                $data['promise']->resolve($data);
264
            }
265
        });
266
    }
267
}
268