Completed
Pull Request — master (#24)
by
unknown
02:45
created

ApiHelper   D

Complexity

Total Complexity 80

Size/Duplication

Total Lines 558
Duplicated Lines 6.81 %

Coupling/Cohesion

Components 1
Dependencies 9

Importance

Changes 2
Bugs 1 Features 0
Metric Value
wmc 80
c 2
b 1
f 0
lcom 1
cbo 9
dl 38
loc 558
rs 4.8717

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A setUp() 0 15 4
D getSiteInfo() 0 80 14
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

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
14
class ApiHelper extends HelperBase
15
{
16
    /** @var MediawikiApi */
17
    private $api;
18
19
    /** @var LabsHelper */
20
    private $labsHelper;
21
22
    /** @var CacheItemPoolInterface */
23
    protected $cache;
24
25
    /** @var ContainerInterface */
26
    protected $container;
27
28
    public function __construct(ContainerInterface $container, LabsHelper $labsHelper)
29
    {
30
        $this->container = $container;
31
        $this->labsHelper = $labsHelper;
32
        $this->cache = $container->get('cache.app');
33
    }
34
35
    private function setUp($project)
36
    {
37
        if (!isset($this->api)) {
38
            $normalizedProject = $this->labsHelper->normalizeProject($project);
39
            $apiPath = $this->container->getParameter('api_path');
40
41
            try {
42
                $this->api = MediawikiApi::newFromApiEndpoint($normalizedProject . $apiPath);
43
            } catch (Exception $e) {
44
                // Do nothing...
45
            } catch (FatalErrorException $e) {
46
                // Do nothing...
47
            }
48
        }
49
    }
50
51
    /**
52
     * Get general siteinfo and namespaces for a project and cache it.
53
     * @param  string [$project] Base project domain with or without protocal, or database name
54
     *                           such as 'en.wikipedia.org', 'https://en.wikipedia.org' or 'enwiki'
55
     *                           Can be left blank for single wikis.
56
     * @return string[] with keys 'general' and 'namespaces'. General info will include 'dbName',
57
     *                           'wikiName', 'url', 'lang', 'articlePath', 'scriptPath',
58
     *                           'script', 'timezone', and 'timeOffset'
59
     */
60
    public function getSiteInfo($project = '')
61
    {
62
        $normalizedProject = $this->labsHelper->normalizeProject($project);
63
64
        if (!$normalizedProject) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $normalizedProject of type string|false is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
65
            throw new Exception("Unable to find project '$project'");
66
        }
67
68
        if ($this->container->getParameter('app.single_wiki')) {
69
            $project = $this->container->getParameter('wiki_url');
0 ignored issues
show
Unused Code introduced by
$project 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...
70
        } else {
71
            $project = "https://" . $normalizedProject;
0 ignored issues
show
Unused Code introduced by
$project 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...
72
        }
73
74
        $cacheKey = "siteinfo." . $normalizedProject;
75
        if ($this->cacheHas($cacheKey)) {
76
            return $this->cacheGet($cacheKey);
77
        }
78
79
        $this->setUp($normalizedProject);
80
        $params = [ 'meta'=>'siteinfo', 'siprop'=>'general|namespaces' ];
81
        $query = new SimpleRequest('query', $params);
82
83
        $result = [
84
            'general' => [],
85
            'namespaces' => []
86
        ];
87
88
        try {
89
            $res = $this->api->getRequest($query);
90
91
            if (isset($res['query']['general'])) {
92
                $info = $res['query']['general'];
93
                $result['general'] = [
94
                    'wikiName' => $info['sitename'],
95
                    'dbName' => $info['wikiid'],
96
                    'url' => $info['server'],
97
                    'lang' => $info['lang'],
98
                    'articlePath' => $info['articlepath'],
99
                    'scriptPath' => $info['scriptpath'],
100
                    'script' => $info['script'],
101
                    'timezone' => $info['timezone'],
102
                    'timeOffset' => $info['timeoffset'],
103
                ];
104
105
                if ($this->container->getParameter('app.is_labs') && substr($result['general']['dbName'], -2) != '_p') {
106
                    $result['general']['dbName'] .= '_p';
107
                }
108
            }
109
110
            if (isset($res['query']['namespaces'])) {
111
                foreach ($res['query']['namespaces'] as $namespace) {
112
                    if ($namespace['id'] < 0) {
113
                        continue;
114
                    }
115
116
                    if (isset($namespace['name'])) {
117
                        $name = $namespace['name'];
118
                    } elseif (isset($namespace['*'])) {
119
                        $name = $namespace['*'];
120
                    } else {
121
                        continue;
122
                    }
123
124
                    // FIXME: Figure out a way to i18n-ize this
125
                    if ($name === '') {
126
                        $name = 'Article';
127
                    }
128
129
                    $result['namespaces'][$namespace['id']] = $name;
130
                }
131
            }
132
133
            $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...
134
        } catch (Exception $e) {
135
            // The api returned an error!  Ignore
136
        }
137
138
        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...
139
    }
140
141 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...
142
    {
143
        $this->setUp($project);
144
        $params = [ "list"=>"users", "ususers"=>$username, "usprop"=>"groups" ];
145
        $query = new SimpleRequest('query', $params);
146
        $result = [];
147
148
        try {
149
            $res = $this->api->getRequest($query);
150
            if (isset($res["batchcomplete"]) && isset($res["query"]["users"][0]["groups"])) {
151
                $result = $res["query"]["users"][0]["groups"];
152
            }
153
        } catch (Exception $e) {
154
            // The api returned an error!  Ignore
155
        }
156
157
        return $result;
158
    }
159
160 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...
161
    {
162
        $this->setUp($project);
163
        $params = [ "meta"=>"globaluserinfo", "guiuser"=>$username, "guiprop"=>"groups" ];
164
        $query = new SimpleRequest('query', $params);
165
        $result = [];
166
167
        try {
168
            $res = $this->api->getRequest($query);
169
            if (isset($res["batchcomplete"]) && isset($res["query"]["globaluserinfo"]["groups"])) {
170
                $result = $res["query"]["globaluserinfo"]["groups"];
171
            }
172
        } catch (Exception $e) {
173
            // The api returned an error!  Ignore
174
        }
175
176
        return $result;
177
    }
178
179
    /**
180
     * Get a list of namespaces on the given project.
181
     *
182
     * @param string    $project such as en.wikipedia.org
183
     * @return string[] Array of namespace IDs (keys) to names (values).
184
     */
185
    public function namespaces($project)
186
    {
187
        return $this->getSiteInfo($project)['namespaces'];
188
    }
189
190
    public function getAdmins($project)
191
    {
192
        $params = [
193
            'list' => 'allusers',
194
            'augroup' => 'sysop|bureaucrat|steward|oversight|checkuser',
195
            'auprop' => 'groups',
196
            'aulimit' => '500',
197
        ];
198
199
        $result = [];
200
        $admins = $this->massApi($params, $project, 'allusers', 'aufrom')['allusers'];
201
202
        foreach ($admins as $admin) {
203
            $groups = [];
204
            if (in_array("sysop", $admin["groups"])) {
205
                $groups[] = "A";
206
            }
207
            if (in_array("bureaucrat", $admin["groups"])) {
208
                $groups[] = "B";
209
            }
210
            if (in_array("steward", $admin["groups"])) {
211
                $groups[] = "S" ;
212
            }
213
            if (in_array("checkuser", $admin["groups"])) {
214
                $groups[] = "CU";
215
            }
216
            if (in_array("oversight", $admin["groups"])) {
217
                $groups[] = "OS";
218
            }
219
            if (in_array("bot", $admin["groups"])) {
220
                $groups[] = "Bot";
221
            }
222
            $result[ $admin["name"] ] = [
223
                "groups" => implode('/', $groups)
224
            ];
225
        }
226
227
        return $result;
228
    }
229
230
    /**
231
     * Get basic info about a page via the API
232
     * @param  string  $project      Full domain of project (en.wikipedia.org)
233
     * @param  string  $page         Page title
234
     * @param  boolean $followRedir  Whether or not to resolve redirects
235
     * @return array   Associative array of data
236
     */
237
    public function getBasicPageInfo($project, $page, $followRedir)
238
    {
239
        $this->setUp($project);
240
241
        // @TODO: Also include 'extlinks' prop when we start checking for dead external links.
242
        $params = [
243
            'prop' => 'info|pageprops',
244
            'inprop' => 'protection|talkid|watched|watchers|notificationtimestamp|subjectid|url|readable',
245
            'converttitles' => '',
246
            // '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...
247
            // '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...
248
            'titles' => $page,
249
            'formatversion' => 2
250
            // '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...
251
        ];
252
253
        if ($followRedir) {
254
            $params['redirects'] = '';
255
        }
256
257
        $query = new SimpleRequest('query', $params);
258
        $result = [];
259
260
        try {
261
            $res = $this->api->getRequest($query);
262
            if (isset($res['query']['pages'])) {
263
                $result = $res['query']['pages'][0];
264
            }
265
        } catch (Exception $e) {
266
            // The api returned an error!  Ignore
267
        }
268
269
        return $result;
270
    }
271
272
    /**
273
     * Get HTML display titles of a set of pages (or the normal title if there's no display title).
274
     * This will send t/50 API requests where t is the number of titles supplied.
275
     * @param string $project The project.
276
     * @param string[] $pageTitles The titles to fetch.
277
     * @return string[] Keys are the original supplied title, and values are the display titles.
278
     */
279
    public function displayTitles($project, $pageTitles)
280
    {
281
        $this->setUp($project);
282
        $displayTitles = [];
283
        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...
284
            $titleSlice = array_slice($pageTitles, $n, 50);
285
            $params = [
286
                'prop' => 'info|pageprops',
287
                'inprop' => 'displaytitle',
288
                'titles' => join('|', $titleSlice),
289
            ];
290
            $query = new SimpleRequest('query', $params);
291
            $result = $this->api->getRequest($query);
292
293
            // Extract normalization info.
294
            $normalized = [];
295
            if (isset($result['query']['normalized'])) {
296
                array_map(
297
                    function ($e) use (&$normalized) {
298
                        $normalized[$e['to']] = $e['from'];
299
                    },
300
                    $result['query']['normalized']
301
                );
302
            }
303
304
            // Match up the normalized titles with the display titles and the original titles.
305
            foreach ($result['query']['pages'] as $pageInfo) {
306
                $displayTitle = isset($pageInfo['pageprops']['displaytitle'])
307
                    ? $pageInfo['pageprops']['displaytitle']
308
                    : $pageInfo['title'];
309
                $origTitle = isset($normalized[$pageInfo['title']])
310
                    ? $normalized[$pageInfo['title']] : $pageInfo['title'];
311
                $displayTitles[$origTitle] = $displayTitle;
312
            }
313
        }
314
315
        return $displayTitles;
316
    }
317
318
    /**
319
     * Get assessments of the given pages, if a supported project
320
     * @param  string       $project    Project such as en.wikipedia.org
321
     * @param  string|array $pageTitles Single page title or array of titles
322
     * @return array|null               Page assessments info or null if none found
323
     */
324
    public function getPageAssessments($project, $pageTitles)
325
    {
326
        // From config/assessments.yml
327
        $config = $this->getAssessmentsConfig();
328
329
        // return null if unsupported project
330
        if (!in_array($project, array_keys($config))) {
331
            return null;
332
        }
333
334
        $config = $config[$project];
335
336
        $params = [
337
            'prop' => 'pageassessments',
338
            'titles' => is_string($pageTitles) ? $pageTitles : implode('|', $pageTitles),
339
            'palimit' => 500,
340
        ];
341
342
        // get assessments for this page from the API
343
        $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...
344
            return isset($data['pages'][0]['pageassessments']) ? $data['pages'][0]['pageassessments'] : [];
345
        }, 'pacontinue')['pages'];
346
347
        $decoratedAssessments = [];
348
349
        // Set the default decorations for the overall quality assessment
350
        // This will be replaced with the first valid class defined for any WikiProject
351
        $overallQuality = $config['class']['Unknown'];
352
        $overallQuality['value'] = '???';
353
354
        if (empty($assessments)) {
355
            return null;
356
        }
357
358
        // loop through each assessment and decorate with colors, category URLs and images, if applicable
359
        foreach ($assessments as $wikiproject => $assessment) {
360
            $classValue = $assessment['class'];
361
362
            // Use ??? as the presented value when the class is unknown or is not defined in the config
363
            if ($classValue === 'Unknown' || $classValue === '' || !isset($config['class'][$classValue])) {
364
                $classAttrs = $config['class']['Unknown'];
365
                $assessment['class']['value'] = '???';
366
                $assessment['class']['category'] = $classAttrs['category'];
367
                $assessment['class']['badge'] = "https://upload.wikimedia.org/wikipedia/commons/". $classAttrs['badge'];
368
            } else {
369
                $classAttrs = $config['class'][$classValue];
370
                $assessment['class'] = [
371
                    'value' => $classValue,
372
                    'color' => $classAttrs['color'],
373
                    'category' => $classAttrs['category'],
374
                ];
375
376
                // add full URL to badge icon
377
                if ($classAttrs['badge'] !== '') {
378
                    $assessment['class']['badge'] = "https://upload.wikimedia.org/wikipedia/commons/" .
379
                        $classAttrs['badge'];
380
                }
381
382
                if ($overallQuality['value'] === '???') {
383
                    $overallQuality = $assessment['class'];
384
                    $overallQuality['category'] = $classAttrs['category'];
385
                }
386
            }
387
388
            $importanceValue = $assessment['importance'];
389
            $importanceUnknown = $importanceValue === 'Unknown' || $importanceValue === '';
390
391
            if ($importanceUnknown || !isset($config['importance'][$importanceValue])) {
392
                $importanceAttrs = $config['importance']['Unknown'];
393
                $assessment['importance'] = $importanceAttrs;
394
                $assessment['importance']['value'] = '???';
395
                $assessment['importance']['category'] = $importanceAttrs['category'];
396
            } else {
397
                $importanceAttrs = $config['importance'][$importanceValue];
398
                $assessment['importance'] = [
399
                    'value' => $importanceValue,
400
                    'color' => $importanceAttrs['color'],
401
                    'weight' => $importanceAttrs['weight'], // numerical weight for sorting purposes
402
                    'category' => $importanceAttrs['category'],
403
                ];
404
            }
405
406
            $decoratedAssessments[$wikiproject] = $assessment;
407
        }
408
409
        return [
410
            'assessment' => $overallQuality,
411
            'wikiprojects' => $decoratedAssessments,
412
            'wikiproject_prefix' => $config['wikiproject_prefix']
413
        ];
414
    }
415
416
    /**
417
     * Get the image URL of the badge for the given page assessment
418
     * @param  string $project Project such as en.wikipedia.org
419
     * @param  string $class   Valid classification for project, such as 'Start', 'GA', etc.
420
     * @return string          URL to image
421
     */
422
    public function getAssessmentBadgeURL($project, $class)
423
    {
424
        $config = $this->getAssessmentsConfig();
425
426
        if (isset($config[$project]['class'][$class])) {
427
            return "https://upload.wikimedia.org/wikipedia/commons/" . $config[$project]['class'][$class]['badge'];
428
        } elseif (isset($config[$project]['class']['Unknown'])) {
429
            return "https://upload.wikimedia.org/wikipedia/commons/" . $config[$project]['class']['Unknown']['badge'];
430
        } else {
431
            return "";
432
        }
433
    }
434
435
    /**
436
     * Fetch assessments data from config/assessments.yml and cache in static variable
437
     * @return array Mappings of project/quality/class with badges, colors and category links
438
     */
439
    private function getAssessmentsConfig()
440
    {
441
        static $assessmentsConfig = null;
442
        if ($assessmentsConfig === null) {
443
            $assessmentsConfig = $this->container->getParameter('assessments');
444
        }
445
        return $assessmentsConfig;
446
    }
447
448
    /**
449
     * Does the given project support page assessments?
450
     * @param  string  $project Project to query, e.g. en.wikipedia.org
451
     * @return boolean True or false
452
     */
453
    public function projectHasPageAssessments($project)
454
    {
455
        return in_array($project, array_keys($this->getAssessmentsConfig()));
456
    }
457
458
    /**
459
     * Make mass API requests to MediaWiki API
460
     * The API normally limits to 500 pages, but gives you a 'continue' value
461
     *   to finish iterating through the resource.
462
     * Adapted from https://github.com/MusikAnimal/pageviews
463
     * @param  array       $params        Associative array of params to pass to API
464
     * @param  string      $project       Project to query, e.g. en.wikipedia.org
465
     * @param  string|func $dataKey       The key for the main chunk of data, in the query hash
466
     *                                    (e.g. 'categorymembers' for API:Categorymembers).
467
     *                                    If this is a function it is given the response data,
468
     *                                    and expected to return the data we want to concatentate.
469
     * @param  string      [$continueKey] the key to look in the continue hash, if present
470
     *                                    (e.g. 'cmcontinue' for API:Categorymembers)
471
     * @param  integer     [$limit]       Max number of pages to fetch
472
     * @return array                      Associative array with data
473
     */
474
    public function massApi($params, $project, $dataKey, $continueKey = 'continue', $limit = 5000)
475
    {
476
        $this->setUp($project);
477
478
        // Passed by reference to massApiInternal so we can keep track of
479
        //   everything we need during the recursive calls
480
        // The magically essential part here is $data['promise'] which we'll
481
        //   wait to be resolved
482
        $data = [
483
            'params' => $params,
484
            'project' => $project,
485
            'continueKey' => $continueKey,
486
            'dataKey' => $dataKey,
487
            'limit' => $limit,
488
            'resolveData' => [
489
                'pages' => []
490
            ],
491
            'continueValue' => null,
492
            'promise' => new \GuzzleHttp\Promise\Promise(),
493
        ];
494
495
        // wait for all promises to complete, even if some of them fail
496
        \GuzzleHttp\Promise\settle($this->massApiInternal($data))->wait();
497
498
        return $data['resolveData'];
499
    }
500
501
    /**
502
     * Internal function used by massApi() to make recursive calls
503
     * @param  array &$data Everything we need to keep track of, as defined in massApi()
504
     * @return null         Nothing. $data['promise']->then is used to continue flow of
505
     *                      execution after all recursive calls are complete
506
     */
507
    private function massApiInternal(&$data)
508
    {
509
        $requestData = array_merge([
510
            'action' => 'query',
511
            'format' => 'json',
512
            'formatversion' => '2',
513
        ], $data['params']);
514
515
        if ($data['continueValue']) {
516
            $requestData[$data['continueKey']] = $data['continueValue'];
517
        }
518
519
        $query = FluentRequest::factory()->setAction('query')->setParams($requestData);
520
        $innerPromise = $this->api->getRequestAsync($query);
521
522
        $innerPromise->then(function ($result) use (&$data) {
523
            // some failures come back as 200s, so we still resolve and let the outer function handle it
524
            if (isset($result['error']) || !isset($result['query'])) {
525
                return $data['promise']->resolve($data);
526
            }
527
528
            $dataKey = $data['dataKey'];
529
            $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...
530
531
            // allow custom function to parse the data we want, if provided
532
            if (is_callable($dataKey)) {
533
                $data['resolveData']['pages'] = array_merge(
534
                    $data['resolveData']['pages'],
535
                    $data['dataKey']($result['query'])
536
                );
537
                $isFinished = count($data['resolveData']['pages']) >= $data['limit'];
538
            } else {
539
                // append new data to data from last request. We might want both 'pages' and dataKey
540
                if (isset($result['query']['pages'])) {
541
                    $data['resolveData']['pages'] = array_merge(
542
                        $data['resolveData']['pages'],
543
                        $result['query']['pages']
544
                    );
545
                }
546
                if ($result['query'][$dataKey]) {
547
                    $newValues = isset($data['resolveData'][$dataKey]) ? $data['resolveData'][$dataKey] : [];
548
                    $data['resolveData'][$dataKey] = array_merge($newValues, $result['query'][$dataKey]);
549
                }
550
551
                // If pages is not the collection we want, it will be either an empty array or one entry with
552
                //   basic page info depending on what API we're hitting. So resolveData[dataKey] will hit the limit
553
                $isFinished = count($data['resolveData']['pages']) >= $data['limit'] ||
554
                    count($data['resolveData'][$dataKey]) >= $data['limit'];
555
            }
556
557
            // make recursive call if needed, waiting 100ms
558
            if (!$isFinished && isset($result['continue']) && isset($result['continue'][$data['continueKey']])) {
559
                usleep(100000);
560
                $data['continueValue'] = $result['continue'][$data['continueKey']];
561
                return $this->massApiInternal($data);
562
            } else {
563
                // indicate there were more entries than the limit
564
                if ($result['continue']) {
565
                    $data['resolveData']['continue'] = true;
566
                }
567
                $data['promise']->resolve($data);
568
            }
569
        });
570
    }
571
}
572