Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.18% covered (warning)
87.18%
34 / 39
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
GlobalSearchApiController
86.84% covered (warning)
86.84%
33 / 38
50.00% covered (danger)
50.00%
2 / 4
14.45
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
 search
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
4
 executeSearch
78.57% covered (warning)
78.57%
11 / 14
0.00% covered (danger)
0.00%
0 / 1
7.48
 jsonResponse
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
2.26
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\Search\Presentation\Api;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Engine\Application\Security\PermissionContextFactory;
12use App\Core\Engine\Domain\Model\PermissionContext;
13use App\Core\Search\Application\Service\GlobalSearchServiceInterface;
14use App\Core\Search\Domain\Exception\SearchException;
15use App\Core\Search\Domain\Model\SearchMode;
16use App\Core\Search\Domain\Model\SearchQuery;
17use JsonException;
18use Psr\Http\Message\ResponseFactoryInterface;
19use Psr\Http\Message\ResponseInterface;
20use Psr\Http\Message\ServerRequestInterface;
21
22/**
23 * Global Search REST API Controller.
24 *
25 * Exposes GET /api/v1/search endpoint for querying records across all accessible CRUD modules.
26 *
27 * @package App\Core\Search\Presentation\Api
28 */
29final readonly class GlobalSearchApiController
30{
31    /**
32     * GlobalSearchApiController constructor.
33     *
34     * @param GlobalSearchServiceInterface $searchService  Search application service.
35     * @param PermissionContextFactory     $contextFactory Security context factory.
36     * @param ResponseFactoryInterface     $factory        PSR-7 response factory.
37     */
38    public function __construct(
39        private GlobalSearchServiceInterface $searchService,
40        private PermissionContextFactory     $contextFactory,
41        private ResponseFactoryInterface     $factory
42    ) {
43    }
44
45    /**
46     * Handles search execution request.
47     *
48     * @param ServerRequestInterface $request HTTP server request.
49     * @return ResponseInterface JSON API response.
50     */
51    public function search(ServerRequestInterface $request): ResponseInterface
52    {
53        $context = $this->contextFactory->createFromRequest($request);
54        if (!$context->isAuthenticated()) {
55            return $this->jsonResponse(['success' => false, 'error' => 'Authentication required.'], 401);
56        }
57
58        $params = $request->getQueryParams();
59        $term = isset($params['q']) ? trim((string) $params['q']) : '';
60
61        if ($term === '') {
62            return $this->jsonResponse([
63                'success' => true,
64                'data'    => [
65                    'query'             => '',
66                    'mode'              => 'smart',
67                    'total_results'     => 0,
68                    'modules_count'     => 0,
69                    'execution_time_ms' => 0.0,
70                    'groups'            => [],
71                ],
72            ]);
73        }
74
75        return $this->executeSearch($params, $term, $context);
76    }
77
78    /**
79     * Executes search query with parsed options and context.
80     *
81     * @param array<string, mixed> $params  Query parameters.
82     * @param string               $term    Sanitized search term.
83     * @param PermissionContext    $context User permission context.
84     * @return ResponseInterface JSON API response.
85     */
86    private function executeSearch(array $params, string $term, PermissionContext $context): ResponseInterface
87    {
88        $modeStr = isset($params['mode']) ? (string) $params['mode'] : 'smart';
89        $mode = SearchMode::tryFrom($modeStr) ?? SearchMode::SMART;
90
91        $modules = [];
92        if (isset($params['modules']) && is_string($params['modules']) && $params['modules'] !== '') {
93            $modules = array_filter(array_map('trim', explode(',', $params['modules'])));
94        }
95
96        $limit = isset($params['limit']) ? (int) $params['limit'] : SearchQuery::DEFAULT_LIMIT_PER_MODULE;
97
98        try {
99            $query = new SearchQuery($term, $mode, array_values($modules), $limit);
100            $resultSet = $this->searchService->search($query, $context);
101
102            return $this->jsonResponse([
103                'success' => true,
104                'data'    => $resultSet->jsonSerialize(),
105            ]);
106        } catch (SearchException $e) {
107            return $this->jsonResponse(['success' => false, 'error' => $e->getMessage()], 400);
108        }
109    }
110
111    /**
112     * Builds standard JSON response.
113     *
114     * @param array<string, mixed> $payload Response data.
115     * @param int                  $status  HTTP status code.
116     * @return ResponseInterface Formatted PSR-7 response.
117     */
118    private function jsonResponse(array $payload, int $status = 200): ResponseInterface
119    {
120        $response = $this->factory->createResponse($status);
121        try {
122            $response->getBody()->write((string) json_encode($payload, JSON_THROW_ON_ERROR));
123        } catch (JsonException) {
124            $response->getBody()->write('{"success":false,"error":"JSON encoding error"}');
125        }
126
127        return $response->withHeader('Content-Type', 'application/json; charset=UTF-8');
128    }
129}