Skip to content

Commit 287c544

Browse files
committed
feat(api): switched to REST API v4.0
1 parent 6593058 commit 287c544

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1695
-765
lines changed

docs/openapi.json

Lines changed: 611 additions & 76 deletions
Large diffs are not rendered by default.

docs/openapi.yaml

Lines changed: 469 additions & 76 deletions
Large diffs are not rendered by default.

phpmyfaq/api/index.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?php
22

33
/**
4-
* phpMyFAQ REST API: api/v3.2
4+
* phpMyFAQ REST API: api/v4.0
55
*
66
* This Source Code Form is subject to the terms of the Mozilla Public License,
77
* v. 2.0. If a copy of the MPL was not distributed with this file, You can

phpmyfaq/src/phpMyFAQ/Controller/AbstractController.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
use Twig\TwigFilter;
4747

4848
#[OA\Info(
49-
version: '3.2',
49+
version: '4.0',
5050
description: 'phpMyFAQ includes a REST API and offers APIs for various services like fetching the phpMyFAQ '
5151
. 'version or doing a search against the phpMyFAQ installation.',
5252
title: 'REST API for phpMyFAQ 4.2',

phpmyfaq/src/phpMyFAQ/Controller/Api/AbstractApiController.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919

2020
namespace phpMyFAQ\Controller\Api;
2121

22+
use Exception;
23+
use Override;
2224
use phpMyFAQ\Api\Filtering\FilterRequest;
2325
use phpMyFAQ\Api\Pagination\PaginationMetadata;
2426
use phpMyFAQ\Api\Pagination\PaginationRequest;
@@ -43,8 +45,10 @@ abstract class AbstractApiController extends AbstractController
4345

4446
/**
4547
* Initializes API controller and verifies API access is enabled.
48+
*
49+
* @throws Exception
4650
*/
47-
#[\Override]
51+
#[Override]
4852
protected function initializeFromContainer(): void
4953
{
5054
parent::initializeFromContainer();
@@ -56,10 +60,10 @@ protected function initializeFromContainer(): void
5660

5761
/**
5862
* Parses pagination parameters from the request
59-
*
6063
* Supports both page-based (page + per_page) and offset-based (limit + offset) pagination.
6164
*
62-
* @param int $defaultPerPage Default items per page
65+
* @param Request $request
66+
* @param int $defaultPerPage Default items per page
6367
* @param int|null $maxPerPage Maximum items per page (uses class constant if null)
6468
* @return PaginationRequest
6569
*/

phpmyfaq/src/phpMyFAQ/Controller/Api/AttachmentController.php

Lines changed: 45 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
final class AttachmentController extends AbstractApiController
3232
{
3333
#[OA\Get(
34-
path: '/api/v3.2/attachments/{faqId}',
34+
path: '/api/v4.0/attachments/{faqId}',
3535
operationId: 'getAttachments',
3636
description: 'Returns a paginated list of attachments for a given FAQ record ID with optional sorting.',
3737
tags: ['Public Endpoints'],
@@ -93,48 +93,54 @@ final class AttachmentController extends AbstractApiController
9393
#[OA\Response(
9494
response: 200,
9595
description: 'Paginated list of attachments with metadata.',
96-
content: new OA\JsonContent(example: '{
97-
"success": true,
98-
"data": [
99-
{
100-
"filename": "attachment-1.pdf",
101-
"url": "https://www.example.org/attachment/1"
102-
},
103-
{
104-
"filename": "attachment-2.pdf",
105-
"url": "https://www.example.org/attachment/2"
106-
}
96+
content: new OA\JsonContent(example: [
97+
'success' => true,
98+
'data' => [
99+
[
100+
'filename' => 'attachment-1.pdf',
101+
'url' => 'https://www.example.org/attachment/1',
102+
],
103+
[
104+
'filename' => 'attachment-2.pdf',
105+
'url' => 'https://www.example.org/attachment/2',
106+
],
107107
],
108-
"meta": {
109-
"pagination": {
110-
"total": 2,
111-
"count": 2,
112-
"per_page": 25,
113-
"current_page": 1,
114-
"total_pages": 1,
115-
"offset": 0,
116-
"has_more": false,
117-
"has_previous": false,
118-
"links": {
119-
"first": "/api/v3.2/attachments/1?page=1&per_page=25",
120-
"last": "/api/v3.2/attachments/1?page=1&per_page=25",
121-
"prev": null,
122-
"next": null
123-
}
124-
},
125-
"sorting": {
126-
"field": "filename",
127-
"order": "asc"
128-
}
129-
}
130-
}'),
108+
'meta' => [
109+
'pagination' => [
110+
'total' => 2,
111+
'count' => 2,
112+
'per_page' => 25,
113+
'current_page' => 1,
114+
'total_pages' => 1,
115+
'offset' => 0,
116+
'has_more' => false,
117+
'has_previous' => false,
118+
'links' => [
119+
'first' => '/api/v4.0/attachments/1?page=1&per_page=25',
120+
'last' => '/api/v4.0/attachments/1?page=1&per_page=25',
121+
'prev' => null,
122+
'next' => null,
123+
],
124+
],
125+
'sorting' => [
126+
'field' => 'filename',
127+
'order' => 'asc',
128+
],
129+
],
130+
]),
131131
)]
132132
#[OA\Response(
133-
response: 404,
134-
description: 'If the FAQ has no attachments.',
135-
content: new OA\JsonContent(example: '{"success": true, "data": [], "meta": {"pagination": {"total": 0}}}'),
133+
response: 500,
134+
description: 'If the attachments cannot be fetched.',
135+
content: new OA\JsonContent(example: [
136+
'success' => false,
137+
'error' => [
138+
'code' => 'ATTACHMENT_ERROR',
139+
'message' => 'Failed to fetch attachments',
140+
],
141+
]),
136142
)]
137-
#[Route(path: 'v3.2/attachments/{faqId}', name: 'api.attachments', methods: ['GET'])]
143+
#[Route(path: 'v4.0/attachments/{faqId}', name: 'api.attachments', methods: ['GET'])]
138144
public function list(Request $request): JsonResponse
139145
{
140146
$faqId = (int) Filter::filterVar($request->attributes->get(key: 'faqId'), FILTER_VALIDATE_INT);

phpmyfaq/src/phpMyFAQ/Controller/Api/BackupController.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public function __construct()
4848
/**
4949
* @throws Exception
5050
*/
51-
#[OA\Get(path: '/api/v3.2/backup/{type}', operationId: 'createBackup', tags: ['Endpoints with Authentication'])]
51+
#[OA\Get(path: '/api/v4.0/backup/{type}', operationId: 'createBackup', tags: ['Endpoints with Authentication'])]
5252
#[OA\Header(
5353
header: 'Accept-Language',
5454
description: 'The language code for the login.',
@@ -83,7 +83,7 @@ public function __construct()
8383
response: 401,
8484
description: 'If the user is not authenticated and/or does not have sufficient permissions.',
8585
)]
86-
#[Route(path: 'v3.2/backup/{type}', name: 'api.backup', methods: ['GET'])]
86+
#[Route(path: 'v4.0/backup/{type}', name: 'api.backup', methods: ['GET'])]
8787
public function download(Request $request): Response
8888
{
8989
$this->userHasPermission(PermissionType::BACKUP);

phpmyfaq/src/phpMyFAQ/Controller/Api/CategoryController.php

Lines changed: 49 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public function setOrderFactory(callable $orderFactory): void
6868
/**
6969
* @throws \Exception
7070
*/
71-
#[OA\Get(path: '/api/v3.2/categories', operationId: 'getCategories', tags: ['Public Endpoints'])]
71+
#[OA\Get(path: '/api/v4.0/categories', operationId: 'getCategories', tags: ['Public Endpoints'])]
7272
#[OA\Header(
7373
header: 'Accept-Language',
7474
description: 'The language code for the categories.',
@@ -117,45 +117,43 @@ enum: ['id', 'name', 'parent_id', 'active'],
117117
#[OA\Response(
118118
response: 200,
119119
description: 'Returns paginated categories for the given language provided by "Accept-Language".',
120-
content: new OA\JsonContent(example: '{
121-
"success": true,
122-
"data": [
123-
{
124-
"id": 1,
125-
"lang": "en",
126-
"parent_id": 0,
127-
"name": "Test",
128-
"description": "Hello, World! Hello, Tests!",
129-
"user_id": 1,
130-
"group_id": 1,
131-
"active": 1,
132-
"show_home": 1,
133-
"image": "category-1-en.png",
134-
"level": 1
135-
}
120+
content: new OA\JsonContent(example: [
121+
'success' => true,
122+
'data' => [[
123+
'id' => 1,
124+
'lang' => 'en',
125+
'parent_id' => 0,
126+
'name' => 'Test',
127+
'description' => 'Hello, World! Hello, Tests!',
128+
'user_id' => 1,
129+
'group_id' => 1,
130+
'active' => 1,
131+
'show_home' => 1,
132+
'image' => 'category-1-en.png',
133+
'level' => 1,
134+
]],
135+
'meta' => [
136+
'pagination' => [
137+
'total' => 50,
138+
'count' => 25,
139+
'per_page' => 25,
140+
'current_page' => 1,
141+
'total_pages' => 2,
142+
'links' => [
143+
'first' => '/api/v4.0/categories?page=1&per_page=25',
144+
'last' => '/api/v4.0/categories?page=2&per_page=25',
145+
'prev' => null,
146+
'next' => '/api/v4.0/categories?page=2&per_page=25',
147+
],
148+
],
149+
'sorting' => [
150+
'field' => 'id',
151+
'order' => 'asc',
152+
],
136153
],
137-
"meta": {
138-
"pagination": {
139-
"total": 50,
140-
"count": 25,
141-
"per_page": 25,
142-
"current_page": 1,
143-
"total_pages": 2,
144-
"links": {
145-
"first": "/api/v3.2/categories?page=1&per_page=25",
146-
"last": "/api/v3.2/categories?page=2&per_page=25",
147-
"prev": null,
148-
"next": "/api/v3.2/categories?page=2&per_page=25"
149-
}
150-
},
151-
"sorting": {
152-
"field": "id",
153-
"order": "asc"
154-
}
155-
}
156-
}'),
154+
]),
157155
)]
158-
#[Route(path: 'v3.2/categories', name: 'api.categories.list', methods: ['GET'])]
156+
#[Route(path: 'v4.0/categories', name: 'api.categories.list', methods: ['GET'])]
159157
public function list(?Request $request = null): JsonResponse
160158
{
161159
$request ??= Request::createFromGlobals();
@@ -204,7 +202,7 @@ public function list(?Request $request = null): JsonResponse
204202
* @throws JsonException
205203
* @throws \Exception
206204
*/
207-
#[OA\Post(path: '/api/v3.2/category', operationId: 'createCategory', tags: ['Endpoints with Authentication'])]
205+
#[OA\Post(path: '/api/v4.0/category', operationId: 'createCategory', tags: ['Endpoints with Authentication'])]
208206
#[OA\Header(
209207
header: 'Accept-Language',
210208
description: 'The language code for the login.',
@@ -260,25 +258,23 @@ public function list(?Request $request = null): JsonResponse
260258
}',
261259
),
262260
)]
263-
#[OA\Response(
264-
response: 201,
265-
description: 'If all posted data is correct.',
266-
content: new OA\JsonContent(example: '{ "stored": true }'),
267-
)]
268-
#[OA\Response(
269-
response: 400,
270-
description: "If something didn't worked out.",
271-
content: new OA\JsonContent(example: '{ "stored": false, "error": "Cannot add category" }'),
272-
)]
261+
#[OA\Response(response: 201, description: 'If all posted data is correct.', content: new OA\JsonContent(example: [
262+
'stored' => true,
263+
]))]
264+
#[OA\Response(response: 400, description: "If something didn't worked out.", content: new OA\JsonContent(example: [
265+
'stored' => false,
266+
'error' => 'Cannot add category',
267+
]))]
273268
#[OA\Response(
274269
response: 409,
275270
description: 'If the parent category name cannot be mapped.',
276-
content: new OA\JsonContent(
277-
example: '{ "stored": false, "error": "The given parent category name was not found." }',
278-
),
271+
content: new OA\JsonContent(example: [
272+
'stored' => false,
273+
'error' => 'The given parent category name was not found.',
274+
]),
279275
)]
280276
#[OA\Response(response: 401, description: 'If the user is not authenticated.')]
281-
#[Route(path: 'v3.2/category', name: 'api.category.create', methods: ['POST'])]
277+
#[Route(path: 'v4.0/category', name: 'api.category.create', methods: ['POST'])]
282278
public function create(Request $request): JsonResponse
283279
{
284280
$this->hasValidToken();

phpmyfaq/src/phpMyFAQ/Controller/Api/CommentController.php

Lines changed: 37 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public function __construct(
4040
* @throws Exception
4141
*/
4242
#[OA\Get(
43-
path: '/api/v3.2/comments/{faqId}',
43+
path: '/api/v4.0/comments/{faqId}',
4444
operationId: 'getComments',
4545
description: 'Returns a paginated list of comments for a given FAQ record ID.',
4646
tags: ['Public Endpoints'],
@@ -97,43 +97,43 @@ enum: ['id_comment', 'id', 'usr', 'datum'],
9797
required: false,
9898
schema: new OA\Schema(type: 'string', default: 'asc', enum: ['asc', 'desc']),
9999
)]
100-
#[OA\Response(response: 200, description: 'Returns paginated comments for the FAQ.', content: new OA\JsonContent(
101-
example: '{
102-
"success": true,
103-
"data": [
104-
{
105-
"id": 2,
106-
"recordId": 142,
107-
"categoryId": null,
108-
"type": "faq",
109-
"username": "phpMyFAQ User",
110-
"comment": "Foo! Bar?",
111-
"date": "2019-12-24T12:24:57+0100",
112-
"helped": null
113-
}
100+
#[OA\Response(
101+
response: 200,
102+
description: 'Returns paginated comments for the FAQ.',
103+
content: new OA\JsonContent(example: [
104+
'success' => true,
105+
'data' => [[
106+
'id' => 2,
107+
'recordId' => 142,
108+
'categoryId' => null,
109+
'type' => 'faq',
110+
'username' => 'phpMyFAQ User',
111+
'comment' => 'Foo! Bar?',
112+
'date' => '2019-12-24T12:24:57+0100',
113+
'helped' => null,
114+
]],
115+
'meta' => [
116+
'pagination' => [
117+
'total' => 50,
118+
'count' => 25,
119+
'per_page' => 25,
120+
'current_page' => 1,
121+
'total_pages' => 2,
122+
'links' => [
123+
'first' => '/api/v4.0/comments/142?page=1&per_page=25',
124+
'last' => '/api/v4.0/comments/142?page=2&per_page=25',
125+
'prev' => null,
126+
'next' => '/api/v4.0/comments/142?page=2&per_page=25',
127+
],
128+
],
129+
'sorting' => [
130+
'field' => 'id_comment',
131+
'order' => 'asc',
132+
],
114133
],
115-
"meta": {
116-
"pagination": {
117-
"total": 50,
118-
"count": 25,
119-
"per_page": 25,
120-
"current_page": 1,
121-
"total_pages": 2,
122-
"links": {
123-
"first": "/api/v3.2/comments/142?page=1&per_page=25",
124-
"last": "/api/v3.2/comments/142?page=2&per_page=25",
125-
"prev": null,
126-
"next": "/api/v3.2/comments/142?page=2&per_page=25"
127-
}
128-
},
129-
"sorting": {
130-
"field": "id_comment",
131-
"order": "asc"
132-
}
133-
}
134-
}',
135-
))]
136-
#[Route(path: 'v3.2/comments/{recordId}', name: 'api.comments', methods: ['GET'])]
134+
]),
135+
)]
136+
#[Route(path: 'v4.0/comments/{recordId}', name: 'api.comments', methods: ['GET'])]
137137
public function list(Request $request): JsonResponse
138138
{
139139
$recordId = (int) Filter::filterVar($request->attributes->get(key: 'recordId'), FILTER_VALIDATE_INT);

0 commit comments

Comments
 (0)