Passed
Push — master ( ab6f49...c762ff )
by
unknown
16:59 queued 08:07
created

SearchController   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 243
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 122
c 1
b 0
f 0
dl 0
loc 243
rs 9.84
wmc 32

6 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
C hydrateQuestionResultsWithQuizIds() 0 65 13
A xapianDemoIndexAction() 0 19 2
A xapianSearchAction() 0 34 3
A xapianSearchPageAction() 0 36 3
B hydrateResultsWithCourseRootNode() 0 39 10
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Controller;
8
9
use Chamilo\CoreBundle\Entity\Course;
10
use Chamilo\CoreBundle\Search\Xapian\XapianIndexService;
11
use Chamilo\CoreBundle\Search\Xapian\XapianSearchService;
12
use Chamilo\CourseBundle\Entity\CQuizRelQuestion;
13
use Doctrine\ORM\EntityManagerInterface;
14
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
15
use Symfony\Component\HttpFoundation\JsonResponse;
16
use Symfony\Component\HttpFoundation\Request;
17
use Symfony\Component\HttpFoundation\Response;
18
use Symfony\Component\Routing\Attribute\Route;
19
20
final class SearchController extends AbstractController
21
{
22
    public function __construct(
23
        private readonly XapianSearchService $xapianSearchService,
24
        private readonly XapianIndexService $xapianIndexService,
25
        private readonly EntityManagerInterface $em,
26
    ) {
27
    }
28
29
    /**
30
     * Minimal Xapian search endpoint returning JSON.
31
     *
32
     * Example: /search/xapian?q=test
33
     */
34
    #[Route(
35
        path: '/search/xapian',
36
        name: 'chamilo_core.search_xapian',
37
        methods: ['GET']
38
    )]
39
    public function xapianSearchAction(Request $request): JsonResponse
40
    {
41
        $q = \trim((string) $request->query->get('q', ''));
42
43
        if ($q === '') {
44
            return $this->json([
45
                'query'   => '',
46
                'total'   => 0,
47
                'results' => [],
48
            ]);
49
        }
50
51
        try {
52
            $result = $this->xapianSearchService->search(
53
                queryString: $q,
54
                offset: 0,
55
                length: 20,
56
            );
57
58
            return $this->json([
59
                'query'   => $q,
60
                'total'   => $result['count'],
61
                'results' => $result['results'],
62
            ]);
63
        } catch (\Throwable $e) {
64
            return $this->json([
65
                'query' => $q,
66
                'error' => $e->getMessage(),
67
            ], 500);
68
        }
69
    }
70
71
    /**
72
     * HTML search page using Xapian.
73
     *
74
     * Example: /search/xapian/ui?q=test
75
     */
76
    #[Route(
77
        path: '/search/xapian/ui',
78
        name: 'chamilo_core.search_xapian_ui',
79
        methods: ['GET']
80
    )]
81
    public function xapianSearchPageAction(Request $request): Response
82
    {
83
        $q = \trim((string) $request->query->get('q', ''));
84
85
        $total = 0;
86
        $results = [];
87
        $error = null;
88
89
        if ($q !== '') {
90
            try {
91
                $searchResult = $this->xapianSearchService->search(
92
                    queryString: $q,
93
                    offset: 0,
94
                    length: 20
95
                );
96
97
                $total   = $searchResult['count']   ?? 0;
98
                $results = $searchResult['results'] ?? [];
99
100
                $results = $this->hydrateResultsWithCourseRootNode($results);
101
                $results = $this->hydrateQuestionResultsWithQuizIds($results);
102
            } catch (\Throwable $e) {
103
                $error = $e->getMessage();
104
            }
105
        }
106
107
        return $this->render('@ChamiloCore/Search/xapian_search.html.twig', [
108
            'query'   => $q,
109
            'total'   => $total,
110
            'results' => $results,
111
            'error'   => $error,
112
        ]);
113
    }
114
115
    /**
116
     * Demo endpoint: index a sample document into Xapian.
117
     *
118
     * Call this once, then query /search/xapian?q=demo or ?q=chamilo.
119
     */
120
    #[Route(
121
        path: '/search/xapian/demo-index',
122
        name: 'chamilo_core.search_xapian_demo_index',
123
        methods: ['POST']
124
    )]
125
    public function xapianDemoIndexAction(): JsonResponse
126
    {
127
        try {
128
            $docId = $this->xapianIndexService->indexDemoDocument();
129
130
            return $this->json([
131
                'indexed' => true,
132
                'doc_id'  => $docId,
133
            ]);
134
        } catch (\Throwable $e) {
135
            return $this->json([
136
                'indexed' => false,
137
                'error'   => $e->getMessage(),
138
            ], 500);
139
        }
140
    }
141
142
    /**
143
     * Attach course_root_node_id to each result if we can resolve it from course_id.
144
     *
145
     * @param array<int, array<string, mixed>> $results
146
     *
147
     * @return array<int, array<string, mixed>>
148
     */
149
    private function hydrateResultsWithCourseRootNode(array $results): array
150
    {
151
        foreach ($results as &$result) {
152
            if (!\is_array($result)) {
153
                continue;
154
            }
155
156
            $data = $result['data'] ?? [];
157
            if (!\is_array($data)) {
158
                $data = [];
159
            }
160
161
            // If the field already exists (coming from the indexer), keep it.
162
            if (!empty($data['course_root_node_id'])) {
163
                $result['data'] = $data;
164
                continue;
165
            }
166
167
            $courseId = isset($data['course_id']) && $data['course_id'] !== ''
168
                ? (int) $data['course_id']
169
                : null;
170
171
            if (!$courseId) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $courseId of type integer|null is loosely compared to false; this is ambiguous if the integer can be 0. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
172
                $result['data'] = $data;
173
                continue;
174
            }
175
176
            /** @var Course|null $course */
177
            $course = $this->em->find(Course::class, $courseId);
178
            if (!$course || !$course->getResourceNode()) {
179
                $result['data'] = $data;
180
                continue;
181
            }
182
183
            $data['course_root_node_id'] = (string) $course->getResourceNode()->getId();
184
            $result['data'] = $data;
185
        }
186
187
        return $results;
188
    }
189
190
    /**
191
     * For question results, resolve the related quiz from c_quiz_rel_question
192
     * and attach quiz_id into the result data so the Twig can build the link.
193
     *
194
     * @param array<int, array<string, mixed>> $results
195
     *
196
     * @return array<int, array<string, mixed>>
197
     */
198
    private function hydrateQuestionResultsWithQuizIds(array $results): array
199
    {
200
        foreach ($results as &$result) {
201
            if (!\is_array($result)) {
202
                continue;
203
            }
204
205
            $data = $result['data'] ?? [];
206
            if (!\is_array($data)) {
207
                $data = [];
208
            }
209
210
            $kind = $data['kind'] ?? null;
211
            $tool = $data['tool'] ?? null;
212
213
            $isQuestion =
214
                ('question' === $kind)
215
                || ('quiz_question' === $tool);
216
217
            if (!$isQuestion) {
218
                $result['data'] = $data;
219
220
                continue;
221
            }
222
223
            if (!empty($data['quiz_id'])) {
224
                $result['data'] = $data;
225
226
                continue;
227
            }
228
229
            $questionId = isset($data['question_id']) && '' !== $data['question_id']
230
                ? (int) $data['question_id']
231
                : null;
232
233
            if (null === $questionId) {
234
                $result['data'] = $data;
235
236
                continue;
237
            }
238
239
            /** @var CQuizRelQuestion|null $rel */
240
            $rel = $this->em
241
                ->getRepository(CQuizRelQuestion::class)
242
                ->findOneBy(['question' => $questionId]);
243
244
            if (!$rel) {
245
                $result['data'] = $data;
246
247
                continue;
248
            }
249
250
            $quiz = $rel->getQuiz();
251
            if (!$quiz || null === $quiz->getIid()) {
252
                $result['data'] = $data;
253
254
                continue;
255
            }
256
257
            $data['quiz_id'] = (string) $quiz->getIid();
258
259
            $result['data'] = $data;
260
        }
261
262
        return $results;
263
    }
264
}
265