Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
88.89% covered (warning)
88.89%
72 / 81
66.67% covered (warning)
66.67%
10 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
ModuleBuilderApiController
88.75% covered (warning)
88.75%
71 / 80
66.67% covered (warning)
66.67%
10 / 15
34.55
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
 metadataOptions
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 actionMetadataOptions
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
2.50
 schema
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 actionGetSchema
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 suggestField
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 actionSuggestField
70.00% covered (warning)
70.00%
7 / 10
0.00% covered (danger)
0.00%
0 / 1
4.43
 create
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 actionCreateModule
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
6
 update
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 actionUpdateModule
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
2.04
 delete
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 actionDeleteModule
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
4.01
 parseRequestBody
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 resolvePermissionContext
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
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\ModuleBuilder\Presentation\Api;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Engine\Domain\Model\PermissionContext;
12use App\Core\ModuleBuilder\Application\Service\ModuleBuilderServiceInterface;
13use App\Core\ModuleBuilder\Domain\Model\ModuleDefinitionDto;
14use App\Core\ModuleBuilder\Domain\Service\UiTypeMetadataCatalog;
15use App\Shared\Infrastructure\Http\ApiResponseTrait;
16use Nyholm\Psr7\Factory\Psr17Factory;
17use Psr\Http\Message\ResponseInterface;
18use Psr\Http\Message\ServerRequestInterface;
19use Throwable;
20
21/**
22 * REST API Controller serving module builder endpoints (/api/v1/system/module-builder/...).
23 *
24 * @package App\Core\ModuleBuilder\Presentation\Api
25 */
26final readonly class ModuleBuilderApiController
27{
28    use ApiResponseTrait;
29
30    /**
31     * @param ModuleBuilderServiceInterface $builderService Module builder service.
32     * @param Psr17Factory                  $psr17          PSR-17 response factory.
33     */
34    public function __construct(
35        private ModuleBuilderServiceInterface $builderService,
36        private Psr17Factory $psr17
37    ) {
38    }
39
40    /**
41     * GET /api/v1/system/module-builder/metadata-options
42     */
43    public function metadataOptions(
44        ServerRequestInterface $request,
45        ?PermissionContext $context = null
46    ): ResponseInterface {
47        return $this->actionMetadataOptions($context ?? $this->resolvePermissionContext($request));
48    }
49
50    public function actionMetadataOptions(
51        PermissionContext $context
52    ): ResponseInterface {
53        try {
54            $data = $this->builderService->getBuilderMetadata($context);
55
56            return $this->jsonSuccess($this->psr17, $data);
57        } catch (Throwable $e) {
58            return $this->jsonError($this->psr17, $e->getMessage(), 500);
59        }
60    }
61
62    /**
63     * GET /api/v1/system/module-builder/schema/{id}
64     */
65    public function schema(
66        int $id,
67        ServerRequestInterface $request,
68        ?PermissionContext $context = null
69    ): ResponseInterface {
70        return $this->actionGetSchema($id, $context ?? $this->resolvePermissionContext($request));
71    }
72
73    public function actionGetSchema(
74        int $id,
75        PermissionContext $context
76    ): ResponseInterface {
77        try {
78            $definition = $this->builderService->getModuleDefinition($id, $context);
79
80            return $this->jsonSuccess($this->psr17, $definition->toArray());
81        } catch (Throwable $e) {
82            return $this->jsonError($this->psr17, $e->getMessage(), 404);
83        }
84    }
85
86    /**
87     * POST /api/v1/system/module-builder/suggest-field
88     */
89    public function suggestField(
90        ServerRequestInterface $request
91    ): ResponseInterface {
92        return $this->actionSuggestField($request);
93    }
94
95    public function actionSuggestField(
96        ServerRequestInterface $request
97    ): ResponseInterface {
98        try {
99            $body = $this->parseRequestBody($request);
100            $label = trim((string) ($body['label'] ?? ''));
101            $uitype = trim((string) ($body['uitype'] ?? $body['uitype_name'] ?? ''));
102            $alias = trim((string) ($body['table_alias'] ?? 't'));
103
104            if ($label === '') {
105                return $this->jsonError($this->psr17, 'Field label is required for suggestion.', 422);
106            }
107
108            $suggestion = UiTypeMetadataCatalog::suggestFor($label, $uitype !== '' ? $uitype : null, $alias);
109
110            return $this->jsonSuccess($this->psr17, $suggestion);
111        } catch (Throwable $e) {
112            return $this->jsonError($this->psr17, $e->getMessage(), 400);
113        }
114    }
115
116    /**
117     * POST /api/v1/system/module-builder/create
118     */
119    public function create(
120        ServerRequestInterface $request,
121        ?PermissionContext $context = null
122    ): ResponseInterface {
123        return $this->actionCreateModule($request, $context ?? $this->resolvePermissionContext($request));
124    }
125
126    public function actionCreateModule(
127        ServerRequestInterface $request,
128        PermissionContext $context
129    ): ResponseInterface {
130        try {
131            $body = $this->parseRequestBody($request);
132            if (empty($body) || (empty($body['basic']) && empty($body['basic_info']) && empty($body['name']))) {
133                return $this->jsonError($this->psr17, 'Module definition payload is required.', 422);
134            }
135            $definition = ModuleDefinitionDto::fromArray($body);
136
137            $moduleId = $this->builderService->createModule($definition, $context);
138
139            return $this->jsonSuccess($this->psr17, [
140                'id' => $moduleId,
141                'name' => $definition->basicInfo->name,
142                'label' => $definition->basicInfo->label,
143                'route_url' => $definition->basicInfo->routeUrl,
144                'table_name' => $definition->basicInfo->tableName,
145                'message' => 'Moduł został pomyślnie utworzony.',
146            ], 201);
147        } catch (Throwable $e) {
148            return $this->jsonError(
149                $this->psr17,
150                $e->getMessage(),
151                400,
152                [
153                    'rolled_back' => true,
154                    'details' => 'Operacja została bezpiecznie wycofana z bazy danych.',
155                ]
156            );
157        }
158    }
159
160    /**
161     * PUT /api/v1/system/module-builder/update/{id}
162     */
163    public function update(
164        int $id,
165        ServerRequestInterface $request,
166        ?PermissionContext $context = null
167    ): ResponseInterface {
168        return $this->actionUpdateModule($request, $id, $context ?? $this->resolvePermissionContext($request));
169    }
170
171    public function actionUpdateModule(
172        ServerRequestInterface $request,
173        int $id,
174        PermissionContext $context
175    ): ResponseInterface {
176        try {
177            $body = $this->parseRequestBody($request);
178            $definition = ModuleDefinitionDto::fromArray($body);
179
180            $this->builderService->updateModule($id, $definition, $context);
181
182            return $this->jsonSuccess($this->psr17, [
183                'id' => $id,
184                'message' => 'Moduł został pomyślnie zaktualizowany.',
185            ]);
186        } catch (Throwable $e) {
187            return $this->jsonError($this->psr17, $e->getMessage(), 400);
188        }
189    }
190
191    /**
192     * DELETE /api/v1/system/module-builder/{id}
193     */
194    public function delete(
195        int $id,
196        ServerRequestInterface $request,
197        ?PermissionContext $context = null
198    ): ResponseInterface {
199        return $this->actionDeleteModule($request, $id, $context ?? $this->resolvePermissionContext($request));
200    }
201
202    public function actionDeleteModule(
203        ServerRequestInterface $request,
204        int $id,
205        PermissionContext $context
206    ): ResponseInterface {
207        try {
208            $dropTable = true;
209            $params = $request->getQueryParams();
210            if (isset($params['drop_table']) && in_array($params['drop_table'], ['0', 'false', false], true)) {
211                $dropTable = false;
212            }
213
214            $this->builderService->deleteModule($id, $dropTable, $context);
215
216            return $this->jsonSuccess($this->psr17, [
217                'id' => $id,
218                'message' => 'Moduł oraz powiązane struktury zostały pomyślnie usunięte.',
219            ]);
220        } catch (Throwable $e) {
221            return $this->jsonError($this->psr17, $e->getMessage(), 400);
222        }
223    }
224
225    /**
226     * Parses request body supporting JSON and urlencoded data.
227     *
228     * @return array<string, mixed>
229     */
230    private function parseRequestBody(ServerRequestInterface $request): array
231    {
232        $body = (array) ($request->getParsedBody() ?? []);
233        if (empty($body)) {
234            $raw = (string) $request->getBody();
235            if ($raw !== '') {
236                $decoded = json_decode($raw, true);
237                if (is_array($decoded)) {
238                    $body = $decoded;
239                }
240            }
241        }
242        return $body;
243    }
244
245    private function resolvePermissionContext(ServerRequestInterface $request): PermissionContext
246    {
247        $context = $request->getAttribute('permission_context');
248        if ($context instanceof PermissionContext) {
249            return $context;
250        }
251
252        return new PermissionContext(0, '127.0.0.1', true, false);
253    }
254}