Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
94.12% |
48 / 51 |
|
75.00% |
3 / 4 |
CRAP | |
0.00% |
0 / 1 |
| EssentialProjectionResolver | |
96.00% |
48 / 50 |
|
75.00% |
3 / 4 |
25 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| ensureEssentialSelects | |
100.00% |
22 / 22 |
|
100.00% |
1 / 1 |
13 | |||
| detectModuleFeatures | |
88.89% |
16 / 18 |
|
0.00% |
0 / 1 |
10.14 | |||
| buildFavoriteSelectExpr | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | /** @license For full copyright and license information, please see the LICENSE.md file. */ |
| 6 | |
| 7 | namespace App\Core\Engine\Application\Query; |
| 8 | |
| 9 | defined('AMMONLY_APP') || exit('Direct script access is forbidden.'); |
| 10 | |
| 11 | use App\Core\Engine\Domain\Model\FieldMetadata; |
| 12 | use App\Core\Engine\Domain\Model\ModuleMetadata; |
| 13 | use App\Core\Engine\Domain\Model\PermissionContext; |
| 14 | use 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 | */ |
| 23 | final 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 | } |