Passed
Pull Request — main (#442)
by MusikAnimal
08:34 queued 04:16
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
     * Get the name of the tool's index route. This is also the name of the associated model.
59
     * @return string
60
     * @codeCoverageIgnore
61
     */
62
    public function getIndexRoute(): string
63
    {
64
        return 'EditCounter';
65
    }
66
67
    /**
68
     * EditCounterController constructor.
69
     * @param RequestStack $requestStack
70
     * @param ContainerInterface $container
71
     * @param CacheItemPoolInterface $cache
72
     * @param Client $guzzle
73
     * @param I18nHelper $i18n
74
     * @param ProjectRepository $projectRepo
75
     * @param UserRepository $userRepo
76
     * @param EditCounterRepository $editCounterRepo
77
     * @param UserRightsRepository $userRightsRepo
78
     * @param PageRepository $pageRepo
79
     */
80
    public function __construct(
81
        RequestStack $requestStack,
82
        ContainerInterface $container,
83
        CacheItemPoolInterface $cache,
84
        Client $guzzle,
85
        I18nHelper $i18n,
86
        ProjectRepository $projectRepo,
87
        UserRepository $userRepo,
88
        EditCounterRepository $editCounterRepo,
89
        UserRightsRepository $userRightsRepo,
90
        PageRepository $pageRepo
91
    ) {
92
        $this->editCounterRepo = $editCounterRepo;
93
        $this->userRightsRepo = $userRightsRepo;
94
        parent::__construct($requestStack, $container, $cache, $guzzle, $i18n, $projectRepo, $userRepo, $pageRepo);
95
    }
96
97
    /**
98
     * Causes the tool to redirect to the Simple Edit Counter if the user has too high of an edit count.
99
     * @inheritDoc
100
     */
101
    public function tooHighEditCountRoute(): string
102
    {
103
        return 'SimpleEditCounterResult';
104
    }
105
106
    /**
107
     * @inheritDoc
108
     */
109
    public function tooHighEditCountActionAllowlist(): array
110
    {
111
        return ['rightsChanges'];
112
    }
113
114
    /**
115
     * @inheritDoc
116
     */
117
    public function restrictedApiActions(): array
118
    {
119
        return ['monthCountsApi', 'timecardApi'];
120
    }
121
122
    /**
123
     * Every action in this controller (other than 'index') calls this first.
124
     * If a response is returned, the calling action is expected to return it.
125
     * @throws AccessDeniedException If attempting to access internal endpoint.
126
     * @throws XtoolsHttpException If an API request to restricted endpoint when user has not opted in.
127
     * @codeCoverageIgnore
128
     */
129
    protected function setUpEditCounter(): void
130
    {
131
        // Whether we're making a subrequest (the view makes a request to another action).
132
        // Subrequests to the same controller do not re-instantiate a new controller, and hence
133
        // this flag would not be set in XtoolsController::__construct(), so we must do it here as well.
134
        $this->isSubRequest = $this->request->get('htmlonly')
135
            || null !== $this->get('request_stack')->getParentRequest();
136
137
        // Return the EditCounter if we already have one.
138
        if (isset($this->editCounter)) {
139
            return;
140
        }
141
142
        // Will redirect to Simple Edit Counter if they have too many edits, as defined self::construct.
143
        $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

143
        $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...
144
145
        // Store which sections of the Edit Counter they requested.
146
        $this->sections = $this->getRequestedSections();
147
148
        $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

148
        $this->userRights = new UserRights($this->userRightsRepo, $this->project, /** @scrutinizer ignore-type */ $this->user, $this->i18n);
Loading history...
149
150
        // Instantiate EditCounter.
151
        $this->editCounter = new EditCounter(
152
            $this->editCounterRepo,
153
            $this->i18n,
154
            $this->userRights,
155
            $this->project,
156
            $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

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

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