Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.33% covered (success)
83.33%
25 / 30
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
TagController
83.33% covered (success)
83.33%
25 / 30
50.00% covered (danger)
50.00%
1 / 2
5.12
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
82.76% covered (success)
82.76%
24 / 29
0.00% covered (danger)
0.00%
0 / 1
4.08
1<?php
2
3/**
4 * The Tags 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-29
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Controller\Api;
21
22use OpenApi\Attributes as OA;
23use phpMyFAQ\Tags;
24use phpMyFAQ\User\CurrentUser;
25use Symfony\Component\HttpFoundation\JsonResponse;
26use Symfony\Component\HttpFoundation\Request;
27use Symfony\Component\Routing\Attribute\Route;
28
29final class TagController extends AbstractApiController
30{
31    public function __construct(
32        private readonly Tags $tags,
33    ) {
34        parent::__construct();
35    }
36
37    /**
38     * @throws \Exception
39     */
40    #[OA\Get(
41        path: '/api/v4.0/tags',
42        operationId: 'getTags',
43        description: 'Returns paginated tags.',
44        tags: ['Public Endpoints'],
45    )]
46    #[OA\Parameter(
47        name: 'page',
48        description: 'Page number for pagination (page-based)',
49        in: 'query',
50        required: false,
51        schema: new OA\Schema(type: 'integer', default: 1),
52    )]
53    #[OA\Parameter(
54        name: 'per_page',
55        description: 'Items per page (page-based, max 100)',
56        in: 'query',
57        required: false,
58        schema: new OA\Schema(type: 'integer', default: 25),
59    )]
60    #[OA\Parameter(
61        name: 'limit',
62        description: 'Number of items to return (offset-based, max 100)',
63        in: 'query',
64        required: false,
65        schema: new OA\Schema(type: 'integer', default: 25),
66    )]
67    #[OA\Parameter(
68        name: 'offset',
69        description: 'Starting offset (offset-based)',
70        in: 'query',
71        required: false,
72        schema: new OA\Schema(type: 'integer', default: 0),
73    )]
74    #[OA\Parameter(
75        name: 'sort',
76        description: 'Field to sort by',
77        in: 'query',
78        required: false,
79        schema: new OA\Schema(type: 'string', default: 'tagFrequency', enum: ['tagId', 'tagName', 'tagFrequency']),
80    )]
81    #[OA\Parameter(
82        name: 'order',
83        description: 'Sort direction',
84        in: 'query',
85        required: false,
86        schema: new OA\Schema(type: 'string', default: 'desc', enum: ['asc', 'desc']),
87    )]
88    #[OA\Response(response: 200, description: 'Returns paginated tags.', content: new OA\JsonContent(example: [
89        'success' => true,
90        'data' => [
91            ['tagId' => 4, 'tagName' => 'phpMyFAQ', 'tagFrequency' => 3],
92            ['tagId' => 1, 'tagName' => 'PHP 8', 'tagFrequency' => 2],
93        ],
94        'meta' => [
95            'pagination' => [
96                'total' => 50,
97                'count' => 25,
98                'per_page' => 25,
99                'current_page' => 1,
100                'total_pages' => 2,
101                'links' => [
102                    'first' => '/api/v4.0/tags?page=1&per_page=25',
103                    'last' => '/api/v4.0/tags?page=2&per_page=25',
104                    'prev' => null,
105                    'next' => '/api/v4.0/tags?page=2&per_page=25',
106                ],
107            ],
108            'sorting' => [
109                'field' => 'tagFrequency',
110                'order' => 'desc',
111            ],
112        ],
113    ]))]
114    #[Route(path: 'v4.0/tags', name: 'api.tags.list', methods: ['GET'])]
115    public function list(?Request $request = null): JsonResponse
116    {
117        $request ??= Request::createFromGlobals();
118
119        [$currentUser, $currentGroups] = CurrentUser::getCurrentUserGroupId($this->currentUser);
120        $this->tags->setUser($currentUser);
121        $this->tags->setGroups($currentGroups);
122
123        // Get pagination and sorting parameters
124        $pagination = $this->getPaginationRequest($request);
125        $sort = $this->getSortRequest(
126            $request,
127            allowedFields: ['tagId', 'tagName', 'tagFrequency'],
128            defaultField: 'tagFrequency',
129            defaultOrder: 'desc',
130        );
131
132        // Get all tags (we'll use a high limit to get all tags)
133        $allTags = $this->tags->getPopularTagsAsArray(limit: 1000);
134        $total = is_countable($allTags) ? count($allTags) : 0;
135
136        // Apply sorting if needed
137        if ($sort->getField()) {
138            usort($allTags, static function ($a, $b) use ($sort) {
139                $field = (string) $sort->getField();
140                $aVal = $a[$field] ?? '';
141                $bVal = $b[$field] ?? '';
142                $result = $aVal <=> $bVal;
143                return $sort->getOrderSql() === 'DESC' ? -$result : $result;
144            });
145        }
146
147        // Apply pagination
148        $result = array_slice($allTags, $pagination->offset, $pagination->limit);
149
150        return $this->paginatedResponse(
151            $request,
152            data: array_values($result),
153            total: $total,
154            pagination: $pagination,
155            options: new PaginatedResponseOptions(sort: $sort),
156        );
157    }
158}