Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.12% covered (success)
94.12%
48 / 51
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
EssentialProjectionResolver
96.00% covered (success)
96.00%
48 / 50
75.00% covered (warning)
75.00%
3 / 4
25
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
 ensureEssentialSelects
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
1 / 1
13
 detectModuleFeatures
88.89% covered (warning)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 1
10.14
 buildFavoriteSelectExpr
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
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;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Engine\Domain\Model\FieldMetadata;
12use App\Core\Engine\Domain\Model\ModuleMetadata;
13use App\Core\Engine\Domain\Model\PermissionContext;
14use PDO;
15
16/**
17 * Essential Projection Resolver.
18 *
19 * Resolves essential select projections (ID, special access/status, favorite, pin) and detects module features.
20 *
21 * @package App\Core\Engine\Application\Query
22 */
23final readonly class EssentialProjectionResolver
24{
25    private const string FORMAT_COL_ALIAS = '`%s`.`%s`';
26
27    /**
28     * EssentialProjectionResolver constructor.
29     *
30     * @param PDO $pdo Database connection for escaping literals.
31     */
32    public function __construct(
33        private PDO $pdo,
34    ) {
35    }
36
37    /**
38     * Ensures essential system fields (id, status, favorite, pin) are included in SELECT projection.
39     *
40     * @param array<int, string>        $selectExprs   Base select column expressions.
41     * @param ModuleMetadata            $module        Target module metadata.
42     * @param array<int, FieldMetadata> $fields        All module fields.
43     * @param array<int, FieldMetadata> $visibleFields Fields currently visible.
44     * @param PermissionContext         $context       Security context.
45     * @return array<int, string> Augmented select expressions.
46     */
47    public function ensureEssentialSelects(
48        array             $selectExprs,
49        ModuleMetadata    $module,
50        array             $fields,
51        array             $visibleFields,
52        PermissionContext $context,
53    ): array {
54        $pkCol = $module->primaryKey !== '' ? $module->primaryKey : 'id';
55        $features = $this->detectModuleFeatures($fields);
56
57        $hasId = false;
58        $hasAccessCol = false;
59        $visibleKeys = [];
60        $accessCol = $features['accessCol'];
61
62        foreach ($visibleFields as $f) {
63            $visibleKeys[] = $f->fieldKey;
64            if ($f->fieldKey === 'id' || $f->fieldKey === $pkCol) {
65                $hasId = true;
66            }
67            if ($accessCol !== null && $f->fieldKey === $accessCol) {
68                $hasAccessCol = true;
69            }
70        }
71
72        if (!$hasId) {
73            $selectExprs[] = sprintf(self::FORMAT_COL_ALIAS, $module->tableAlias, $pkCol) . ' AS `id`';
74        }
75        if (!$hasAccessCol && $accessCol !== null) {
76            $selectExprs[] = sprintf(self::FORMAT_COL_ALIAS, $module->tableAlias, $accessCol)
77                . ' AS `' . $accessCol . '`';
78        }
79        if ($features['hasFavorite']) {
80            $selectExprs[] = $this->buildFavoriteSelectExpr($module, $pkCol, $context->actorUserId);
81        }
82        if ($features['hasPinned'] && !in_array('is_pinned', $visibleKeys, true)) {
83            $selectExprs[] = sprintf(self::FORMAT_COL_ALIAS, $module->tableAlias, 'is_pinned') . ' AS `is_pinned`';
84        }
85
86        return $selectExprs;
87    }
88
89    /**
90     * Detects special system capabilities supported by the module fields.
91     *
92     * @param array<int, FieldMetadata> $fields Module fields list.
93     * @return array{hasRecordStatus: bool, accessCol: string|null, hasFavorite: bool, hasPinned: bool} Feature flags.
94     */
95    public function detectModuleFeatures(array $fields): array
96    {
97        $features = [
98            'hasRecordStatus' => false,
99            'accessCol'       => null,
100            'hasFavorite'     => false,
101            'hasPinned'       => false,
102        ];
103        foreach ($fields as $f) {
104            if ($f->fieldKey === 'special_access' || $f->uitypeName === 'special_access') {
105                $features['hasRecordStatus'] = true;
106                $features['accessCol']       = 'special_access';
107            } elseif ($f->fieldKey === 'record_status' && $features['accessCol'] === null) {
108                $features['hasRecordStatus'] = true;
109                $features['accessCol']       = 'record_status';
110            }
111            if ($f->fieldKey === 'is_favorite' || $f->uitypeName === 'favorite') {
112                $features['hasFavorite'] = true;
113            }
114            if ($f->fieldKey === 'is_pinned' || $f->uitypeName === 'pin') {
115                $features['hasPinned'] = true;
116            }
117        }
118
119        return $features;
120    }
121
122    /**
123     * Builds SQL expression for resolving favorite status for current user.
124     *
125     * @param ModuleMetadata $module Module metadata.
126     * @param string         $pkCol  Primary key column name.
127     * @param int            $userId Current authenticated user ID.
128     * @return string Subquery expression.
129     */
130    public function buildFavoriteSelectExpr(ModuleMetadata $module, string $pkCol, int $userId): string
131    {
132        return sprintf(
133            '(SELECT COUNT(1) > 0 FROM `a_core_record_favorites` AS `_fav` '
134            . 'WHERE `_fav`.`module_name` = %s AND `_fav`.`record_id` = `%s`.`%s` '
135            . 'AND `_fav`.`user_id` = %d) AS `is_favorite`',
136            $this->pdo->quote($module->name),
137            $module->tableAlias,
138            $pkCol,
139            $userId
140        );
141    }
142}