Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.97% covered (success)
96.97%
32 / 33
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
CommentController
96.97% covered (success)
96.97%
32 / 33
50.00% covered (danger)
50.00%
1 / 2
4
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
 list
96.88% covered (success)
96.88%
31 / 32
0.00% covered (danger)
0.00%
0 / 1
3
1<?php
2
3/**
4 * The Comment Controller for the REST API
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2023-2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2023-07-30
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Controller\Api;
21
22use Exception;
23use LogicException;
24use OpenApi\Attributes as OA;
25use phpMyFAQ\Comments;
26use phpMyFAQ\Entity\CommentType;
27use phpMyFAQ\Faq;
28use phpMyFAQ\Filter;
29use phpMyFAQ\User\CurrentUser;
30use Symfony\Component\HttpFoundation\JsonResponse;
31use Symfony\Component\HttpFoundation\Request;
32use Symfony\Component\HttpFoundation\Response;
33use Symfony\Component\Routing\Attribute\Route;
34
35final class CommentController extends AbstractApiController
36{
37    public function __construct(
38        private readonly Comments $comments,
39    ) {
40        parent::__construct();
41    }
42
43    /**
44     * @throws Exception
45     */
46    #[OA\Get(
47        path: '/api/v4.0/comments/{faqId}',
48        operationId: 'getComments',
49        description: 'Returns a paginated list of comments for a given FAQ record ID.',
50        tags: ['Public Endpoints'],
51    )]
52    #[OA\Header(
53        header: 'Accept-Language',
54        description: 'The language code for the login.',
55        schema: new OA\Schema(type: 'string'),
56    )]
57    #[OA\Parameter(
58        name: 'faqId',
59        description: 'The FAQ record ID.',
60        in: 'path',
61        required: true,
62        schema: new OA\Schema(type: 'integer'),
63    )]
64    #[OA\Parameter(
65        name: 'page',
66        description: 'Page number for pagination (page-based)',
67        in: 'query',
68        required: false,
69        schema: new OA\Schema(type: 'integer', default: 1),
70    )]
71    #[OA\Parameter(
72        name: 'per_page',
73        description: 'Items per page (page-based, max 100)',
74        in: 'query',
75        required: false,
76        schema: new OA\Schema(type: 'integer', default: 25),
77    )]
78    #[OA\Parameter(
79        name: 'limit',
80        description: 'Number of items to return (offset-based, max 100)',
81        in: 'query',
82        required: false,
83        schema: new OA\Schema(type: 'integer', default: 25),
84    )]
85    #[OA\Parameter(
86        name: 'offset',
87        description: 'Starting offset (offset-based)',
88        in: 'query',
89        required: false,
90        schema: new OA\Schema(type: 'integer', default: 0),
91    )]
92    #[OA\Parameter(name: 'sort', description: 'Field to sort by', in: 'query', required: false, schema: new OA\Schema(
93        type: 'string',
94        default: 'id_comment',
95        enum: ['id_comment', 'id', 'usr', 'datum'],
96    ))]
97    #[OA\Parameter(
98        name: 'order',
99        description: 'Sort direction',
100        in: 'query',
101        required: false,
102        schema: new OA\Schema(type: 'string', default: 'asc', enum: ['asc', 'desc']),
103    )]
104    #[OA\Response(
105        response: 200,
106        description: 'Returns paginated comments for the FAQ.',
107        content: new OA\JsonContent(example: [
108            'success' => true,
109            'data' => [[
110                'id' => 2,
111                'recordId' => 142,
112                'categoryId' => null,
113                'type' => 'faq',
114                'username' => 'phpMyFAQ User',
115                'comment' => 'Foo! Bar?',
116                'date' => '2019-12-24T12:24:57+0100',
117                'helped' => null,
118            ]],
119            'meta' => [
120                'pagination' => [
121                    'total' => 50,
122                    'count' => 25,
123                    'per_page' => 25,
124                    'current_page' => 1,
125                    'total_pages' => 2,
126                    'links' => [
127                        'first' => '/api/v4.0/comments/142?page=1&per_page=25',
128                        'last' => '/api/v4.0/comments/142?page=2&per_page=25',
129                        'prev' => null,
130                        'next' => '/api/v4.0/comments/142?page=2&per_page=25',
131                    ],
132                ],
133                'sorting' => [
134                    'field' => 'id_comment',
135                    'order' => 'asc',
136                ],
137            ],
138        ]),
139    )]
140    #[Route(path: 'v4.0/comments/{recordId}', name: 'api.comments', methods: ['GET'])]
141    public function list(Request $request): JsonResponse
142    {
143        $recordId = (int) Filter::filterVar($request->attributes->get(key: 'recordId'), FILTER_VALIDATE_INT);
144
145        // Get pagination and sorting parameters
146        $pagination = $this->getPaginationRequest($request);
147        $sort = $this->getSortRequest(
148            $request,
149            allowedFields: ['id_comment', 'id', 'usr', 'datum'],
150            defaultField: 'id_comment',
151            defaultOrder: 'asc',
152        );
153
154        // Do not disclose comments of a FAQ record the requester is not allowed to see.
155        [$currentUser, $currentGroups] = CurrentUser::getCurrentUserGroupId($this->currentUser);
156        $faq = $this->container->get(id: 'phpmyfaq.faq');
157        if (!$faq instanceof Faq) {
158            throw new LogicException('Faq service not found in container.');
159        }
160
161        $faq->setUser($currentUser);
162        $faq->setGroups($currentGroups);
163
164        if (!$faq->isFaqAccessibleForUser($recordId)) {
165            return $this->json([], Response::HTTP_NOT_FOUND);
166        }
167
168        // Get paginated comments
169        $result = $this->comments->getCommentsDataPaginated(
170            referenceId: $recordId,
171            type: CommentType::FAQ,
172            limit: $pagination->limit,
173            offset: $pagination->offset,
174            sortField: $sort->getField() ?? 'id_comment',
175            sortOrder: $sort->getOrderSql(),
176        );
177
178        // Get total count
179        $total = $this->comments->countComments($recordId, CommentType::FAQ);
180
181        return $this->paginatedResponse(
182            $request,
183            data: $result,
184            total: $total,
185            pagination: $pagination,
186            options: new PaginatedResponseOptions(sort: $sort),
187        );
188    }
189}