Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
MediaUploadApiController
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
3 / 3
11
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 actionUpload
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
6
 collectUploadedFiles
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
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\Engine\Presentation\Api;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11use App\Core\Engine\Application\Service\ImageUploadServiceInterface;
12use App\Shared\Infrastructure\Http\ApiResponseTrait;
13use Nyholm\Psr7\Factory\Psr17Factory;
14use Psr\Http\Message\ResponseInterface;
15use Psr\Http\Message\ServerRequestInterface;
16use Psr\Http\Message\UploadedFileInterface;
17use Throwable;
18
19/**
20 * Media Upload API Controller.
21 *
22 * REST and HTMX endpoint for asynchronous multi-image uploads and thumbnail generation.
23 *
24 * Handled routes:
25 *   POST /api/v1/engine/media/upload
26 *   POST /htmx/engine/media/upload
27 *
28 * @package App\Core\Engine\Presentation\Api
29 */
30final readonly class MediaUploadApiController implements MediaUploadApiControllerInterface
31{
32    use ApiResponseTrait;
33
34    /**
35     * MediaUploadApiController constructor.
36     *
37     * @param ImageUploadServiceInterface $uploadService Image upload service.
38     * @param Psr17Factory                $psr17         PSR-17 factory.
39     */
40    public function __construct(
41        private ImageUploadServiceInterface $uploadService,
42        private Psr17Factory $psr17,
43    ) {
44    }
45
46    /**
47     * Handles upload of one or multiple image files.
48     *
49     * @param ServerRequestInterface $request Incoming HTTP request.
50     * @return ResponseInterface JSON API response.
51     */
52    public function actionUpload(ServerRequestInterface $request): ResponseInterface
53    {
54        $uploadedFiles = $request->getUploadedFiles();
55        $filesToProcess = $this->collectUploadedFiles($uploadedFiles);
56
57        if (empty($filesToProcess)) {
58            return $this->jsonError($this->psr17, 'No image files were provided in the upload request.', 400);
59        }
60
61        $results = [];
62        $errors = [];
63
64        foreach ($filesToProcess as $file) {
65            try {
66                $results[] = $this->uploadService->uploadImage($file, 'media');
67            } catch (Throwable $e) {
68                $clientName = $file->getClientFilename() ?? 'unknown';
69                $errors[] = "{$clientName}" . $e->getMessage();
70            }
71        }
72
73        if (empty($results) && !empty($errors)) {
74            return $this->jsonError($this->psr17, implode('; ', $errors), 400);
75        }
76
77        return $this->jsonSuccess($this->psr17, [
78            'files'  => $results,
79            'errors' => $errors,
80        ]);
81    }
82
83    /**
84     * Collects all PSR-7 UploadedFileInterface instances from the uploaded tree.
85     *
86     * @param array<string, mixed> $uploadedFiles Request files array.
87     * @return array<int, UploadedFileInterface> Flat array of valid uploaded files.
88     */
89    private function collectUploadedFiles(array $uploadedFiles): array
90    {
91        $list = [];
92
93        foreach ($uploadedFiles as $entry) {
94            if ($entry instanceof UploadedFileInterface) {
95                $list[] = $entry;
96            } elseif (is_array($entry)) {
97                $list = array_merge($list, $this->collectUploadedFiles($entry));
98            }
99        }
100
101        return $list;
102    }
103}