Passed
Push — develop ( d3c53a...e28085 )
by nguereza
14:20
created

BaseAction   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 674
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 206
c 1
b 0
f 0
dl 0
loc 674
rs 3.6
wmc 60

23 Methods

Rating   Name   Duplication   Size   Complexity  
A restFormValidationErrorResponse() 0 9 1
A addContext() 0 5 1
A redirect() 0 16 3
C setFilters() 0 61 13
A handleFilterDefault() 0 2 1
A setSorts() 0 17 5
A restErrorResponse() 0 13 1
A restServerErrorResponse() 0 9 1
A restBadRequestErrorResponse() 0 9 1
A viewResponse() 0 38 4
B setPagination() 0 34 9
A parseForeignConstraintErrorMessage() 0 27 3
A __construct() 0 13 1
A restNotFoundErrorResponse() 0 9 1
A setFields() 0 10 3
A setView() 0 5 1
A handle() 0 11 1
A addContexts() 0 7 2
A redirectBackToOrigin() 0 15 3
A restConflictErrorResponse() 0 9 1
A getIgnoreDateFilters() 0 3 1
A restResponse() 0 17 2
A addSidebar() 0 10 1

How to fix   Complexity   

Complex Class

Complex classes like BaseAction 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.

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 BaseAction, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Platine Framework
5
 *
6
 * Platine Framework is a lightweight, high-performance, simple and elegant
7
 * PHP Web framework
8
 *
9
 * This content is released under the MIT License (MIT)
10
 *
11
 * Copyright (c) 2020 Platine Framework
12
 *
13
 * Permission is hereby granted, free of charge, to any person obtaining a copy
14
 * of this software and associated documentation files (the "Software"), to deal
15
 * in the Software without restriction, including without limitation the rights
16
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
 * copies of the Software, and to permit persons to whom the Software is
18
 * furnished to do so, subject to the following conditions:
19
 *
20
 * The above copyright notice and this permission notice shall be included in all
21
 * copies or substantial portions of the Software.
22
 *
23
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
 * SOFTWARE.
30
 */
31
32
declare(strict_types=1);
33
34
namespace Platine\Framework\Http\Action;
35
36
use Platine\Config\Config;
37
use Platine\Framework\Audit\Auditor;
38
use Platine\Framework\Helper\ActionHelper;
39
use Platine\Framework\Helper\FileHelper;
40
use Platine\Framework\Helper\Flash;
41
use Platine\Framework\Helper\Sidebar;
42
use Platine\Framework\Helper\ViewContext;
43
use Platine\Framework\Http\RequestData;
44
use Platine\Framework\Http\Response\RedirectResponse;
45
use Platine\Framework\Http\Response\RestResponse;
46
use Platine\Framework\Http\Response\TemplateResponse;
47
use Platine\Framework\Http\RouteHelper;
48
use Platine\Framework\Security\SecurityPolicy;
49
use Platine\Http\Handler\RequestHandlerInterface;
50
use Platine\Http\ResponseInterface;
51
use Platine\Http\ServerRequestInterface;
52
use Platine\Lang\Lang;
53
use Platine\Logger\LoggerInterface;
54
use Platine\Pagination\Pagination;
55
use Platine\Stdlib\Helper\Arr;
56
use Platine\Stdlib\Helper\Str;
57
use Platine\Template\Template;
58
59
/**
60
 * @class BaseAction
61
 * @package Platine\Framework\Http\Action
62
 * @template T
63
 */
64
abstract class BaseAction implements RequestHandlerInterface
65
{
66
    /**
67
     * The field to use in query
68
     * @var string[]
69
     */
70
    protected array $fields = [];
71
72
    /**
73
     * The field columns maps
74
     * @var array<string, string>
75
     */
76
    protected array $fieldMaps = [];
77
78
    /**
79
     * The filter list
80
     * @var array<string, mixed>
81
     */
82
    protected array $filters = [];
83
84
     /**
85
     * The filters name maps
86
     * @var array<string, string>
87
     */
88
    protected array $filterMaps = [];
89
90
    /**
91
     * The sort information's
92
     * @var array<string, string>
93
     */
94
    protected array $sorts = [];
95
96
    /**
97
     * The pagination limit
98
     * @var int|null
99
     */
100
    protected ?int $limit = null;
101
102
    /**
103
     * The pagination current page
104
     * @var int|null
105
     */
106
    protected ?int $page = null;
107
108
    /**
109
     * Whether to query all list without pagination
110
     * @var bool
111
     */
112
    protected bool $all = false;
113
114
    /**
115
     * The name of the view
116
     * @var string
117
     */
118
    protected string $viewName = '';
119
120
    /**
121
     * The pagination instance
122
     * @var Pagination
123
     */
124
    protected Pagination $pagination;
125
126
    /**
127
     * The request to use
128
     * @var ServerRequestInterface
129
     */
130
    protected ServerRequestInterface $request;
131
132
    /**
133
     * The request data instance
134
     * @var RequestData
135
     */
136
    protected RequestData $param;
137
138
    /**
139
     * The Sidebar instance
140
     * @var Sidebar
141
     */
142
    protected Sidebar $sidebar;
143
144
    /**
145
     * The view context
146
     * @var ViewContext<T>
147
     */
148
    protected ViewContext $context;
149
150
    /**
151
     * The template instance
152
     * @var Template
153
     */
154
    protected Template $template;
155
156
    /**
157
    * The RouteHelper instance
158
    * @var RouteHelper
159
    */
160
    protected RouteHelper $routeHelper;
161
162
    /**
163
    * The Flash instance
164
    * @var Flash
165
    */
166
    protected Flash $flash;
167
168
    /**
169
    * The Lang instance
170
    * @var Lang
171
    */
172
    protected Lang $lang;
173
174
    /**
175
    * The LoggerInterface instance
176
    * @var LoggerInterface
177
    */
178
    protected LoggerInterface $logger;
179
180
    /**
181
     * The auditor instance
182
     * @var Auditor
183
     */
184
    protected Auditor $auditor;
185
186
    /**
187
     * The file helper instance
188
     * @var FileHelper<T>
189
     */
190
    protected FileHelper $fileHelper;
191
192
    /**
193
     * The application configuration instance
194
     * @var Config<T>
195
     */
196
    protected Config $config;
197
198
    /**
199
     * Create new instance
200
     * @param ActionHelper<T> $actionHelper
201
     */
202
    public function __construct(ActionHelper $actionHelper)
203
    {
204
        $this->pagination = $actionHelper->getPagination();
205
        $this->sidebar = $actionHelper->getSidebar();
206
        $this->context = $actionHelper->getContext();
207
        $this->template = $actionHelper->getTemplate();
208
        $this->routeHelper = $actionHelper->getRouteHelper();
209
        $this->flash = $actionHelper->getFlash();
210
        $this->lang = $actionHelper->getLang();
211
        $this->logger = $actionHelper->getLogger();
212
        $this->auditor = $actionHelper->getAuditor();
213
        $this->fileHelper = $actionHelper->getFileHelper();
214
        $this->config = $actionHelper->getConfig();
215
    }
216
217
    /**
218
     * {@inheritodc}
219
     */
220
    public function handle(ServerRequestInterface $request): ResponseInterface
221
    {
222
        $this->request = $request;
223
        $this->param = new RequestData($request);
224
225
        $this->setFields();
226
        $this->setFilters();
227
        $this->setSorts();
228
        $this->setPagination();
229
230
        return $this->respond();
231
    }
232
233
    /**
234
     * Set the view name
235
     * @param string $name
236
     * @return self<T>
237
     */
238
    public function setView(string $name): self
239
    {
240
        $this->viewName = $name;
241
242
        return $this;
243
    }
244
245
    /**
246
     * Add sidebar
247
     * @inheritDoc
248
     * @see Sidebar
249
     * @param array<string, mixed> $params
250
     * @param array<string, mixed> $extras
251
     * @return self<T>
252
     */
253
    public function addSidebar(
254
        string $group,
255
        string $title,
256
        string $name,
257
        array $params = [],
258
        array $extras = []
259
    ): self {
260
        $this->sidebar->add($group, $title, $name, $params, $extras);
261
262
        return $this;
263
    }
264
265
    /**
266
     * Add view context
267
     * @param string $name
268
     * @param mixed $value
269
     * @return self<T>
270
     */
271
    public function addContext(string $name, mixed $value): self
272
    {
273
        $this->context->set($name, $value);
274
275
        return $this;
276
    }
277
278
    /**
279
     * Add context in one call
280
     * @param array<string, mixed> $data
281
     * @return self<T>
282
     */
283
    public function addContexts(array $data): self
284
    {
285
        foreach ($data as $name => $value) {
286
            $this->context->set($name, $value);
287
        }
288
289
        return $this;
290
    }
291
292
    /**
293
     * Return the template response
294
     * @return TemplateResponse
295
     */
296
    protected function viewResponse(): TemplateResponse
297
    {
298
        $sidebarContent = $this->sidebar->render();
299
        if (!empty($sidebarContent)) {
300
            $this->addContext('sidebar', $sidebarContent);
301
        }
302
        $this->addContext('pagination', $this->pagination->render());
303
        $this->addContext('app_url', $this->config->get('app.url'));
304
        $this->addContext('request_method', $this->request->getMethod());
305
306
        // Application info
307
        $this->addContext('app_name', $this->config->get('app.name'));
308
        $this->addContext('app_version', $this->config->get('app.version'));
309
310
        // Used in the footer
311
        $this->addContext('current_year', date('Y'));
312
313
        // Maintenance status
314
        $this->addContext('maintenance_state', app()->isInMaintenance());
315
316
        // Set nonce for Content Security Policy
317
        $nonces  = $this->request->getAttribute(SecurityPolicy::class);
318
319
        if ($nonces !== null) {
320
            $this->addContext('style_nonce', $nonces['nonces']['style']);
321
            $this->addContext('script_nonce', $nonces['nonces']['script']);
322
        }
323
324
        // get CSRF token if exist
325
        $csrfToken = $this->request->getAttribute('csrf_token');
326
        if ($csrfToken !== null) {
327
            $this->addContext('csrf_token', $csrfToken);
328
        }
329
330
        return new TemplateResponse(
331
            $this->template,
332
            $this->viewName,
333
            $this->context->all()
334
        );
335
    }
336
337
    /**
338
     * Redirect the user to another route
339
     * @param string $route
340
     * @param array<string, mixed> $params
341
     * @param array<string, mixed> $queries
342
     * @return RedirectResponse
343
     */
344
    protected function redirect(
345
        string $route,
346
        array $params = [],
347
        array $queries = []
348
    ): RedirectResponse {
349
        $queriesStr = null;
350
        if (count($queries) > 0) {
351
            $queriesStr = Arr::query($queries);
352
        }
353
354
        $routeUrl = $this->routeHelper->generateUrl($route, $params);
355
        if ($queriesStr !== null) {
356
            $routeUrl .= '?' . $queriesStr;
357
        }
358
359
        return new RedirectResponse($routeUrl);
360
    }
361
362
    /**
363
     * Return the response
364
     * @return ResponseInterface
365
     */
366
    abstract public function respond(): ResponseInterface;
367
368
    /**
369
     * Set field information's
370
     * @return void
371
     */
372
    protected function setFields(): void
373
    {
374
        $fieldParams = $this->param->get('fields', '');
375
        if (!empty($fieldParams)) {
376
            $fields = explode(',', $fieldParams);
377
            $columns = [];
378
            foreach ($fields as $field) {
379
                $columns[] = $this->fieldMaps[$field] ?? $field;
380
            }
381
            $this->fields = $columns;
382
        }
383
    }
384
385
    /**
386
     * Set filters information's
387
     * @return void
388
     */
389
    protected function setFilters(): void
390
    {
391
        $queries = $this->param->gets();
392
        //remove defaults
393
        unset(
394
            $queries['fields'],
395
            $queries['sort'],
396
            $queries['page'],
397
            $queries['limit'],
398
            $queries['all']
399
        );
400
401
        $filterParams = $queries;
402
        if (count($filterParams) > 0) {
403
            $filters = [];
404
            foreach ($filterParams as $key => $value) {
405
                $name = $this->filterMaps[$key] ?? $key;
406
                if (is_string($value) && Str::length($value) > 0) {
407
                    $filters[$name] = $value;
408
                    continue;
409
                }
410
411
                if (is_array($value) && count($value) > 1) {
412
                    $filters[$name] = $value;
413
                }
414
            }
415
416
            $this->filters = $filters;
417
        }
418
419
        // Handle default filters
420
        $this->handleFilterDefault();
421
422
        // Handle dates filter's
423
        if (array_key_exists('start_date', $this->filters)) {
424
            $startDate = $this->filters['start_date'];
425
            // if no time is provided xxxx-xx-xx
426
            if (Str::length($startDate) === 10) {
427
                $startDate .= ' 00:00:00';
428
            }
429
            $this->filters['start_date'] = $startDate;
430
        }
431
432
        if (array_key_exists('end_date', $this->filters)) {
433
            $endDate = $this->filters['end_date'];
434
            // if no time is provided xxxx-xx-xx
435
            if (Str::length($endDate) === 10) {
436
                $endDate .= ' 23:59:59';
437
            }
438
            $this->filters['end_date'] = $endDate;
439
        }
440
441
        $ignoreDateFilters = $this->getIgnoreDateFilters();
442
443
        foreach ($ignoreDateFilters as $filterName) {
444
            if (array_key_exists($filterName, $this->filters)) {
445
                unset(
446
                    $this->filters['start_date'],
447
                    $this->filters['end_date']
448
                );
449
                break;
450
            }
451
        }
452
    }
453
454
    /**
455
     * Set sort information's
456
     * @return void
457
     */
458
    protected function setSorts(): void
459
    {
460
        $sortParams = $this->param->get('sort', '');
461
        if (!empty($sortParams)) {
462
            $sorts = explode(',', $sortParams);
463
            $columns = [];
464
            foreach ($sorts as $sort) {
465
                $order = 'ASC';
466
                $parts = explode(':', $sort);
467
                if (isset($parts[1]) && strtolower($parts[1]) === 'desc') {
468
                    $order = 'DESC';
469
                }
470
471
                $column = $this->fieldMaps[$parts[0]] ?? $parts[0];
472
                $columns[$column] = $order;
473
            }
474
            $this->sorts = $columns;
475
        }
476
    }
477
478
    /**
479
     * Set the pagination information
480
     * @return void
481
     */
482
    protected function setPagination(): void
483
    {
484
        $param = $this->param;
485
486
        if ($param->get('all', null)) {
487
            $this->all = true;
488
            return;
489
        }
490
491
        $limit = $param->get('limit', null);
492
        if ($limit !== null) {
493
            $this->limit = (int) $limit;
494
        }
495
496
        if ($this->limit !== null && $this->limit > 100) {
497
            $this->limit = 100;
498
        }
499
500
        $page = $param->get('page', null);
501
        if ($page) {
502
            $this->page = (int) $page;
503
        }
504
505
        if ($limit > 0 || $page > 0) {
506
            $this->all = false;
507
        }
508
509
        if ($this->limit > 0) {
510
            $this->pagination->setItemsPerPage($this->limit);
0 ignored issues
show
Bug introduced by
It seems like $this->limit can also be of type null; however, parameter $itemsPerPage of Platine\Pagination\Pagination::setItemsPerPage() does only seem to accept integer, 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

510
            $this->pagination->setItemsPerPage(/** @scrutinizer ignore-type */ $this->limit);
Loading history...
511
        }
512
513
        $currentPage = $this->page ?? 1;
514
515
        $this->pagination->setCurrentPage($currentPage);
516
    }
517
518
    /**
519
     * Parse the error message to handle delete or update of parent record
520
     * @param string $error
521
     * @return string
522
     */
523
    protected function parseForeignConstraintErrorMessage(string $error): string
524
    {
525
        /** MySQL **
526
         * SQLSTATE[23000]: Integrity constraint violation: 1217 Cannot delete or update a
527
         * parent row: a foreign key constraint fails [DELETE FROM `TABLE_NAME` WHERE `id` = XX]
528
         */
529
530
        /** MariaDB *
531
         * SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a
532
         * parent row: a foreign key constraint fails
533
         * ("DB_NAME"."TABLE_NAME", CONSTRAINT "basetable_fk_person_id" FOREIGN KEY ("person_id")
534
         * REFERENCES "persons" ("id") ON DELETE NO ACTION) [DELETE FROM `persons` WHERE `id` = XX]
535
         */
536
        $result = '';
537
        if (strpos($error, 'Cannot delete or update a parent row') !== false) {
538
            if (strpos($error, 'Integrity constraint violation: 1217') !== false) {
539
                // MySQL
540
                $result = $this->lang->tr('This record is related to another one');
541
            } else {
542
                // MariaDB
543
                $arr = explode('.', $error);
544
                $tmp = explode(',', $arr[1] ?? '');
545
                $result = $this->lang->tr('This record is related to another one [%s]', str_replace('_', ' ', $tmp[0]));
546
            }
547
        }
548
549
        return $result;
550
    }
551
552
553
    /**
554
     * Handle filter default dates
555
     * @return void
556
     */
557
    protected function handleFilterDefault(): void
558
    {
559
    }
560
561
   /**
562
    * Ignore date filters if one of the given filters is present
563
    * @return array<string> $filters
564
    */
565
    protected function getIgnoreDateFilters(): array
566
    {
567
        return [];
568
    }
569
570
    /**
571
     * Redirect back to origin if user want to create new entity from
572
     * detail page
573
     * @return ResponseInterface|null
574
     */
575
    protected function redirectBackToOrigin(): ?ResponseInterface
576
    {
577
        $param = $this->param;
578
        $originId = (int) $param->get('origin_id', 0);
579
        $originRoute = $param->get('origin_route');
580
581
        if ($originRoute === null) {
582
            return null;
583
        }
584
585
        if ($originId === 0) {
586
            return $this->redirect($originRoute);
587
        }
588
589
        return $this->redirect($originRoute, ['id' => $originId]);
590
    }
591
592
    // REST API Part
593
    /**
594
     * Return the rest response
595
     * @param array<string, mixed>|object|mixed $data
596
     * @param int $statusCode
597
     * @param int $code
598
     *
599
     * @return ResponseInterface
600
     */
601
    protected function restResponse(
602
        $data = [],
603
        int $statusCode = 200,
604
        int $code = 0
605
    ): ResponseInterface {
606
        $extras = $this->context->all();
607
        if ($this->pagination->getTotalItems() > 0) {
608
            $extras['pagination'] = $this->pagination->getInfo();
609
        }
610
611
        return new RestResponse(
612
            $data,
613
            $extras,
614
            true,
615
            $code,
616
            '',
617
            $statusCode
618
        );
619
    }
620
621
    /**
622
     * Return the rest error response
623
     * @param string $message
624
     * @param int $statusCode
625
     * @param int $code
626
     * @param array<string, mixed> $extras
627
     *
628
     * @return ResponseInterface
629
     */
630
    protected function restErrorResponse(
631
        string $message,
632
        int $statusCode = 401,
633
        int $code = 4000,
634
        array $extras = []
635
    ): ResponseInterface {
636
        return new RestResponse(
637
            [],
638
            $extras,
639
            false,
640
            $code,
641
            $message,
642
            $statusCode
643
        );
644
    }
645
646
    /**
647
     * Return the rest server error response
648
     * @param string $message
649
     * @param int $code the custom error code
650
     *
651
     * @return ResponseInterface
652
     */
653
    protected function restServerErrorResponse(
654
        string $message = '',
655
        int $code = 5000
656
    ): ResponseInterface {
657
        return $this->restErrorResponse(
658
            $message,
659
            500,
660
            $code,
661
            []
662
        );
663
    }
664
665
    /**
666
     * Return the rest bad request error response
667
     * @param string $message
668
     * @param int $code the custom error code
669
     *
670
     * @return ResponseInterface
671
     */
672
    protected function restBadRequestErrorResponse(
673
        string $message = '',
674
        int $code = 4000
675
    ): ResponseInterface {
676
        return $this->restErrorResponse(
677
            $message,
678
            400,
679
            $code,
680
            []
681
        );
682
    }
683
684
    /**
685
     * Return the rest duplicate resource error response
686
     * @param string $message
687
     * @param int $code the custom error code
688
     *
689
     * @return ResponseInterface
690
     */
691
    protected function restConflictErrorResponse(
692
        string $message = '',
693
        int $code = 4090
694
    ): ResponseInterface {
695
        return $this->restErrorResponse(
696
            $message,
697
            409,
698
            $code,
699
            []
700
        );
701
    }
702
703
    /**
704
     * Return the rest not found error response
705
     * @param string $message
706
     * @param int $code the custom error code
707
     *
708
     * @return ResponseInterface
709
     */
710
    protected function restNotFoundErrorResponse(
711
        string $message = '',
712
        int $code = 4040
713
    ): ResponseInterface {
714
        return $this->restErrorResponse(
715
            $message,
716
            404,
717
            $code,
718
            []
719
        );
720
    }
721
722
    /**
723
     * Return the rest form validation error response
724
     * @param array<string, string> $errors
725
     * @param int $code the custom error code
726
     *
727
     * @return ResponseInterface
728
     */
729
    protected function restFormValidationErrorResponse(
730
        array $errors = [],
731
        int $code = 4220
732
    ): ResponseInterface {
733
        return $this->restErrorResponse(
734
            $this->lang->tr('Invalid Request Parameter(s)'),
735
            422,
736
            $code,
737
            ['errors' => $errors]
738
        );
739
    }
740
}
741