BaseAction::handle()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 7
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 11
rs 10
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\Orm\Query\EntityQuery;
55
use Platine\Orm\RepositoryInterface;
56
use Platine\Pagination\Pagination;
57
use Platine\Stdlib\Helper\Arr;
58
use Platine\Stdlib\Helper\Str;
59
use Platine\Template\Template;
60
61
/**
62
 * @class BaseAction
63
 * @package Platine\Framework\Http\Action
64
 * @template T
65
 */
66
abstract class BaseAction implements RequestHandlerInterface
67
{
68
    /**
69
     * The field to use in query
70
     * @var string[]
71
     */
72
    protected array $fields = [];
73
74
    /**
75
     * The field columns maps
76
     * @var array<string, string>
77
     */
78
    protected array $fieldMaps = [];
79
80
    /**
81
     * The filter list
82
     * @var array<string, mixed>
83
     */
84
    protected array $filters = [];
85
86
     /**
87
     * The filters name maps
88
     * @var array<string, string>
89
     */
90
    protected array $filterMaps = [];
91
92
    /**
93
     * The sort information's
94
     * @var array<string, string>
95
     */
96
    protected array $sorts = [];
97
98
    /**
99
     * The pagination limit
100
     * @var int|null
101
     */
102
    protected ?int $limit = null;
103
104
    /**
105
     * The pagination current page
106
     * @var int|null
107
     */
108
    protected ?int $page = null;
109
110
    /**
111
     * Whether to query all list without pagination
112
     * @var bool
113
     */
114
    protected bool $all = false;
115
116
    /**
117
     * The name of the view
118
     * @var string
119
     */
120
    protected string $viewName = '';
121
122
    /**
123
     * The pagination instance
124
     * @var Pagination
125
     */
126
    protected Pagination $pagination;
127
128
    /**
129
     * The request to use
130
     * @var ServerRequestInterface
131
     */
132
    protected ServerRequestInterface $request;
133
134
    /**
135
     * The request data instance
136
     * @var RequestData
137
     */
138
    protected RequestData $param;
139
140
    /**
141
     * The Sidebar instance
142
     * @var Sidebar
143
     */
144
    protected Sidebar $sidebar;
145
146
    /**
147
     * The view context
148
     * @var ViewContext<T>
149
     */
150
    protected ViewContext $context;
151
152
    /**
153
     * The template instance
154
     * @var Template
155
     */
156
    protected Template $template;
157
158
    /**
159
    * The RouteHelper instance
160
    * @var RouteHelper
161
    */
162
    protected RouteHelper $routeHelper;
163
164
    /**
165
    * The Flash instance
166
    * @var Flash
167
    */
168
    protected Flash $flash;
169
170
    /**
171
    * The Lang instance
172
    * @var Lang
173
    */
174
    protected Lang $lang;
175
176
    /**
177
    * The LoggerInterface instance
178
    * @var LoggerInterface
179
    */
180
    protected LoggerInterface $logger;
181
182
    /**
183
     * The auditor instance
184
     * @var Auditor
185
     */
186
    protected Auditor $auditor;
187
188
    /**
189
     * The file helper instance
190
     * @var FileHelper<T>
191
     */
192
    protected FileHelper $fileHelper;
193
194
    /**
195
     * The application configuration instance
196
     * @var Config<T>
197
     */
198
    protected Config $config;
199
200
    /**
201
     * Create new instance
202
     * @param ActionHelper<T> $actionHelper
203
     */
204
    public function __construct(ActionHelper $actionHelper)
205
    {
206
        $this->pagination = $actionHelper->getPagination();
207
        $this->sidebar = $actionHelper->getSidebar();
208
        $this->context = $actionHelper->getContext();
209
        $this->template = $actionHelper->getTemplate();
210
        $this->routeHelper = $actionHelper->getRouteHelper();
211
        $this->flash = $actionHelper->getFlash();
212
        $this->lang = $actionHelper->getLang();
213
        $this->logger = $actionHelper->getLogger();
214
        $this->auditor = $actionHelper->getAuditor();
215
        $this->fileHelper = $actionHelper->getFileHelper();
216
        $this->config = $actionHelper->getConfig();
217
    }
218
219
    /**
220
     * {@inheritodc}
221
     */
222
    public function handle(ServerRequestInterface $request): ResponseInterface
223
    {
224
        $this->request = $request;
225
        $this->param = new RequestData($request);
226
227
        $this->setFields();
228
        $this->setFilters();
229
        $this->setSorts();
230
        $this->setPagination();
231
232
        return $this->respond();
233
    }
234
235
    /**
236
     * Set the view name
237
     * @param string $name
238
     * @return self<T>
239
     */
240
    public function setView(string $name): self
241
    {
242
        $this->viewName = $name;
243
244
        return $this;
245
    }
246
247
    /**
248
     * Add sidebar
249
     * @inheritDoc
250
     * @see Sidebar
251
     * @param array<string, mixed> $params
252
     * @param array<string, mixed> $extras
253
     * @return self<T>
254
     */
255
    public function addSidebar(
256
        string $group,
257
        string $title,
258
        string $name,
259
        array $params = [],
260
        array $extras = []
261
    ): self {
262
        $this->sidebar->add($group, $title, $name, $params, $extras);
263
264
        return $this;
265
    }
266
267
    /**
268
     * Add view context
269
     * @param string $name
270
     * @param mixed $value
271
     * @return self<T>
272
     */
273
    public function addContext(string $name, mixed $value): self
274
    {
275
        $this->context->set($name, $value);
276
277
        return $this;
278
    }
279
280
    /**
281
     * Add context in one call
282
     * @param array<string, mixed> $data
283
     * @return self<T>
284
     */
285
    public function addContexts(array $data): self
286
    {
287
        foreach ($data as $name => $value) {
288
            $this->context->set($name, $value);
289
        }
290
291
        return $this;
292
    }
293
294
    /**
295
     * Return the template response
296
     * @return TemplateResponse
297
     */
298
    protected function viewResponse(): TemplateResponse
299
    {
300
        $sidebarContent = $this->sidebar->render();
301
        if (!empty($sidebarContent)) {
302
            $this->addContext('sidebar', $sidebarContent);
303
        }
304
        $this->addContext('pagination', $this->pagination->render());
305
        $this->addContext('app_url', $this->config->get('app.url'));
306
        $this->addContext('request_method', $this->request->getMethod());
307
308
        // Application info
309
        $this->addContext('app_name', $this->config->get('app.name'));
310
        $this->addContext('app_version', $this->config->get('app.version'));
311
312
        // Used in the footer
313
        $this->addContext('current_year', date('Y'));
314
315
        // Maintenance status
316
        $this->addContext('maintenance_state', app()->isInMaintenance());
317
318
        // Set nonce for Content Security Policy
319
        $nonces  = $this->request->getAttribute(SecurityPolicy::class);
320
321
        if ($nonces !== null) {
322
            $this->addContext('style_nonce', $nonces['nonces']['style']);
323
            $this->addContext('script_nonce', $nonces['nonces']['script']);
324
        }
325
326
        // get CSRF token if exist
327
        $csrfToken = $this->request->getAttribute('csrf_token');
328
        if ($csrfToken !== null) {
329
            $this->addContext('csrf_token', $csrfToken);
330
        }
331
332
        return new TemplateResponse(
333
            $this->template,
334
            $this->viewName,
335
            $this->context->all()
336
        );
337
    }
338
339
    /**
340
     * Redirect the user to another route
341
     * @param string $route
342
     * @param array<string, mixed> $params
343
     * @param array<string, mixed> $queries
344
     * @return RedirectResponse
345
     */
346
    protected function redirect(
347
        string $route,
348
        array $params = [],
349
        array $queries = []
350
    ): RedirectResponse {
351
        $queriesStr = null;
352
        if (count($queries) > 0) {
353
            $queriesStr = Arr::query($queries);
354
        }
355
356
        $routeUrl = $this->routeHelper->generateUrl($route, $params);
357
        if ($queriesStr !== null) {
358
            $routeUrl .= '?' . $queriesStr;
359
        }
360
361
        return new RedirectResponse($routeUrl);
362
    }
363
364
    /**
365
     * Return the response
366
     * @return ResponseInterface
367
     */
368
    abstract public function respond(): ResponseInterface;
369
370
    /**
371
     * Set field information's
372
     * @return void
373
     */
374
    protected function setFields(): void
375
    {
376
        $fieldParams = $this->param->get('fields', '');
377
        if (!empty($fieldParams)) {
378
            $fields = explode(',', $fieldParams);
379
            $columns = [];
380
            foreach ($fields as $field) {
381
                $columns[] = $this->fieldMaps[$field] ?? $field;
382
            }
383
            $this->fields = $columns;
384
        }
385
    }
386
387
    /**
388
     * Set filters information's
389
     * @return void
390
     */
391
    protected function setFilters(): void
392
    {
393
        $queries = $this->param->gets();
394
        //remove defaults
395
        unset(
396
            $queries['fields'],
397
            $queries['sort'],
398
            $queries['page'],
399
            $queries['limit'],
400
            $queries['all']
401
        );
402
403
        $filterParams = $queries;
404
        if (count($filterParams) > 0) {
405
            $filters = [];
406
            foreach ($filterParams as $key => $value) {
407
                $name = $this->filterMaps[$key] ?? $key;
408
                if (is_string($value) && Str::length($value) > 0) {
409
                    $filters[$name] = $value;
410
                    continue;
411
                }
412
413
                if (is_array($value) && count($value) > 1) {
414
                    $filters[$name] = $value;
415
                }
416
            }
417
418
            $this->filters = $filters;
419
        }
420
421
        // Handle default filters
422
        $this->handleFilterDefault();
423
424
        // Handle dates filter's
425
        if (array_key_exists('start_date', $this->filters)) {
426
            $startDate = $this->filters['start_date'];
427
            // if no time is provided xxxx-xx-xx
428
            if (Str::length($startDate) === 10) {
429
                $startDate .= ' 00:00:00';
430
            }
431
            $this->filters['start_date'] = $startDate;
432
        }
433
434
        if (array_key_exists('end_date', $this->filters)) {
435
            $endDate = $this->filters['end_date'];
436
            // if no time is provided xxxx-xx-xx
437
            if (Str::length($endDate) === 10) {
438
                $endDate .= ' 23:59:59';
439
            }
440
            $this->filters['end_date'] = $endDate;
441
        }
442
443
        $ignoreDateFilters = $this->getIgnoreDateFilters();
444
445
        foreach ($ignoreDateFilters as $filterName) {
446
            if (array_key_exists($filterName, $this->filters)) {
447
                unset(
448
                    $this->filters['start_date'],
449
                    $this->filters['end_date']
450
                );
451
                break;
452
            }
453
        }
454
    }
455
456
    /**
457
     * Set sort information's
458
     * @return void
459
     */
460
    protected function setSorts(): void
461
    {
462
        $sortParams = $this->param->get('sort', '');
463
        if (!empty($sortParams)) {
464
            $sorts = explode(',', $sortParams);
465
            $columns = [];
466
            foreach ($sorts as $sort) {
467
                $order = 'ASC';
468
                $parts = explode(':', $sort);
469
                if (isset($parts[1]) && strtolower($parts[1]) === 'desc') {
470
                    $order = 'DESC';
471
                }
472
473
                $column = $this->fieldMaps[$parts[0]] ?? $parts[0];
474
                $columns[$column] = $order;
475
            }
476
            $this->sorts = $columns;
477
        }
478
    }
479
480
    /**
481
     * Set the pagination information
482
     * @return void
483
     */
484
    protected function setPagination(): void
485
    {
486
        $param = $this->param;
487
488
        if ($param->get('all', null)) {
489
            $this->all = true;
490
            return;
491
        }
492
493
        $limit = $param->get('limit', null);
494
        if ($limit !== null) {
495
            $this->limit = (int) $limit;
496
        }
497
498
        if ($this->limit !== null && $this->limit > 100) {
499
            $this->limit = 100;
500
        }
501
502
        $page = $param->get('page', null);
503
        if ($page) {
504
            $this->page = (int) $page;
505
        }
506
507
        if ($limit > 0 || $page > 0) {
508
            $this->all = false;
509
        }
510
511
        if ($this->limit > 0) {
512
            $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

512
            $this->pagination->setItemsPerPage(/** @scrutinizer ignore-type */ $this->limit);
Loading history...
513
        }
514
515
        $currentPage = $this->page ?? 1;
516
517
        $this->pagination->setCurrentPage($currentPage);
518
    }
519
520
    /**
521
     * Parse the error message to handle delete or update of parent record
522
     * @param string $error
523
     * @return string
524
     */
525
    protected function parseForeignConstraintErrorMessage(string $error): string
526
    {
527
        /** MySQL **
528
         * SQLSTATE[23000]: Integrity constraint violation: 1217 Cannot delete or update a
529
         * parent row: a foreign key constraint fails [DELETE FROM `TABLE_NAME` WHERE `id` = XX]
530
         */
531
532
        /** MariaDB *
533
         * SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a
534
         * parent row: a foreign key constraint fails
535
         * ("DB_NAME"."TABLE_NAME", CONSTRAINT "basetable_fk_person_id" FOREIGN KEY ("person_id")
536
         * REFERENCES "persons" ("id") ON DELETE NO ACTION) [DELETE FROM `persons` WHERE `id` = XX]
537
         */
538
        $result = '';
539
        if (strpos($error, 'Cannot delete or update a parent row') !== false) {
540
            if (strpos($error, 'Integrity constraint violation: 1217') !== false) {
541
                // MySQL
542
                $result = $this->lang->tr('This record is related to another one');
543
            } else {
544
                // MariaDB
545
                $arr = explode('.', $error);
546
                $tmp = explode(',', $arr[1] ?? '');
547
                $result = $this->lang->tr('This record is related to another one [%s]', str_replace('_', ' ', $tmp[0]));
548
            }
549
        }
550
551
        return $result;
552
    }
553
554
555
    /**
556
     * Handle filter default dates
557
     * @return void
558
     */
559
    protected function handleFilterDefault(): void
560
    {
561
    }
562
563
   /**
564
    * Ignore date filters if one of the given filters is present
565
    * @return array<string> $filters
566
    */
567
    protected function getIgnoreDateFilters(): array
568
    {
569
        return [];
570
    }
571
572
    /**
573
     * Redirect back to origin if user want to create new entity from
574
     * detail page
575
     * @return ResponseInterface|null
576
     */
577
    protected function redirectBackToOrigin(): ?ResponseInterface
578
    {
579
        $param = $this->param;
580
        $originId = (int) $param->get('origin_id', 0);
581
        $originRoute = $param->get('origin_route');
582
583
        if ($originRoute === null) {
584
            return null;
585
        }
586
587
        if ($originId === 0) {
588
            return $this->redirect($originRoute);
589
        }
590
591
        return $this->redirect($originRoute, ['id' => $originId]);
592
    }
593
594
    /**
595
     * Process pagination and sort
596
     * @param RepositoryInterface $repository
597
     * @param EntityQuery $query
598
     * @param string|array<string> $sortFields
599
     * @param string $sortDir
600
     * @return void
601
     */
602
    protected function handleRestPagination(
603
        RepositoryInterface $repository,
604
        EntityQuery $query,
605
        string|array $sortFields = 'name',
606
        string $sortDir = 'ASC'
607
    ): void {
608
        if ($this->all === false) {
609
            $totalItems = $repository->filters($this->filters)
610
                                     ->query()
611
                                     ->count('id');
612
613
            $currentPage = (int) $this->param->get('page', 1);
614
615
            $this->pagination->setTotalItems($totalItems)
616
                             ->setCurrentPage($currentPage);
617
618
            $limit = $this->pagination->getItemsPerPage();
619
            $offset = $this->pagination->getOffset();
620
621
            $query = $query->limit($limit)
622
                           ->offset($offset);
623
        }
624
625
        if (count($this->sorts) > 0) {
626
            foreach ($this->sorts as $column => $order) {
627
                $query = $query->orderBy($column, $order);
628
            }
629
        } else {
630
            $query = $query->orderBy($sortFields, $sortDir);
0 ignored issues
show
Unused Code introduced by
The assignment to $query is dead and can be removed.
Loading history...
631
        }
632
    }
633
634
    // REST API Part
635
    /**
636
     * Return the rest response
637
     * @param mixed $data
638
     * @param int $statusCode
639
     * @param int $code the custom code
640
     *
641
     * @return ResponseInterface
642
     */
643
    protected function restResponse(
644
        mixed $data = [],
645
        int $statusCode = 200,
646
        int $code = 0
647
    ): ResponseInterface {
648
        $extras = $this->context->all();
649
        if ($this->pagination->getTotalItems() > 0) {
650
            $extras['pagination'] = $this->pagination->getInfo();
651
        }
652
653
        return new RestResponse(
654
            $data,
655
            $extras,
656
            true,
657
            $code,
658
            '',
659
            $statusCode
660
        );
661
    }
662
663
    /**
664
     * Return the rest created response
665
     * @param array<string, mixed>|object|mixed $data
666
     * @param int $code the custom code
667
     *
668
     * @return ResponseInterface
669
     */
670
    protected function restCreatedResponse(mixed $data = [], int $code = 0): ResponseInterface
671
    {
672
        return $this->restResponse(
673
            $data,
674
            201,
675
            $code
676
        );
677
    }
678
679
    /**
680
     * Return the rest no content response
681
     * @param int $code the custom code
682
     *
683
     * @return ResponseInterface
684
     */
685
    protected function restNoContentResponse(int $code = 0): ResponseInterface
686
    {
687
        return $this->restResponse(
688
            [],
689
            204,
690
            $code
691
        );
692
    }
693
694
    /**
695
     * Return the rest error response
696
     * @param string $message
697
     * @param int $statusCode
698
     * @param int $code
699
     * @param array<string, mixed> $extras
700
     *
701
     * @return ResponseInterface
702
     */
703
    protected function restErrorResponse(
704
        string $message,
705
        int $statusCode = 401,
706
        int $code = 4000,
707
        array $extras = []
708
    ): ResponseInterface {
709
        return new RestResponse(
710
            [],
711
            $extras,
712
            false,
713
            $code,
714
            $message,
715
            $statusCode
716
        );
717
    }
718
719
    /**
720
     * Return the rest server error response
721
     * @param string $message
722
     * @param int $code the custom error code
723
     *
724
     * @return ResponseInterface
725
     */
726
    protected function restServerErrorResponse(
727
        string $message = '',
728
        int $code = 5000
729
    ): ResponseInterface {
730
        return $this->restErrorResponse(
731
            $message,
732
            500,
733
            $code,
734
            []
735
        );
736
    }
737
738
    /**
739
     * Return the rest bad request error response
740
     * @param string $message
741
     * @param int $code the custom error code
742
     *
743
     * @return ResponseInterface
744
     */
745
    protected function restBadRequestErrorResponse(
746
        string $message = '',
747
        int $code = 4000
748
    ): ResponseInterface {
749
        return $this->restErrorResponse(
750
            $message,
751
            400,
752
            $code,
753
            []
754
        );
755
    }
756
757
    /**
758
     * Return the rest duplicate resource error response
759
     * @param string $message
760
     * @param int $code the custom error code
761
     *
762
     * @return ResponseInterface
763
     */
764
    protected function restConflictErrorResponse(
765
        string $message = '',
766
        int $code = 4090
767
    ): ResponseInterface {
768
        return $this->restErrorResponse(
769
            $message,
770
            409,
771
            $code,
772
            []
773
        );
774
    }
775
776
    /**
777
     * Return the rest not found error response
778
     * @param string $message
779
     * @param int $code the custom error code
780
     *
781
     * @return ResponseInterface
782
     */
783
    protected function restNotFoundErrorResponse(
784
        string $message = '',
785
        int $code = 4040
786
    ): ResponseInterface {
787
        return $this->restErrorResponse(
788
            $message,
789
            404,
790
            $code,
791
            []
792
        );
793
    }
794
795
    /**
796
     * Return the rest form validation error response
797
     * @param array<string, string> $errors
798
     * @param int $code the custom error code
799
     *
800
     * @return ResponseInterface
801
     */
802
    protected function restFormValidationErrorResponse(
803
        array $errors = [],
804
        int $code = 4220
805
    ): ResponseInterface {
806
        return $this->restErrorResponse(
807
            $this->lang->tr('Invalid Request Parameter(s)'),
808
            422,
809
            $code,
810
            ['errors' => $errors]
811
        );
812
    }
813
}
814