Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.33% covered (warning)
83.33%
30 / 36
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
GridQueryBuilder
83.33% covered (warning)
83.33%
30 / 36
66.67% covered (warning)
66.67%
2 / 3
9.37
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 tableExists
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 fetchData
81.25% covered (warning)
81.25%
26 / 32
0.00% covered (danger)
0.00%
0 / 1
7.32
1<?php
2
3declare(strict_types=1);
4
5namespace App\Infrastructure\Shared\Query;
6
7use App\Domain\Shared\Query\GridQuerySpecification;
8use Yiisoft\Db\Connection\ConnectionInterface;
9use Yiisoft\Db\Query\Query;
10
11 class GridQueryBuilder
12{
13    public function __construct(
14        private ConnectionInterface $db
15    ) {}
16
17    /**
18     * Checks whether a database table exists in the current connection schema.
19     */
20    public function tableExists(string $tableName): bool
21    {
22        $rawName = str_replace(['{{%', '}}', '`'], '', $tableName);
23        $schema = $this->db->getSchema();
24        return $schema->getTableSchema($rawName) !== null;
25    }
26
27    /**
28     * Executes a database-engine agnostic query with pagination, per-column filtering, and sorting using Yii3 Query builder.
29     * Compatible across MySQL, PostgreSQL, SQLite, SQL Server, and Oracle.
30     *
31     * @param string $tableName Primary table name (e.g. '{{%hosts_records}}' or 'c01_menu_records')
32     * @param GridQuerySpecification $spec Specification containing pagination, sorting, and filters
33     * @param string $defaultSortField Default column to sort by if none specified
34     * @param string $defaultSortDirection Default sort direction ('ASC' or 'DESC')
35     * @return array{items: array<int, array<string, mixed>>, totalCount: int, tableExists?: bool, tableName?: string}
36     */
37    public function fetchData(
38        string $tableName,
39        GridQuerySpecification $spec,
40        string $defaultSortField = 'created_at',
41        string $defaultSortDirection = 'DESC'
42    ): array {
43        $rawName = str_replace(['{{%', '}}', '`'], '', $tableName);
44
45        if (!$this->tableExists($tableName)) {
46            return [
47                'items' => [],
48                'totalCount' => 0,
49                'tableExists' => false,
50                'tableName' => $rawName,
51            ];
52        }
53
54        $allowedColumns = $spec->getAllowedColumns();
55        $query = (new Query($this->db))->from($tableName);
56
57        // Apply per-column search filters in a multi-db safe way
58        foreach ($spec->getFilters() as $column => $searchTerm) {
59            $sqlColumn = $allowedColumns[$column] ?? $column;
60            $query->andWhere(['like', $sqlColumn, $searchTerm]);
61        }
62
63        // Count total matching records before applying pagination (using a cloned query instance)
64        $countQuery = clone $query;
65        $totalCount = (int) $countQuery->count();
66
67        // Apply sorting
68        $sortField = $spec->getSortField();
69        $sortDirection = $spec->getSortDirection() === 'DESC' ? SORT_DESC : SORT_ASC;
70
71        if ($sortField !== null && isset($allowedColumns[$sortField])) {
72            $orderByColumn = $allowedColumns[$sortField];
73            $query->orderBy([$orderByColumn => $sortDirection]);
74        } else {
75            $defaultCol = $allowedColumns[$defaultSortField] ?? $defaultSortField;
76            $defaultDir = $defaultSortDirection === 'DESC' ? SORT_DESC : SORT_ASC;
77            $query->orderBy([$defaultCol => $defaultDir]);
78        }
79
80        // Apply engine-agnostic LIMIT & OFFSET
81        /** @var array<int, array<string, mixed>> $items */
82        $items = $query->limit($spec->getPageSize())
83            ->offset($spec->getOffset())
84            ->all();
85
86        return [
87            'items' => $items,
88            'totalCount' => $totalCount,
89            'tableExists' => true,
90            'tableName' => $rawName,
91        ];
92    }
93}