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

EditCounterController::__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\I18nHelper;
9
use App\Model\EditCounter;
10
use App\Model\GlobalContribs;
11
use App\Model\UserRights;
12
use App\Repository\EditCounterRepository;
13
use App\Repository\EditRepository;
14
use App\Repository\GlobalContribsRepository;
15
use App\Repository\PageRepository;
16
use App\Repository\ProjectRepository;
17
use App\Repository\UserRepository;
18
use App\Repository\UserRightsRepository;
19
use GuzzleHttp\Client;
20
use Psr\Cache\CacheItemPoolInterface;
21
use Psr\Container\ContainerInterface;
22
use Symfony\Component\HttpFoundation\Cookie;
23
use Symfony\Component\HttpFoundation\JsonResponse;
24
use Symfony\Component\HttpFoundation\RedirectResponse;
25
use Symfony\Component\HttpFoundation\RequestStack;
26
use Symfony\Component\HttpFoundation\Response;
27
use Symfony\Component\Routing\Annotation\Route;
28
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
29
30
/**
31
 * Class EditCounterController
32
 */
33
class EditCounterController extends XtoolsController
34
{
35
    /**
36
     * Available statistic sections. These can be hand-picked on the index form so that you only get the data you
37
     * want and hence speed up the tool. Keys are the i18n messages (and DOM IDs), values are the action names.
38
     */
39
    private const AVAILABLE_SECTIONS = [
40
        'general-stats' => 'EditCounterGeneralStats',
41
        'namespace-totals' => 'EditCounterNamespaceTotals',
42
        'year-counts' => 'EditCounterYearCounts',
43
        'month-counts' => 'EditCounterMonthCounts',
44
        'timecard' => 'EditCounterTimecard',
45
        'top-edited-pages' => 'TopEditsResultNamespace',
46
        'rights-changes' => 'EditCounterRightsChanges',
47
    ];
48
49
    protected EditCounter $editCounter;
50
    protected EditCounterRepository $editCounterRepo;
51
    protected UserRights $userRights;
52
    protected UserRightsRepository $userRightsRepo;
53
54
    /** @var string[] Which sections to show. */
55
    protected array $sections;
56
57
    /**
58
     * @inheritDoc
59
     * @codeCoverageIgnore
60
     */
61
    public function getIndexRoute(): string
62
    {
63
        return 'EditCounter';
64
    }
65
66
    /**
67
     * EditCounterController constructor.
68
     * @param RequestStack $requestStack
69
     * @param ContainerInterface $container
70
     * @param CacheItemPoolInterface $cache
71
     * @param Client $guzzle
72
     * @param I18nHelper $i18n
73
     * @param ProjectRepository $projectRepo
74
     * @param UserRepository $userRepo
75
     * @param EditCounterRepository $editCounterRepo
76
     * @param UserRightsRepository $userRightsRepo
77
     * @param PageRepository $pageRepo
78
     */
79
    public function __construct(
80
        RequestStack $requestStack,
81
        ContainerInterface $container,
82
        CacheItemPoolInterface $cache,
83
        Client $guzzle,
84
        I18nHelper $i18n,
85
        ProjectRepository $projectRepo,
86
        UserRepository $userRepo,
87
        EditCounterRepository $editCounterRepo,
88
        UserRightsRepository $userRightsRepo,
89
        PageRepository $pageRepo
90
    ) {
91
        $this->editCounterRepo = $editCounterRepo;
92
        $this->userRightsRepo = $userRightsRepo;
93
        parent::__construct($requestStack, $container, $cache, $guzzle, $i18n, $projectRepo, $userRepo, $pageRepo);
94
    }
95
96
    /**
97
     * Causes the tool to redirect to the Simple Edit Counter if the user has too high of an edit count.
98
     * @inheritDoc
99
     * @codeCoverageIgnore
100
     */
101
    public function tooHighEditCountRoute(): string
102
    {
103
        return 'SimpleEditCounterResult';
104
    }
105
106
    /**
107
     * @inheritDoc
108
     * @codeCoverageIgnore
109
     */
110
    public function tooHighEditCountActionAllowlist(): array
111
    {
112
        return ['rightsChanges'];
113
    }
114
115
    /**
116
     * @inheritDoc
117
     * @codeCoverageIgnore
118
     */
119
    public function restrictedApiActions(): array
120
    {
121
        return ['monthCountsApi', 'timecardApi'];
122
    }
123
124
    /**
125
     * Every action in this controller (other than 'index') calls this first.
126
     * If a response is returned, the calling action is expected to return it.
127
     * @throws AccessDeniedException If attempting to access internal endpoint.
128
     * @throws XtoolsHttpException If an API request to restricted endpoint when user has not opted in.
129
     * @codeCoverageIgnore
130
     */
131
    protected function setUpEditCounter(): void
132
    {
133
        // Whether we're making a subrequest (the view makes a request to another action).
134
        // Subrequests to the same controller do not re-instantiate a new controller, and hence
135
        // this flag would not be set in XtoolsController::__construct(), so we must do it here as well.
136
        $this->isSubRequest = $this->request->get('htmlonly')
137
            || null !== $this->get('request_stack')->getParentRequest();
138
139
        // Return the EditCounter if we already have one.
140
        if (isset($this->editCounter)) {
141
            return;
142
        }
143
144
        // Will redirect to Simple Edit Counter if they have too many edits, as defined self::construct.
145
        $this->validateUser($this->user->getUsername());
0 ignored issues
show
Bug introduced by
The method getUsername() 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

145
        $this->validateUser($this->user->/** @scrutinizer ignore-call */ getUsername());

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...
146
147
        // Store which sections of the Edit Counter they requested.
148
        $this->sections = $this->getRequestedSections();
149
150
        $this->userRights = new UserRights($this->userRightsRepo, $this->project, $this->user, $this->i18n);
0 ignored issues
show
Bug introduced by
It seems like $this->user can also be of type null; however, parameter $user of App\Model\UserRights::__construct() does only seem to accept App\Model\User, 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

150
        $this->userRights = new UserRights($this->userRightsRepo, $this->project, /** @scrutinizer ignore-type */ $this->user, $this->i18n);
Loading history...
151
152
        // Instantiate EditCounter.
153
        $this->editCounter = new EditCounter(
154
            $this->editCounterRepo,
155
            $this->i18n,
156
            $this->userRights,
157
            $this->project,
158
            $this->user
0 ignored issues
show
Bug introduced by
It seems like $this->user can also be of type null; however, parameter $user of App\Model\EditCounter::__construct() does only seem to accept App\Model\User, 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

158
            /** @scrutinizer ignore-type */ $this->user
Loading history...
159
        );
160
    }
161
162
    /**
163
     * The initial GET request that displays the search form.
164
     * @Route("/ec", name="EditCounter")
165
     * @Route("/ec/index.php", name="EditCounterIndexPhp")
166
     * @Route("/ec/{project}", name="EditCounterProject")
167
     * @return RedirectResponse|Response
168
     */
169
    public function indexAction()
170
    {
171
        if (isset($this->params['project']) && isset($this->params['username'])) {
172
            return $this->redirectFromSections();
173
        }
174
175
        $this->sections = $this->getRequestedSections(true);
176
177
        // Otherwise fall through.
178
        return $this->render('editCounter/index.html.twig', [
179
            'xtPageTitle' => 'tool-editcounter',
180
            'xtSubtitle' => 'tool-editcounter-desc',
181
            'xtPage' => 'EditCounter',
182
            'project' => $this->project,
183
            'sections' => $this->sections,
184
            'availableSections' => $this->getSectionNames(),
185
            'isAllSections' => $this->sections === $this->getSectionNames(),
186
        ]);
187
    }
188
189
    /**
190
     * Get the requested sections either from the URL, cookie, or the defaults (all sections).
191
     * @param bool $useCookies Whether or not to check cookies for the preferred sections.
192
     *   This option should not be true except on the index form.
193
     * @return array|mixed|string[]
194
     * @codeCoverageIgnore
195
     */
196
    private function getRequestedSections(bool $useCookies = false)
197
    {
198
        // Happens from sub-tool index pages, e.g. see self::generalStatsIndexAction().
199
        if (isset($this->sections)) {
200
            return $this->sections;
201
        }
202
203
        // Query param for sections gets priority.
204
        $sectionsQuery = $this->request->get('sections', '');
205
206
        // If not present, try the cookie, and finally the defaults (all sections).
207
        if ($useCookies && '' == $sectionsQuery) {
208
            $sectionsQuery = $this->request->cookies->get('XtoolsEditCounterOptions', '');
209
        }
210
211
        // Either a pipe-separated string or an array.
212
        $sections = is_array($sectionsQuery) ? $sectionsQuery : explode('|', $sectionsQuery);
213
214
        // Filter out any invalid section IDs.
215
        $sections = array_filter($sections, function ($section) {
216
            return in_array($section, $this->getSectionNames());
217
        });
218
219
        // Fallback for when no valid sections were requested or provided by the cookie.
220
        if (0 === count($sections)) {
221
            $sections = $this->getSectionNames();
222
        }
223
224
        return $sections;
225
    }
226
227
    /**
228
     * Get the names of the available sections.
229
     * @return string[]
230
     * @codeCoverageIgnore
231
     */
232
    private function getSectionNames(): array
233
    {
234
        return array_keys(self::AVAILABLE_SECTIONS);
235
    }
236
237
    /**
238
     * Redirect to the appropriate action based on what sections are being requested.
239
     * @return RedirectResponse
240
     * @codeCoverageIgnore
241
     */
242
    private function redirectFromSections(): RedirectResponse
243
    {
244
        $this->sections = $this->getRequestedSections();
245
246
        if (1 === count($this->sections)) {
247
            // Redirect to dedicated route.
248
            $response = $this->redirectToRoute(self::AVAILABLE_SECTIONS[$this->sections[0]], $this->params);
249
        } elseif ($this->sections === $this->getSectionNames()) {
250
            $response = $this->redirectToRoute('EditCounterResult', $this->params);
251
        } else {
252
            // Add sections to the params, which $this->generalUrl() will append to the URL.
253
            $this->params['sections'] = implode('|', $this->sections);
254
255
            // We want a pretty URL, with pipes | instead of the encoded value %7C
256
            $url = str_replace('%7C', '|', $this->generateUrl('EditCounterResult', $this->params));
257
258
            $response = $this->redirect($url);
259
        }
260
261
        // Save the preferred sections in a cookie.
262
        $response->headers->setCookie(
263
            new Cookie('XtoolsEditCounterOptions', implode('|', $this->sections))
264
        );
265
266
        return $response;
267
    }
268
269
    /**
270
     * Display all results.
271
     * @Route(
272
     *     "/ec/{project}/{username}",
273
     *     name="EditCounterResult",
274
     *     requirements={
275
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
276
     *     }
277
     * )
278
     * @return Response|RedirectResponse
279
     * @codeCoverageIgnore
280
     */
281
    public function resultAction()
282
    {
283
        $this->setUpEditCounter();
284
285
        if (1 === count($this->sections)) {
286
            // Redirect to dedicated route.
287
            return $this->redirectToRoute(self::AVAILABLE_SECTIONS[$this->sections[0]], $this->params);
288
        }
289
290
        $ret = [
291
            'xtTitle' => $this->user->getUsername() . ' - ' . $this->project->getTitle(),
292
            'xtPage' => 'EditCounter',
293
            'user' => $this->user,
294
            'project' => $this->project,
295
            'ec' => $this->editCounter,
296
            'sections' => $this->sections,
297
            'isAllSections' => $this->sections === $this->getSectionNames(),
298
        ];
299
300
        // Used when querying for global rights changes.
301
        if ($this->getParameter('app.is_wmf')) {
302
            $ret['metaProject'] = $this->projectRepo->getProject('metawiki');
303
        }
304
305
        return $this->getFormattedResponse('editCounter/result', $ret);
306
    }
307
308
    /**
309
     * Display the general statistics section.
310
     * @Route(
311
     *     "/ec-generalstats/{project}/{username}",
312
     *     name="EditCounterGeneralStats",
313
     *     requirements={
314
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
315
     *     }
316
     * )
317
     * @param GlobalContribsRepository $globalContribsRepo
318
     * @param EditRepository $editRepo
319
     * @return Response
320
     * @codeCoverageIgnore
321
     */
322
    public function generalStatsAction(
323
        GlobalContribsRepository $globalContribsRepo,
324
        EditRepository $editRepo
325
    ): Response {
326
        $this->setUpEditCounter();
327
328
        $globalContribs = new GlobalContribs(
329
            $globalContribsRepo,
330
            $this->pageRepo,
331
            $this->userRepo,
332
            $editRepo,
333
            $this->user
0 ignored issues
show
Bug introduced by
It seems like $this->user can also be of type null; however, parameter $user of App\Model\GlobalContribs::__construct() does only seem to accept App\Model\User, 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

333
            /** @scrutinizer ignore-type */ $this->user
Loading history...
334
        );
335
        $ret = [
336
            'xtTitle' => $this->user->getUsername(),
337
            'xtPage' => 'EditCounter',
338
            'subtool_msg_key' => 'general-stats',
339
            'is_sub_request' => $this->isSubRequest,
340
            'user' => $this->user,
341
            'project' => $this->project,
342
            'ec' => $this->editCounter,
343
            'gc' => $globalContribs,
344
        ];
345
346
        // Output the relevant format template.
347
        return $this->getFormattedResponse('editCounter/general_stats', $ret);
348
    }
349
350
    /**
351
     * Search form for general stats.
352
     * @Route(
353
     *     "/ec-generalstats",
354
     *     name="EditCounterGeneralStatsIndex",
355
     *     requirements={
356
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
357
     *     }
358
     * )
359
     * @return Response
360
     */
361
    public function generalStatsIndexAction(): Response
362
    {
363
        $this->sections = ['general-stats'];
364
        return $this->indexAction();
365
    }
366
367
    /**
368
     * Display the namespace totals section.
369
     * @Route(
370
     *     "/ec-namespacetotals/{project}/{username}",
371
     *     name="EditCounterNamespaceTotals",
372
     *     requirements={
373
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
374
     *     }
375
     * )
376
     * @return Response
377
     * @codeCoverageIgnore
378
     */
379
    public function namespaceTotalsAction(): Response
380
    {
381
        $this->setUpEditCounter();
382
383
        $ret = [
384
            'xtTitle' => $this->user->getUsername(),
385
            'xtPage' => 'EditCounter',
386
            'subtool_msg_key' => 'namespace-totals',
387
            'is_sub_request' => $this->isSubRequest,
388
            'user' => $this->user,
389
            'project' => $this->project,
390
            'ec' => $this->editCounter,
391
        ];
392
393
        // Output the relevant format template.
394
        return $this->getFormattedResponse('editCounter/namespace_totals', $ret);
395
    }
396
397
    /**
398
     * Search form for namespace totals.
399
     * @Route("/ec-namespacetotals", name="EditCounterNamespaceTotalsIndex")
400
     * @return Response
401
     */
402
    public function namespaceTotalsIndexAction(): Response
403
    {
404
        $this->sections = ['namespace-totals'];
405
        return $this->indexAction();
406
    }
407
408
    /**
409
     * Display the timecard section.
410
     * @Route(
411
     *     "/ec-timecard/{project}/{username}",
412
     *     name="EditCounterTimecard",
413
     *     requirements={
414
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
415
     *     }
416
     * )
417
     * @return Response
418
     * @codeCoverageIgnore
419
     */
420
    public function timecardAction(): Response
421
    {
422
        $this->setUpEditCounter();
423
424
        $ret = [
425
            'xtTitle' => $this->user->getUsername(),
426
            'xtPage' => 'EditCounter',
427
            'subtool_msg_key' => 'timecard',
428
            'is_sub_request' => $this->isSubRequest,
429
            'user' => $this->user,
430
            'project' => $this->project,
431
            'ec' => $this->editCounter,
432
            'opted_in_page' => $this->getOptedInPage(),
433
        ];
434
435
        // Output the relevant format template.
436
        return $this->getFormattedResponse('editCounter/timecard', $ret);
437
    }
438
439
    /**
440
     * Search form for timecard.
441
     * @Route("/ec-timecard", name="EditCounterTimecardIndex")
442
     * @return Response
443
     */
444
    public function timecardIndexAction(): Response
445
    {
446
        $this->sections = ['timecard'];
447
        return $this->indexAction();
448
    }
449
450
    /**
451
     * Display the year counts section.
452
     * @Route(
453
     *     "/ec-yearcounts/{project}/{username}",
454
     *     name="EditCounterYearCounts",
455
     *     requirements={
456
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
457
     *     }
458
     * )
459
     * @return Response
460
     * @codeCoverageIgnore
461
     */
462
    public function yearCountsAction(): Response
463
    {
464
        $this->setUpEditCounter();
465
466
        $ret = [
467
            'xtTitle' => $this->user->getUsername(),
468
            'xtPage' => 'EditCounter',
469
            'subtool_msg_key' => 'year-counts',
470
            'is_sub_request' => $this->isSubRequest,
471
            'user' => $this->user,
472
            'project' => $this->project,
473
            'ec' => $this->editCounter,
474
        ];
475
476
        // Output the relevant format template.
477
        return $this->getFormattedResponse('editCounter/yearcounts', $ret);
478
    }
479
480
    /**
481
     * Search form for year counts.
482
     * @Route("/ec-yearcounts", name="EditCounterYearCountsIndex")
483
     * @return Response
484
     */
485
    public function yearCountsIndexAction(): Response
486
    {
487
        $this->sections = ['year-counts'];
488
        return $this->indexAction();
489
    }
490
491
    /**
492
     * Display the month counts section.
493
     * @Route(
494
     *     "/ec-monthcounts/{project}/{username}",
495
     *     name="EditCounterMonthCounts",
496
     *     requirements={
497
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
498
     *     }
499
     * )
500
     * @return Response
501
     * @codeCoverageIgnore
502
     */
503
    public function monthCountsAction(): Response
504
    {
505
        $this->setUpEditCounter();
506
507
        $ret = [
508
            'xtTitle' => $this->user->getUsername(),
509
            'xtPage' => 'EditCounter',
510
            'subtool_msg_key' => 'month-counts',
511
            'is_sub_request' => $this->isSubRequest,
512
            'user' => $this->user,
513
            'project' => $this->project,
514
            'ec' => $this->editCounter,
515
            'opted_in_page' => $this->getOptedInPage(),
516
        ];
517
518
        // Output the relevant format template.
519
        return $this->getFormattedResponse('editCounter/monthcounts', $ret);
520
    }
521
522
    /**
523
     * Search form for month counts.
524
     * @Route("/ec-monthcounts", name="EditCounterMonthCountsIndex")
525
     * @return Response
526
     */
527
    public function monthCountsIndexAction(): Response
528
    {
529
        $this->sections = ['month-counts'];
530
        return $this->indexAction();
531
    }
532
533
    /**
534
     * Display the user rights changes section.
535
     * @Route(
536
     *     "/ec-rightschanges/{project}/{username}",
537
     *     name="EditCounterRightsChanges",
538
     *     requirements={
539
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
540
     *     }
541
     * )
542
     * @return Response
543
     * @codeCoverageIgnore
544
     */
545
    public function rightsChangesAction(): Response
546
    {
547
        $this->setUpEditCounter();
548
549
        $ret = [
550
            'xtTitle' => $this->user->getUsername(),
551
            'xtPage' => 'EditCounter',
552
            'is_sub_request' => $this->isSubRequest,
553
            'user' => $this->user,
554
            'project' => $this->project,
555
            'ec' => $this->editCounter,
556
        ];
557
558
        if ($this->getParameter('app.is_wmf')) {
559
            $ret['metaProject'] = $this->projectRepo->getProject('metawiki');
560
        }
561
562
        // Output the relevant format template.
563
        return $this->getFormattedResponse('editCounter/rights_changes', $ret);
564
    }
565
566
    /**
567
     * Search form for rights changes.
568
     * @Route("/ec-rightschanges", name="EditCounterRightsChangesIndex")
569
     * @return Response
570
     */
571
    public function rightsChangesIndexAction(): Response
572
    {
573
        $this->sections = ['rights-changes'];
574
        return $this->indexAction();
575
    }
576
577
    /************************ API endpoints ************************/
578
579
    /**
580
     * Get various log counts for the user as JSON.
581
     * @Route(
582
     *     "/api/user/log_counts/{project}/{username}",
583
     *     name="UserApiLogCounts",
584
     *     requirements={
585
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
586
     *     }
587
     * )
588
     * @return JsonResponse
589
     * @codeCoverageIgnore
590
     */
591
    public function logCountsApiAction(): JsonResponse
592
    {
593
        $this->setUpEditCounter();
594
595
        return $this->getFormattedApiResponse([
596
            'log_counts' => $this->editCounter->getLogCounts(),
597
        ]);
598
    }
599
600
    /**
601
     * Get the namespace totals for the user as JSON.
602
     * @Route(
603
     *     "/api/user/namespace_totals/{project}/{username}",
604
     *     name="UserApiNamespaceTotals",
605
     *     requirements={
606
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
607
     *     }
608
     * )
609
     * @return JsonResponse
610
     * @codeCoverageIgnore
611
     */
612
    public function namespaceTotalsApiAction(): JsonResponse
613
    {
614
        $this->setUpEditCounter();
615
616
        return $this->getFormattedApiResponse([
617
            'namespace_totals' => (object)$this->editCounter->namespaceTotals(),
618
        ]);
619
    }
620
621
    /**
622
     * Get the month counts for the user as JSON.
623
     * @Route(
624
     *     "/api/user/month_counts/{project}/{username}",
625
     *     name="UserApiMonthCounts",
626
     *     requirements={
627
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
628
     *     }
629
     * )
630
     * @return JsonResponse
631
     * @codeCoverageIgnore
632
     */
633
    public function monthCountsApiAction(): JsonResponse
634
    {
635
        $this->setUpEditCounter();
636
637
        $ret = $this->editCounter->monthCounts();
638
639
        // Remove labels that are only needed by Twig views, and not consumers of the API.
640
        unset($ret['yearLabels']);
641
        unset($ret['monthLabels']);
642
643
        return $this->getFormattedApiResponse($ret);
644
    }
645
646
    /**
647
     * Get the timecard data as JSON.
648
     * @Route(
649
     *     "/api/user/timecard/{project}/{username}",
650
     *     name="UserApiTimeCard",
651
     *     requirements={
652
     *         "username" = "(ipr-.+\/\d+[^\/])|([^\/]+)",
653
     *     }
654
     * )
655
     * @return JsonResponse
656
     * @codeCoverageIgnore
657
     */
658
    public function timecardApiAction(): JsonResponse
659
    {
660
        $this->setUpEditCounter();
661
662
        return $this->getFormattedApiResponse([
663
            'timecard' => $this->editCounter->timeCard(),
664
        ]);
665
    }
666
}
667