Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.67% covered (success)
97.67%
42 / 43
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
1 / 1
RelationMmQueryResolver
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
2 / 2
7
100.00% covered (success)
100.00%
1 / 1
 buildAvailableRecordsWhere
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
6
 buildEmptyAvailablePayload
100.00% covered (success)
100.00%
14 / 14
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\Service\Relation;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Engine\Domain\Model\ModuleMetadata;
12use App\Core\Engine\Domain\Model\RelationMmMetadata;
13
14/**
15 * Relation M:M Query Resolver.
16 *
17 * Constructs SQL WHERE clauses and query payloads for M:M relation searches and linkable record queries.
18 *
19 * @package App\Core\Engine\Application\Service\Relation
20 */
21final readonly class RelationMmQueryResolver
22{
23    private const string FMT_TARGET_COL = 't.`%s`';
24
25    /**
26     * Builds WHERE clause and bound parameters for available records query.
27     *
28     * @param array{table: string, source_col: string, target_col: string, source_id: int} $relationInfo
29     * @param array<string, string> $columnFilters  Column-level search filters.
30     * @param array<string, bool>   $validKeys      Allowed field keys.
31     * @param array<string>         $searchableCols Columns participating in general search.
32     * @param string                $generalQuery   General search query.
33     * @return array{0: string, 1: array<string, mixed>} WHERE SQL and parameters.
34     */
35    public function buildAvailableRecordsWhere(
36        array  $relationInfo,
37        array  $columnFilters,
38        array  $validKeys,
39        array  $searchableCols,
40        string $generalQuery,
41    ): array {
42        $targetCol         = $relationInfo['target_col'];
43        $intermediateTable = $relationInfo['table'];
44        $sourceCol         = $relationInfo['source_col'];
45        $sourceRecordId    = $relationInfo['source_id'];
46
47        $conditions = [
48            sprintf(
49                't.`id` NOT IN (SELECT rel.`%s` FROM `%s` AS rel WHERE rel.`%s` = :source_id)',
50                $targetCol,
51                $intermediateTable,
52                $sourceCol
53            )
54        ];
55        $params = [':source_id' => $sourceRecordId];
56
57        $colIdx = 0;
58        foreach ($columnFilters as $colKey => $colVal) {
59            $trimmedVal = trim((string) $colVal);
60            if ($trimmedVal !== '' && isset($validKeys[$colKey])) {
61                $pName = ':cf_' . $colIdx++;
62                $conditions[] = sprintf(self::FMT_TARGET_COL . ' LIKE %s', $colKey, $pName);
63                $params[$pName] = '%' . $trimmedVal . '%';
64            }
65        }
66
67        if ($generalQuery !== '') {
68            $searchOrs = [];
69            foreach ($searchableCols as $idx => $col) {
70                $pName = ':gq_' . $idx;
71                $searchOrs[] = sprintf('%s LIKE %s', $col, $pName);
72                $params[$pName] = '%' . $generalQuery . '%';
73            }
74            $conditions[] = '(' . implode(' OR ', $searchOrs) . ')';
75        }
76
77        return ['WHERE ' . implode(' AND ', $conditions), $params];
78    }
79
80    /**
81     * Returns empty available payload when relation definition is missing.
82     *
83     * @param RelationMmMetadata|null $relation     Relation definition.
84     * @param ModuleMetadata          $targetModule Target module metadata.
85     * @param int                     $perPage      Pagination limit.
86     * @return array<string, mixed> Empty results payload.
87     */
88    public function buildEmptyAvailablePayload(
89        ?RelationMmMetadata $relation,
90        ModuleMetadata      $targetModule,
91        int                 $perPage,
92    ): array {
93        return [
94            'relation'       => $relation,
95            'target_module'  => $targetModule,
96            'fields'         => [],
97            'records'        => [],
98            'total'          => 0,
99            'page'           => 1,
100            'per_page'       => $perPage,
101            'total_pages'    => 1,
102            'sort_field'     => 'id',
103            'sort_order'     => 'DESC',
104            'column_filters' => [],
105            'query'          => '',
106        ];
107    }
108}