Passed
Pull Request — main (#442)
by MusikAnimal
08:21 queued 04:15
created

ArticleInfoController::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 10
dl 0
loc 15
rs 10
c 0
b 0
f 0

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace App\Controller;
6
7
use App\Exception\XtoolsHttpException;
8
use App\Helper\AutomatedEditsHelper;
9
use App\Helper\I18nHelper;
10
use App\Model\ArticleInfo;
11
use App\Model\Authorship;
12
use App\Model\Page;
13
use App\Model\Project;
14
use App\Repository\ArticleInfoRepository;
15
use App\Repository\PageRepository;
16
use App\Repository\ProjectRepository;
17
use App\Repository\UserRepository;
18
use GuzzleHttp\Client;
19
use GuzzleHttp\Exception\ServerException;
20
use Psr\Cache\CacheItemPoolInterface;
21
use Psr\Container\ContainerInterface;
22
use Symfony\Component\HttpFoundation\JsonResponse;
23
use Symfony\Component\HttpFoundation\RequestStack;
24
use Symfony\Component\HttpFoundation\Response;
25
use Symfony\Component\Routing\Annotation\Route;
26
27
/**
28
 * This controller serves the search form and results for the ArticleInfo tool
29
 */
30
class ArticleInfoController extends XtoolsController
31
{
32
    protected ArticleInfo $articleInfo;
33
    protected ArticleInfoRepository $articleInfoRepo;
34
    protected AutomatedEditsHelper $autoEditsHelper;
35
36
    /**
37
     * Get the name of the tool's index route. This is also the name of the associated model.
38
     * @return string
39
     * @codeCoverageIgnore
40
     */
41
    public function getIndexRoute(): string
42
    {
43
        return 'ArticleInfo';
44
    }
45
46
    /**
47
     * @param RequestStack $requestStack
48
     * @param ContainerInterface $container
49
     * @param CacheItemPoolInterface $cache
50
     * @param Client $guzzle
51
     * @param I18nHelper $i18n
52
     * @param ProjectRepository $projectRepo
53
     * @param UserRepository $userRepo
54
     * @param PageRepository $pageRepo
55
     * @param ArticleInfoRepository $articleInfoRepo
56
     * @param AutomatedEditsHelper $autoEditsHelper
57
     */
58
    public function __construct(
59
        RequestStack $requestStack,
60
        ContainerInterface $container,
61
        CacheItemPoolInterface $cache,
62
        Client $guzzle,
63
        I18nHelper $i18n,
64
        ProjectRepository $projectRepo,
65
        UserRepository $userRepo,
66
        PageRepository $pageRepo,
67
        ArticleInfoRepository $articleInfoRepo,
68
        AutomatedEditsHelper $autoEditsHelper
69
    ) {
70
        $this->articleInfoRepo = $articleInfoRepo;
71
        $this->autoEditsHelper = $autoEditsHelper;
72
        parent::__construct($requestStack, $container, $cache, $guzzle, $i18n, $projectRepo, $userRepo, $pageRepo);
73
    }
74
75
    /**
76
     * The search form.
77
     * @Route("/articleinfo", name="ArticleInfo")
78
     * @Route("/articleinfo/index.php", name="articleInfoIndexPhp")
79
     * @Route("/articleinfo/{project}", name="ArticleInfoProject")
80
     * @return Response
81
     */
82
    public function indexAction(): Response
83
    {
84
        if (isset($this->params['project']) && isset($this->params['page'])) {
85
            return $this->redirectToRoute('ArticleInfoResult', $this->params);
86
        }
87
88
        return $this->render('articleInfo/index.html.twig', array_merge([
89
            'xtPage' => 'ArticleInfo',
90
            'xtPageTitle' => 'tool-articleinfo',
91
            'xtSubtitle' => 'tool-articleinfo-desc',
92
93
            // Defaults that will get overridden if in $params.
94
            'start' => '',
95
            'end' => '',
96
            'page' => '',
97
        ], $this->params, ['project' => $this->project]));
98
    }
99
100
    /**
101
     * Setup the ArticleInfo instance and its Repository.
102
     */
103
    private function setupArticleInfo(): void
104
    {
105
        if (isset($this->articleInfo)) {
106
            return;
107
        }
108
109
        $this->articleInfo = new ArticleInfo(
110
            $this->articleInfoRepo,
111
            $this->i18n,
112
            $this->autoEditsHelper,
113
            $this->page,
0 ignored issues
show
Bug introduced by
It seems like $this->page can also be of type null; however, parameter $page of App\Model\ArticleInfo::__construct() does only seem to accept App\Model\Page, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

113
            /** @scrutinizer ignore-type */ $this->page,
Loading history...
114
            $this->start,
115
            $this->end
116
        );
117
    }
118
119
    /**
120
     * Generate ArticleInfo gadget script for use on-wiki. This automatically points the
121
     * script to this installation's API. Pass ?uglify=1 to uglify the code.
122
     *
123
     * @Route("/articleinfo-gadget.js", name="ArticleInfoGadget")
124
     * @link https://www.mediawiki.org/wiki/XTools/ArticleInfo_gadget
125
     *
126
     * @return Response
127
     * @codeCoverageIgnore
128
     */
129
    public function gadgetAction(): Response
130
    {
131
        $rendered = $this->renderView('articleInfo/articleinfo.js.twig');
132
        $response = new Response($rendered);
133
        $response->headers->set('Content-Type', 'text/javascript');
134
        return $response;
135
    }
136
137
    /**
138
     * Display the results in given date range.
139
     * @Route(
140
     *    "/articleinfo/{project}/{page}/{start}/{end}", name="ArticleInfoResult",
141
     *     requirements={
142
     *         "page"="(.+?)(?!\/(?:|\d{4}-\d{2}-\d{2})(?:\/(|\d{4}-\d{2}-\d{2}))?)?$",
143
     *         "start"="|\d{4}-\d{2}-\d{2}",
144
     *         "end"="|\d{4}-\d{2}-\d{2}",
145
     *     },
146
     *     defaults={
147
     *         "start"=false,
148
     *         "end"=false,
149
     *     }
150
     * )
151
     * @return Response
152
     * @codeCoverageIgnore
153
     */
154
    public function resultAction(): Response
155
    {
156
        if (!$this->isDateRangeValid($this->page, $this->start, $this->end)) {
0 ignored issues
show
Bug introduced by
It seems like $this->page can also be of type null; however, parameter $page of App\Controller\ArticleIn...ler::isDateRangeValid() does only seem to accept App\Model\Page, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

156
        if (!$this->isDateRangeValid(/** @scrutinizer ignore-type */ $this->page, $this->start, $this->end)) {
Loading history...
157
            $this->addFlashMessage('notice', 'date-range-outside-revisions');
158
159
            return $this->redirectToRoute('ArticleInfo', [
160
                'project' => $this->request->get('project'),
161
            ]);
162
        }
163
164
        $this->setupArticleInfo();
165
        $this->articleInfo->prepareData();
166
167
        $maxRevisions = $this->getParameter('app.max_page_revisions');
168
169
        // Show message if we hit the max revisions.
170
        if ($this->articleInfo->tooManyRevisions()) {
171
            $this->addFlashMessage('notice', 'too-many-revisions', [
172
                $this->i18n->numberFormat($maxRevisions),
173
                $maxRevisions,
174
            ]);
175
        }
176
177
        // For when there is very old data (2001 era) which may cause miscalculations.
178
        if ($this->articleInfo->getFirstEdit()->getYear() < 2003) {
179
            $this->addFlashMessage('warning', 'old-page-notice');
180
        }
181
182
        // When all username info has been hidden (see T303724).
183
        if (0 === $this->articleInfo->getNumEditors()) {
184
            $this->addFlashMessage('warning', 'error-usernames-missing');
185
        }
186
187
        $ret = [
188
            'xtPage' => 'ArticleInfo',
189
            'xtTitle' => $this->page->getTitle(),
0 ignored issues
show
Bug introduced by
The method getTitle() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

189
            'xtTitle' => $this->page->/** @scrutinizer ignore-call */ getTitle(),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
190
            'project' => $this->project,
191
            'editorlimit' => (int)$this->request->query->get('editorlimit', 20),
192
            'botlimit' => $this->request->query->get('botlimit', 10),
193
            'pageviewsOffset' => 60,
194
            'ai' => $this->articleInfo,
195
            'showAuthorship' => Authorship::isSupportedPage($this->page) && $this->articleInfo->getNumEditors() > 0,
0 ignored issues
show
Bug introduced by
It seems like $this->page can also be of type null; however, parameter $page of App\Model\Authorship::isSupportedPage() does only seem to accept App\Model\Page, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

195
            'showAuthorship' => Authorship::isSupportedPage(/** @scrutinizer ignore-type */ $this->page) && $this->articleInfo->getNumEditors() > 0,
Loading history...
196
        ];
197
198
        // Output the relevant format template.
199
        return $this->getFormattedResponse('articleInfo/result', $ret);
200
    }
201
202
    /**
203
     * Check if there were any revisions of given page in given date range.
204
     * @param Page $page
205
     * @param false|int $start
206
     * @param false|int $end
207
     * @return bool
208
     */
209
    private function isDateRangeValid(Page $page, $start, $end): bool
210
    {
211
        return $page->getNumRevisions(null, $start, $end) > 0;
212
    }
213
214
    /************************ API endpoints ************************/
215
216
    /**
217
     * Get basic info on a given article.
218
     * @Route(
219
     *     "/api/articleinfo/{project}/{page}",
220
     *     name="ArticleInfoApiAction",
221
     *     requirements={"page"=".+"}
222
     * )
223
     * @Route("/api/page/articleinfo/{project}/{page}", requirements={"page"=".+"})
224
     * @return Response|JsonResponse
225
     * See ArticleInfoControllerTest::testArticleInfoApi()
226
     * @codeCoverageIgnore
227
     */
228
    public function articleInfoApiAction(): Response
229
    {
230
        $this->recordApiUsage('page/articleinfo');
231
232
        $this->setupArticleInfo();
233
        $data = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $data is dead and can be removed.
Loading history...
234
235
        try {
236
            $data = $this->articleInfo->getArticleInfoApiData($this->project, $this->page);
0 ignored issues
show
Bug introduced by
It seems like $this->page can also be of type null; however, parameter $page of App\Model\ArticleInfoApi::getArticleInfoApiData() does only seem to accept App\Model\Page, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

236
            $data = $this->articleInfo->getArticleInfoApiData($this->project, /** @scrutinizer ignore-type */ $this->page);
Loading history...
237
        } catch (ServerException $e) {
238
            // The Wikimedia action API can fail for any number of reasons. To our users
239
            // any ServerException means the data could not be fetched, so we capture it here
240
            // to avoid the flood of automated emails when the API goes down, etc.
241
            $data['error'] = $this->i18n->msg('api-error', [$this->project->getDomain()]);
242
        }
243
244
        if ('html' === $this->request->query->get('format')) {
245
            return $this->getApiHtmlResponse($this->project, $this->page, $data);
0 ignored issues
show
Bug introduced by
It seems like $this->page can also be of type null; however, parameter $page of App\Controller\ArticleIn...r::getApiHtmlResponse() does only seem to accept App\Model\Page, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

245
            return $this->getApiHtmlResponse($this->project, /** @scrutinizer ignore-type */ $this->page, $data);
Loading history...
246
        }
247
248
        return $this->getFormattedApiResponse($data);
249
    }
250
251
    /**
252
     * Get the Response for the HTML output of the ArticleInfo API action.
253
     * @param Project $project
254
     * @param Page $page
255
     * @param string[] $data The pre-fetched data.
256
     * @return Response
257
     * @codeCoverageIgnore
258
     */
259
    private function getApiHtmlResponse(Project $project, Page $page, array $data): Response
260
    {
261
        $response = $this->render('articleInfo/api.html.twig', [
262
            'project' => $project,
263
            'page' => $page,
264
            'data' => $data,
265
        ]);
266
267
        // All /api routes by default respond with a JSON content type.
268
        $response->headers->set('Content-Type', 'text/html');
269
270
        // This endpoint is hit constantly and user could be browsing the same page over
271
        // and over (popular noticeboard, for instance), so offload brief caching to browser.
272
        $response->setClientTtl(350);
273
274
        return $response;
275
    }
276
277
    /**
278
     * Get prose statistics for the given article.
279
     * @Route(
280
     *     "/api/page/prose/{project}/{page}",
281
     *     name="PageApiProse",
282
     *     requirements={"page"=".+"}
283
     * )
284
     * @return JsonResponse
285
     * @codeCoverageIgnore
286
     */
287
    public function proseStatsApiAction(): JsonResponse
288
    {
289
        $this->recordApiUsage('page/prose');
290
        $this->setupArticleInfo();
291
        return $this->getFormattedApiResponse($this->articleInfo->getProseStats());
292
    }
293
294
    /**
295
     * Get the page assessments of one or more pages, along with various related metadata.
296
     * @Route(
297
     *     "/api/page/assessments/{project}/{pages}",
298
     *     name="PageApiAssessments",
299
     *     requirements={"pages"=".+"}
300
     * )
301
     * @param string $pages May be multiple pages separated by pipes, e.g. Foo|Bar|Baz
302
     * @return JsonResponse
303
     * @codeCoverageIgnore
304
     */
305
    public function assessmentsApiAction(string $pages): JsonResponse
306
    {
307
        $this->recordApiUsage('page/assessments');
308
309
        $pages = explode('|', $pages);
310
        $out = [];
311
312
        foreach ($pages as $pageTitle) {
313
            try {
314
                $page = $this->validatePage($pageTitle);
315
                $assessments = $page->getProject()
316
                    ->getPageAssessments()
317
                    ->getAssessments($page);
318
319
                $out[$page->getTitle()] = $this->request->get('classonly')
320
                    ? $assessments['assessment']
321
                    : $assessments;
322
            } catch (XtoolsHttpException $e) {
323
                $out[$pageTitle] = false;
324
            }
325
        }
326
327
        return $this->getFormattedApiResponse($out);
328
    }
329
330
    /**
331
     * Get number of in and outgoing links and redirects to the given page.
332
     * @Route(
333
     *     "/api/page/links/{project}/{page}",
334
     *     name="PageApiLinks",
335
     *     requirements={"page"=".+"}
336
     * )
337
     * @return JsonResponse
338
     * @codeCoverageIgnore
339
     */
340
    public function linksApiAction(): JsonResponse
341
    {
342
        $this->recordApiUsage('page/links');
343
        return $this->getFormattedApiResponse($this->page->countLinksAndRedirects());
344
    }
345
346
    /**
347
     * Get the top editors to a page.
348
     * @Route(
349
     *     "/api/page/top_editors/{project}/{page}/{start}/{end}/{limit}", name="PageApiTopEditors",
350
     *     requirements={
351
     *         "page"="(.+?)(?!\/(?:|\d{4}-\d{2}-\d{2})(?:\/(|\d{4}-\d{2}-\d{2}))?(?:\/(\d+))?)?$",
352
     *         "start"="|\d{4}-\d{2}-\d{2}",
353
     *         "end"="|\d{4}-\d{2}-\d{2}",
354
     *         "limit"="|\d+"
355
     *     },
356
     *     defaults={
357
     *         "start"=false,
358
     *         "end"=false,
359
     *         "limit"=20,
360
     *     }
361
     * )
362
     * @return JsonResponse
363
     * @codeCoverageIgnore
364
     */
365
    public function topEditorsApiAction(): JsonResponse
366
    {
367
        $this->recordApiUsage('page/top_editors');
368
369
        $this->setupArticleInfo();
370
        $topEditors = $this->articleInfo->getTopEditorsByEditCount(
371
            (int)$this->limit,
372
            '' != $this->request->query->get('nobots')
373
        );
374
375
        return $this->getFormattedApiResponse([
376
            'top_editors' => $topEditors,
377
        ]);
378
    }
379
}
380