Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.74% covered (success)
95.74%
45 / 47
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
UniversalJoinClauseBuilder
97.83% covered (success)
97.83%
45 / 46
80.00% covered (warning)
80.00%
4 / 5
24
0.00% covered (danger)
0.00%
0 / 1
 filterRequiredRelationalFields
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 filterVisible
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
6
 buildSelectExpressions
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 resolveSortColumn
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
7
 resolveSortOrder
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Engine\Application\Query\Clause;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Engine\Domain\Model\FieldMetadata;
12use App\Core\Engine\Domain\Model\FilterMetadata;
13use App\Core\Grid\GridRequest;
14use App\Core\Engine\Domain\Model\ModuleMetadata;
15
16/**
17 * UniversalJoinClauseBuilder.
18 *
19 * Resolves relational JOIN dependencies, visible SELECT expressions, and ORDER BY sort expressions.
20 *
21 * @package App\Core\Engine\Application\Query\Clause
22 */
23final readonly class UniversalJoinClauseBuilder
24{
25    /**
26     * Filters relational fields to those required by visible columns, WHERE filters, or ORDER BY.
27     *
28     * @param array<int, FieldMetadata> $fields        All field definitions for the module.
29     * @param array<int, FieldMetadata> $visibleFields Visible field definitions in active filter.
30     * @param string                    $whereSql      Generated WHERE SQL snippet.
31     * @param string                    $sortColumn    Resolved sort column expression.
32     * @return array<int, FieldMetadata> Filtered list of required relational fields.
33     */
34    public function filterRequiredRelationalFields(
35        array  $fields,
36        array  $visibleFields,
37        string $whereSql,
38        string $sortColumn
39    ): array {
40        $visibleKeys = array_map(static fn(FieldMetadata $f): string => $f->fieldKey, $visibleFields);
41
42        return array_values(array_filter(
43            $fields,
44            static function (FieldMetadata $f) use ($visibleKeys, $whereSql, $sortColumn): bool {
45                if (!$f->hasRelation()) {
46                    return false;
47                }
48                $joinAlias = 'rel_' . $f->fieldKey;
49                return in_array($f->fieldKey, $visibleKeys, true)
50                    || str_contains($whereSql, $joinAlias)
51                    || str_contains($sortColumn, $joinAlias);
52            }
53        ));
54    }
55
56    /**
57     * Filters the field list to only include fields visible in the active filter.
58     *
59     * @param array<int, FieldMetadata> $fields All module fields.
60     * @param FilterMetadata            $filter Active filter configuration.
61     * @return array<int, FieldMetadata> Filtered list of visible fields.
62     */
63    public function filterVisible(array $fields, FilterMetadata $filter): array
64    {
65        if ($filter->visibleFields !== []) {
66            $map = [];
67            foreach ($fields as $f) {
68                $map[$f->fieldKey] = $f;
69            }
70
71            $ordered = [];
72            foreach ($filter->visibleFields as $key) {
73                if (isset($map[$key])) {
74                    $ordered[] = $map[$key];
75                }
76            }
77
78            if ($ordered !== []) {
79                return $ordered;
80            }
81        }
82
83        $filtered = array_filter(
84            $fields,
85            static fn(FieldMetadata $f): bool => $filter->isFieldVisible($f->fieldKey)
86        );
87
88        return array_values($filtered);
89    }
90
91    /**
92     * Builds the SELECT expressions for the visible fields.
93     *
94     * @param array<int, FieldMetadata> $fields Visible field definitions.
95     * @return array<int, string> Array of SQL SELECT expressions.
96     */
97    public function buildSelectExpressions(array $fields): array
98    {
99        $expressions = [];
100        foreach ($fields as $field) {
101            if ($field->fieldKey === 'is_favorite' || $field->uitypeName === 'favorite') {
102                continue;
103            }
104            $expressions[] = sprintf('%s AS `%s`', $field->columnExpression, $field->fieldKey);
105        }
106
107        return $expressions;
108    }
109
110    /**
111     * Resolves the sort column SQL expression safely.
112     *
113     * @param ModuleMetadata            $module      Module metadata.
114     * @param array<int, FieldMetadata> $fields      Module field definitions.
115     * @param FilterMetadata            $filter      Active filter.
116     * @param GridRequest               $gridRequest Pagination request.
117     * @return string Validated SQL sort column expression.
118     */
119    public function resolveSortColumn(
120        ModuleMetadata $module,
121        array          $fields,
122        FilterMetadata $filter,
123        GridRequest    $gridRequest
124    ): string {
125        $requested = ($gridRequest->sortColumn !== null && $gridRequest->sortColumn !== '')
126            ? $gridRequest->sortColumn
127            : $filter->defaultSort;
128
129        foreach ($fields as $field) {
130            if ($field->fieldKey === $requested) {
131                if ($field->fieldKey === 'is_favorite' || $field->uitypeName === 'favorite') {
132                    return '`is_favorite`';
133                }
134                return $field->columnExpression;
135            }
136        }
137
138        return sprintf('`%s`.`id`', $module->tableAlias);
139    }
140
141    /**
142     * Resolves the sort order direction ('ASC' or 'DESC').
143     *
144     * @param FilterMetadata $filter      Active filter.
145     * @param GridRequest    $gridRequest Grid pagination request.
146     * @return string Upper-case sort order ('ASC' or 'DESC').
147     */
148    public function resolveSortOrder(FilterMetadata $filter, GridRequest $gridRequest): string
149    {
150        $rawOrder = $gridRequest->sortDirection !== ''
151            ? $gridRequest->sortDirection
152            : ($filter->defaultOrder ?? 'ASC');
153        $order = strtoupper((string) $rawOrder);
154        return $order === 'ASC' ? 'ASC' : 'DESC';
155    }
156}