- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
168 lines
5.1 KiB
PHP
168 lines
5.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Application\Api\Images;
|
|
|
|
use App\Domain\Media\Image;
|
|
use App\Framework\Attributes\Route;
|
|
use App\Framework\Http\HttpRequest;
|
|
use App\Framework\Http\Method;
|
|
use App\Framework\Http\Responses\JsonResponse;
|
|
use App\Framework\Http\Status;
|
|
use App\Framework\Pagination\PaginationService;
|
|
use App\Framework\Pagination\ValueObjects\Direction;
|
|
|
|
final readonly class ListImagesController
|
|
{
|
|
public function __construct(
|
|
private PaginationService $paginationService
|
|
) {
|
|
}
|
|
|
|
#[Route(path: '/api/images', method: Method::GET)]
|
|
public function __invoke(HttpRequest $request): JsonResponse
|
|
{
|
|
// Parse query parameters
|
|
$limit = $this->parseLimit($request);
|
|
$page = $this->parsePage($request);
|
|
$sortField = $this->parseSortField($request);
|
|
$direction = $this->parseDirection($request);
|
|
$cursor = $this->parseCursor($request);
|
|
|
|
// Create pagination request
|
|
if ($cursor !== null) {
|
|
// Cursor-based pagination
|
|
$paginationRequest = $this->paginationService->cursorRequest(
|
|
limit: $limit,
|
|
cursorValue: $cursor,
|
|
sortField: $sortField,
|
|
direction: $direction
|
|
);
|
|
} else {
|
|
// Offset-based pagination
|
|
$offset = ($page - 1) * $limit;
|
|
$paginationRequest = $this->paginationService->offsetRequest(
|
|
limit: $limit,
|
|
offset: $offset,
|
|
sortField: $sortField,
|
|
direction: $direction
|
|
);
|
|
}
|
|
|
|
// Get paginated results
|
|
$paginator = $this->paginationService->forEntity(Image::class);
|
|
$paginationResponse = $paginator->paginate($paginationRequest);
|
|
|
|
// Transform images to API format
|
|
$transformedData = array_map([$this, 'transformImage'], $paginationResponse->data);
|
|
|
|
// Create response with transformed data
|
|
$responseData = [
|
|
'data' => $transformedData,
|
|
'meta' => $paginationResponse->meta->toArray(),
|
|
];
|
|
|
|
return new JsonResponse($responseData, Status::OK);
|
|
}
|
|
|
|
/**
|
|
* Parse limit parameter with validation
|
|
*/
|
|
private function parseLimit(HttpRequest $request): int
|
|
{
|
|
$limit = $request->query->getInt('limit', 20);
|
|
|
|
// Validate limit bounds
|
|
if ($limit < 1) {
|
|
$limit = 1;
|
|
} elseif ($limit > 100) {
|
|
$limit = 100;
|
|
}
|
|
|
|
return $limit;
|
|
}
|
|
|
|
/**
|
|
* Parse page parameter with validation
|
|
*/
|
|
private function parsePage(HttpRequest $request): int
|
|
{
|
|
$page = $request->query->getInt('page', 1);
|
|
|
|
return max(1, $page);
|
|
}
|
|
|
|
/**
|
|
* Parse sort field parameter
|
|
*/
|
|
private function parseSortField(HttpRequest $request): ?string
|
|
{
|
|
$sortField = $request->query->getString('sort');
|
|
|
|
// Allow only specific fields for security
|
|
$allowedFields = ['ulid', 'filename', 'width', 'height', 'fileSize'];
|
|
|
|
if ($sortField && in_array($sortField, $allowedFields)) {
|
|
return $sortField;
|
|
}
|
|
|
|
// Default sort by creation time (via ULID)
|
|
return 'ulid';
|
|
}
|
|
|
|
/**
|
|
* Parse direction parameter
|
|
*/
|
|
private function parseDirection(HttpRequest $request): string
|
|
{
|
|
$direction = $request->query->getString('direction', 'desc');
|
|
|
|
return in_array($direction, ['asc', 'desc']) ? $direction : 'desc';
|
|
}
|
|
|
|
/**
|
|
* Parse cursor parameter
|
|
*/
|
|
private function parseCursor(HttpRequest $request): ?string
|
|
{
|
|
return $request->query->getString('cursor');
|
|
}
|
|
|
|
/**
|
|
* Transform Image entity to API representation
|
|
*/
|
|
private function transformImage(Image $image): array
|
|
{
|
|
return [
|
|
'ulid' => $image->getUlidString(),
|
|
'filename' => $image->filename,
|
|
'original_filename' => $image->originalFilename,
|
|
'url' => '/images/' . $image->filename,
|
|
'thumbnail_url' => '/images/' . str_replace('_original.', '_thumbnail.', $image->filename),
|
|
'alt_text' => $image->altText,
|
|
'dimensions' => [
|
|
'width' => $image->width,
|
|
'height' => $image->height,
|
|
'aspect_ratio' => $image->getAspectRatio(),
|
|
'orientation' => $image->getDimensions()->getOrientation()->value,
|
|
],
|
|
'mime_type' => $image->mimeType->value,
|
|
'file_size' => [
|
|
'bytes' => $image->fileSize->toBytes(),
|
|
'human_readable' => $image->getHumanReadableFileSize(),
|
|
],
|
|
'hash' => $image->hash->toString(),
|
|
'is_image' => $image->isImageFile(),
|
|
'created_at' => $image->ulid->getDateTime()->format('c'),
|
|
'variants' => array_map(fn ($variant) => [
|
|
'type' => $variant->variantType,
|
|
'width' => $variant->width,
|
|
'height' => $variant->height,
|
|
'path' => $variant->path,
|
|
'url' => '/images/' . $variant->filename,
|
|
], $image->variants ?? []),
|
|
];
|
|
}
|
|
}
|