Completed
Push — master ( 050bb3...76bbec )
by Sam
06:05
created

ApiHelper   C

Complexity

Total Complexity 78

Size/Duplication

Total Lines 551
Duplicated Lines 10.89 %

Coupling/Cohesion

Components 2
Dependencies 10

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 78
c 2
b 1
f 0
lcom 2
cbo 10
dl 60
loc 551
rs 5.4563

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A groups() 18 18 4
A globalGroups() 18 18 4
A namespaces() 0 4 1
C getAdmins() 0 39 8
B getBasicPageInfo() 0 34 4
B displayTitles() 0 38 6
D getPageAssessments() 0 91 14
A getAssessmentBadgeURL() 0 12 3
A getAssessmentsConfig() 0 8 2
A projectHasPageAssessments() 0 4 1
B massApi() 0 26 1
C massApiInternal() 0 64 13
A setUp() 0 7 2
D getSiteInfo() 22 76 14

How to fix   Duplicated Code    Complexity   

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:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ApiHelper 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 ApiHelper, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace AppBundle\Helper;
4
5
use DateInterval;
6
use Mediawiki\Api\MediawikiApi;
7
use Mediawiki\Api\SimpleRequest;
8
use Mediawiki\Api\FluentRequest;
9
use Psr\Cache\CacheItemPoolInterface;
10
use Symfony\Component\Config\Definition\Exception\Exception;
11
use Symfony\Component\Debug\Exception\FatalErrorException;
12
use Symfony\Component\DependencyInjection\ContainerInterface;
13
use Xtools\ProjectRepository;
14
15
class ApiHelper extends HelperBase
16
{
17
    /** @var MediawikiApi */
18
    private $api;
19
20
    /** @var LabsHelper */
21
    private $labsHelper;
22
23
    /** @var CacheItemPoolInterface */
24
    protected $cache;
25
26
    /** @var ContainerInterface */
27
    protected $container;
28
29
    public function __construct(ContainerInterface $container, LabsHelper $labsHelper)
30
    {
31
        $this->container = $container;
32
        $this->labsHelper = $labsHelper;
33
        $this->cache = $container->get('cache.app');
34
    }
35
36
    /**
37
     * Set up the MediawikiApi object for the given project.
38
     *
39
     * @param string $project
40
     */
41
    private function setUp($project)
42
    {
43
        if (!$this->api instanceof MediawikiApi) {
44
            $project = ProjectRepository::getProject($project, $this->container);
1 ignored issue
show
Compatibility introduced by
$this->container of type object<Symfony\Component...ion\ContainerInterface> is not a sub-type of object<Symfony\Component...ncyInjection\Container>. It seems like you assume a concrete implementation of the interface Symfony\Component\Depend...tion\ContainerInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
45
            $this->api = $project->getApi();
46
        }
47
    }
48
49
    /**
50
     * Get general siteinfo and namespaces for a project and cache it.
51
     * @param  string [$project] Base project domain with or without protocal, or database name
52
     *                           such as 'en.wikipedia.org', 'https://en.wikipedia.org' or 'enwiki'
53
     *                           Can be left blank for single wikis.
54
     * @return string[] with keys 'general' and 'namespaces'. General info will include 'dbName',
55
     *                           'wikiName', 'url', 'lang', 'articlePath', 'scriptPath',
56
     *                           'script', 'timezone', and 'timeOffset'
57
     */
58
    public function getSiteInfo($projectName = '')
59
    {
60
        if ($this->container->getParameter('app.single_wiki')) {
61
            $projectName = $this->container->getParameter('wiki_url');
62
        }
63
        $project = ProjectRepository::getProject($projectName, $this->container);
1 ignored issue
show
Compatibility introduced by
$this->container of type object<Symfony\Component...ion\ContainerInterface> is not a sub-type of object<Symfony\Component...ncyInjection\Container>. It seems like you assume a concrete implementation of the interface Symfony\Component\Depend...tion\ContainerInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
64
65
        if (!$project->exists()) {
66
            throw new Exception("Unable to find project '$projectName'");
67
        }
68
69
        $cacheKey = "siteinfo." . $project->getDatabaseName();
70
        if ($this->cacheHas($cacheKey)) {
71
            return $this->cacheGet($cacheKey);
72
        }
73
74
        $params = [ 'meta'=>'siteinfo', 'siprop'=>'general|namespaces' ];
75
        $query = new SimpleRequest('query', $params);
76
77
        $result = [
78
            'general' => [],
79
            'namespaces' => []
80
        ];
81
82
        try {
83
            $res = $project->getApi()->getRequest($query);
84
85
            if (isset($res['query']['general'])) {
86
                $info = $res['query']['general'];
87
                $result['general'] = [
88
                    'wikiName' => $info['sitename'],
89
                    'dbName' => $info['wikiid'],
90
                    'url' => $info['server'],
91
                    'lang' => $info['lang'],
92
                    'articlePath' => $info['articlepath'],
93
                    'scriptPath' => $info['scriptpath'],
94
                    'script' => $info['script'],
95
                    'timezone' => $info['timezone'],
96
                    'timeOffset' => $info['timeoffset'],
97
                ];
98
99
                if ($this->container->getParameter('app.is_labs') && substr($result['general']['dbName'], -2) != '_p') {
100
                    $result['general']['dbName'] .= '_p';
101
                }
102
            }
103
104 View Code Duplication
            if (isset($res['query']['namespaces'])) {
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...
105
                foreach ($res['query']['namespaces'] as $namespace) {
106
                    if ($namespace['id'] < 0) {
107
                        continue;
108
                    }
109
110
                    if (isset($namespace['name'])) {
111
                        $name = $namespace['name'];
112
                    } elseif (isset($namespace['*'])) {
113
                        $name = $namespace['*'];
114
                    } else {
115
                        continue;
116
                    }
117
118
                    // FIXME: Figure out a way to i18n-ize this
119
                    if ($name === '') {
120
                        $name = 'Article';
121
                    }
122
123
                    $result['namespaces'][$namespace['id']] = $name;
124
                }
125
            }
126
127
            $this->cacheSave($cacheKey, $result, 'P7D');
0 ignored issues
show
Documentation introduced by
$result is of type array<string,array>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
128
        } catch (Exception $e) {
129
            // The api returned an error!  Ignore
130
        }
131
132
        return $result;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $result; (array<string,array>) is incompatible with the return type documented by AppBundle\Helper\ApiHelper::getSiteInfo of type string[].

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
133
    }
134
135 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...
136
    {
137
        $this->setUp($project);
138
        $params = [ "list"=>"users", "ususers"=>$username, "usprop"=>"groups" ];
139
        $query = new SimpleRequest('query', $params);
140
        $result = [];
141
142
        try {
143
            $res = $this->api->getRequest($query);
144
            if (isset($res["batchcomplete"]) && isset($res["query"]["users"][0]["groups"])) {
145
                $result = $res["query"]["users"][0]["groups"];
146
            }
147
        } catch (Exception $e) {
148
            // The api returned an error!  Ignore
149
        }
150
151
        return $result;
152
    }
153
154 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...
155
    {
156
        $this->setUp($project);
157
        $params = [ "meta"=>"globaluserinfo", "guiuser"=>$username, "guiprop"=>"groups" ];
158
        $query = new SimpleRequest('query', $params);
159
        $result = [];
160
161
        try {
162
            $res = $this->api->getRequest($query);
163
            if (isset($res["batchcomplete"]) && isset($res["query"]["globaluserinfo"]["groups"])) {
164
                $result = $res["query"]["globaluserinfo"]["groups"];
165
            }
166
        } catch (Exception $e) {
167
            // The api returned an error!  Ignore
168
        }
169
170
        return $result;
171
    }
172
173
    /**
174
     * Get a list of namespaces on the given project.
175
     *
176
     * @param string    $project such as en.wikipedia.org
177
     * @return string[] Array of namespace IDs (keys) to names (values).
178
     */
179
    public function namespaces($project)
180
    {
181
        return $this->getSiteInfo($project)['namespaces'];
182
    }
183
184
    public function getAdmins($project)
185
    {
186
        $params = [
187
            'list' => 'allusers',
188
            'augroup' => 'sysop|bureaucrat|steward|oversight|checkuser',
189
            'auprop' => 'groups',
190
            'aulimit' => '500',
191
        ];
192
193
        $result = [];
194
        $admins = $this->massApi($params, $project, 'allusers', 'aufrom')['allusers'];
195
196
        foreach ($admins as $admin) {
197
            $groups = [];
198
            if (in_array("sysop", $admin["groups"])) {
199
                $groups[] = "A";
200
            }
201
            if (in_array("bureaucrat", $admin["groups"])) {
202
                $groups[] = "B";
203
            }
204
            if (in_array("steward", $admin["groups"])) {
205
                $groups[] = "S" ;
206
            }
207
            if (in_array("checkuser", $admin["groups"])) {
208
                $groups[] = "CU";
209
            }
210
            if (in_array("oversight", $admin["groups"])) {
211
                $groups[] = "OS";
212
            }
213
            if (in_array("bot", $admin["groups"])) {
214
                $groups[] = "Bot";
215
            }
216
            $result[ $admin["name"] ] = [
217
                "groups" => implode('/', $groups)
218
            ];
219
        }
220
221
        return $result;
222
    }
223
224
    /**
225
     * Get basic info about a page via the API
226
     * @param  string  $project      Full domain of project (en.wikipedia.org)
227
     * @param  string  $page         Page title
228
     * @param  boolean $followRedir  Whether or not to resolve redirects
229
     * @return array   Associative array of data
230
     */
231
    public function getBasicPageInfo($project, $page, $followRedir)
232
    {
233
        $this->setUp($project);
234
235
        // @TODO: Also include 'extlinks' prop when we start checking for dead external links.
236
        $params = [
237
            'prop' => 'info|pageprops',
238
            'inprop' => 'protection|talkid|watched|watchers|notificationtimestamp|subjectid|url|readable',
239
            'converttitles' => '',
240
            // 'ellimit' => 20,
1 ignored issue
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
241
            // 'elexpandurl' => '',
1 ignored issue
show
Unused Code Comprehensibility introduced by
58% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
242
            'titles' => $page,
243
            'formatversion' => 2
244
            // 'pageids' => $pageIds // FIXME: allow page IDs
1 ignored issue
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
245
        ];
246
247
        if ($followRedir) {
248
            $params['redirects'] = '';
249
        }
250
251
        $query = new SimpleRequest('query', $params);
252
        $result = [];
253
254
        try {
255
            $res = $this->api->getRequest($query);
256
            if (isset($res['query']['pages'])) {
257
                $result = $res['query']['pages'][0];
258
            }
259
        } catch (Exception $e) {
260
            // The api returned an error!  Ignore
261
        }
262
263
        return $result;
264
    }
265
266
    /**
267
     * Get HTML display titles of a set of pages (or the normal title if there's no display title).
268
     * This will send t/50 API requests where t is the number of titles supplied.
269
     * @param string $project The project.
270
     * @param string[] $pageTitles The titles to fetch.
271
     * @return string[] Keys are the original supplied title, and values are the display titles.
272
     */
273
    public function displayTitles($project, $pageTitles)
274
    {
275
        $this->setUp($project);
276
        $displayTitles = [];
277
        for ($n = 0; $n < count($pageTitles); $n += 50) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
278
            $titleSlice = array_slice($pageTitles, $n, 50);
279
            $params = [
280
                'prop' => 'info|pageprops',
281
                'inprop' => 'displaytitle',
282
                'titles' => join('|', $titleSlice),
283
            ];
284
            $query = new SimpleRequest('query', $params);
285
            $result = $this->api->getRequest($query);
286
287
            // Extract normalization info.
288
            $normalized = [];
289
            if (isset($result['query']['normalized'])) {
290
                array_map(
291
                    function ($e) use (&$normalized) {
292
                        $normalized[$e['to']] = $e['from'];
293
                    },
294
                    $result['query']['normalized']
295
                );
296
            }
297
298
            // Match up the normalized titles with the display titles and the original titles.
299
            foreach ($result['query']['pages'] as $pageInfo) {
300
                $displayTitle = isset($pageInfo['pageprops']['displaytitle'])
301
                    ? $pageInfo['pageprops']['displaytitle']
302
                    : $pageInfo['title'];
303
                $origTitle = isset($normalized[$pageInfo['title']])
304
                    ? $normalized[$pageInfo['title']] : $pageInfo['title'];
305
                $displayTitles[$origTitle] = $displayTitle;
306
            }
307
        }
308
309
        return $displayTitles;
310
    }
311
312
    /**
313
     * Get assessments of the given pages, if a supported project
314
     * @param  string       $project    Project such as en.wikipedia.org
315
     * @param  string|array $pageTitles Single page title or array of titles
316
     * @return array|null               Page assessments info or null if none found
317
     */
318
    public function getPageAssessments($project, $pageTitles)
319
    {
320
        // From config/assessments.yml
321
        $config = $this->getAssessmentsConfig();
322
323
        // return null if unsupported project
324
        if (!in_array($project, array_keys($config))) {
325
            return null;
326
        }
327
328
        $config = $config[$project];
329
330
        $params = [
331
            'prop' => 'pageassessments',
332
            'titles' => is_string($pageTitles) ? $pageTitles : implode('|', $pageTitles),
333
            'palimit' => 500,
334
        ];
335
336
        // get assessments for this page from the API
337
        $assessments = $this->massApi($params, $project, function ($data) {
0 ignored issues
show
Documentation introduced by
function ($data) { r...essments'] : array(); } is of type object<Closure>, but the function expects a string|object<AppBundle\Helper\func>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
338
            return isset($data['pages'][0]['pageassessments']) ? $data['pages'][0]['pageassessments'] : [];
339
        }, 'pacontinue')['pages'];
340
341
        $decoratedAssessments = [];
342
343
        // Set the default decorations for the overall quality assessment
344
        // This will be replaced with the first valid class defined for any WikiProject
345
        $overallQuality = $config['class']['Unknown'];
346
        $overallQuality['value'] = '???';
347
348
        if (empty($assessments)) {
349
            return null;
350
        }
351
352
        // loop through each assessment and decorate with colors, category URLs and images, if applicable
353
        foreach ($assessments as $wikiproject => $assessment) {
354
            $classValue = $assessment['class'];
355
356
            // Use ??? as the presented value when the class is unknown or is not defined in the config
357
            if ($classValue === 'Unknown' || $classValue === '' || !isset($config['class'][$classValue])) {
358
                $classAttrs = $config['class']['Unknown'];
359
                $assessment['class']['value'] = '???';
360
                $assessment['class']['category'] = $classAttrs['category'];
361
                $assessment['class']['badge'] = "https://upload.wikimedia.org/wikipedia/commons/". $classAttrs['badge'];
362
            } else {
363
                $classAttrs = $config['class'][$classValue];
364
                $assessment['class'] = [
365
                    'value' => $classValue,
366
                    'color' => $classAttrs['color'],
367
                    'category' => $classAttrs['category'],
368
                ];
369
370
                // add full URL to badge icon
371
                if ($classAttrs['badge'] !== '') {
372
                    $assessment['class']['badge'] = "https://upload.wikimedia.org/wikipedia/commons/" .
373
                        $classAttrs['badge'];
374
                }
375
376
                if ($overallQuality['value'] === '???') {
377
                    $overallQuality = $assessment['class'];
378
                    $overallQuality['category'] = $classAttrs['category'];
379
                }
380
            }
381
382
            $importanceValue = $assessment['importance'];
383
            $importanceUnknown = $importanceValue === 'Unknown' || $importanceValue === '';
384
385
            if ($importanceUnknown || !isset($config['importance'][$importanceValue])) {
386
                $importanceAttrs = $config['importance']['Unknown'];
387
                $assessment['importance'] = $importanceAttrs;
388
                $assessment['importance']['value'] = '???';
389
                $assessment['importance']['category'] = $importanceAttrs['category'];
390
            } else {
391
                $importanceAttrs = $config['importance'][$importanceValue];
392
                $assessment['importance'] = [
393
                    'value' => $importanceValue,
394
                    'color' => $importanceAttrs['color'],
395
                    'weight' => $importanceAttrs['weight'], // numerical weight for sorting purposes
396
                    'category' => $importanceAttrs['category'],
397
                ];
398
            }
399
400
            $decoratedAssessments[$wikiproject] = $assessment;
401
        }
402
403
        return [
404
            'assessment' => $overallQuality,
405
            'wikiprojects' => $decoratedAssessments,
406
            'wikiproject_prefix' => $config['wikiproject_prefix']
407
        ];
408
    }
409
410
    /**
411
     * Get the image URL of the badge for the given page assessment
412
     * @param  string $project Project such as en.wikipedia.org
413
     * @param  string $class   Valid classification for project, such as 'Start', 'GA', etc.
414
     * @return string          URL to image
415
     */
416
    public function getAssessmentBadgeURL($project, $class)
417
    {
418
        $config = $this->getAssessmentsConfig();
419
420
        if (isset($config[$project]['class'][$class])) {
421
            return "https://upload.wikimedia.org/wikipedia/commons/" . $config[$project]['class'][$class]['badge'];
422
        } elseif (isset($config[$project]['class']['Unknown'])) {
423
            return "https://upload.wikimedia.org/wikipedia/commons/" . $config[$project]['class']['Unknown']['badge'];
424
        } else {
425
            return "";
426
        }
427
    }
428
429
    /**
430
     * Fetch assessments data from config/assessments.yml and cache in static variable
431
     * @return array Mappings of project/quality/class with badges, colors and category links
432
     */
433
    private function getAssessmentsConfig()
434
    {
435
        static $assessmentsConfig = null;
436
        if ($assessmentsConfig === null) {
437
            $assessmentsConfig = $this->container->getParameter('assessments');
438
        }
439
        return $assessmentsConfig;
440
    }
441
442
    /**
443
     * Does the given project support page assessments?
444
     * @param  string  $project Project to query, e.g. en.wikipedia.org
445
     * @return boolean True or false
446
     */
447
    public function projectHasPageAssessments($project)
448
    {
449
        return in_array($project, array_keys($this->getAssessmentsConfig()));
450
    }
451
452
    /**
453
     * Make mass API requests to MediaWiki API
454
     * The API normally limits to 500 pages, but gives you a 'continue' value
455
     *   to finish iterating through the resource.
456
     * Adapted from https://github.com/MusikAnimal/pageviews
457
     * @param  array       $params        Associative array of params to pass to API
458
     * @param  string      $project       Project to query, e.g. en.wikipedia.org
459
     * @param  string|func $dataKey       The key for the main chunk of data, in the query hash
460
     *                                    (e.g. 'categorymembers' for API:Categorymembers).
461
     *                                    If this is a function it is given the response data,
462
     *                                    and expected to return the data we want to concatentate.
463
     * @param  string      [$continueKey] the key to look in the continue hash, if present
464
     *                                    (e.g. 'cmcontinue' for API:Categorymembers)
465
     * @param  integer     [$limit]       Max number of pages to fetch
466
     * @return array                      Associative array with data
467
     */
468
    public function massApi($params, $project, $dataKey, $continueKey = 'continue', $limit = 5000)
469
    {
470
        $this->setUp($project);
471
472
        // Passed by reference to massApiInternal so we can keep track of
473
        //   everything we need during the recursive calls
474
        // The magically essential part here is $data['promise'] which we'll
475
        //   wait to be resolved
476
        $data = [
477
            'params' => $params,
478
            'project' => $project,
479
            'continueKey' => $continueKey,
480
            'dataKey' => $dataKey,
481
            'limit' => $limit,
482
            'resolveData' => [
483
                'pages' => []
484
            ],
485
            'continueValue' => null,
486
            'promise' => new \GuzzleHttp\Promise\Promise(),
487
        ];
488
489
        // wait for all promises to complete, even if some of them fail
490
        \GuzzleHttp\Promise\settle($this->massApiInternal($data))->wait();
491
492
        return $data['resolveData'];
493
    }
494
495
    /**
496
     * Internal function used by massApi() to make recursive calls
497
     * @param  array &$data Everything we need to keep track of, as defined in massApi()
498
     * @return null         Nothing. $data['promise']->then is used to continue flow of
499
     *                      execution after all recursive calls are complete
500
     */
501
    private function massApiInternal(&$data)
502
    {
503
        $requestData = array_merge([
504
            'action' => 'query',
505
            'format' => 'json',
506
            'formatversion' => '2',
507
        ], $data['params']);
508
509
        if ($data['continueValue']) {
510
            $requestData[$data['continueKey']] = $data['continueValue'];
511
        }
512
513
        $query = FluentRequest::factory()->setAction('query')->setParams($requestData);
514
        $innerPromise = $this->api->getRequestAsync($query);
515
516
        $innerPromise->then(function ($result) use (&$data) {
517
            // some failures come back as 200s, so we still resolve and let the outer function handle it
518
            if (isset($result['error']) || !isset($result['query'])) {
519
                return $data['promise']->resolve($data);
520
            }
521
522
            $dataKey = $data['dataKey'];
523
            $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...
524
525
            // allow custom function to parse the data we want, if provided
526
            if (is_callable($dataKey)) {
527
                $data['resolveData']['pages'] = array_merge(
528
                    $data['resolveData']['pages'],
529
                    $data['dataKey']($result['query'])
530
                );
531
                $isFinished = count($data['resolveData']['pages']) >= $data['limit'];
532
            } else {
533
                // append new data to data from last request. We might want both 'pages' and dataKey
534
                if (isset($result['query']['pages'])) {
535
                    $data['resolveData']['pages'] = array_merge(
536
                        $data['resolveData']['pages'],
537
                        $result['query']['pages']
538
                    );
539
                }
540
                if ($result['query'][$dataKey]) {
541
                    $newValues = isset($data['resolveData'][$dataKey]) ? $data['resolveData'][$dataKey] : [];
542
                    $data['resolveData'][$dataKey] = array_merge($newValues, $result['query'][$dataKey]);
543
                }
544
545
                // If pages is not the collection we want, it will be either an empty array or one entry with
546
                //   basic page info depending on what API we're hitting. So resolveData[dataKey] will hit the limit
547
                $isFinished = count($data['resolveData']['pages']) >= $data['limit'] ||
548
                    count($data['resolveData'][$dataKey]) >= $data['limit'];
549
            }
550
551
            // make recursive call if needed, waiting 100ms
552
            if (!$isFinished && isset($result['continue']) && isset($result['continue'][$data['continueKey']])) {
553
                usleep(100000);
554
                $data['continueValue'] = $result['continue'][$data['continueKey']];
555
                return $this->massApiInternal($data);
556
            } else {
557
                // indicate there were more entries than the limit
558
                if ($result['continue']) {
559
                    $data['resolveData']['continue'] = true;
560
                }
561
                $data['promise']->resolve($data);
562
            }
563
        });
564
    }
565
}
566