- 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.
145 lines
4.8 KiB
PHP
145 lines
4.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Application\Api\Images;
|
|
|
|
use App\Domain\Media\ImageRepository;
|
|
use App\Framework\Attributes\Route;
|
|
use App\Framework\Exception\ErrorCode;
|
|
use App\Framework\Exception\FrameworkException;
|
|
use App\Framework\Http\Exception\NotFound;
|
|
use App\Framework\Http\HttpRequest;
|
|
use App\Framework\Http\Method;
|
|
use App\Framework\Http\Responses\JsonResponse;
|
|
use App\Framework\Http\Status;
|
|
|
|
final readonly class UpdateImageController
|
|
{
|
|
public function __construct(
|
|
private ImageRepository $imageRepository
|
|
) {
|
|
}
|
|
|
|
#[Route(path: '/api/images/{ulid}', method: Method::PUT)]
|
|
public function __invoke(HttpRequest $request): JsonResponse
|
|
{
|
|
$ulid = $request->routeParameters->getString('ulid');
|
|
|
|
// Find image by ULID
|
|
$image = $this->imageRepository->findByUlid($ulid);
|
|
if (! $image) {
|
|
throw NotFound::create(
|
|
ErrorCode::ENTITY_NOT_FOUND,
|
|
"Image with ULID {$ulid} not found"
|
|
)->withData(['ulid' => $ulid]);
|
|
}
|
|
|
|
// Parse request body
|
|
$updateData = $this->parseUpdateData($request);
|
|
|
|
// Update only allowed fields
|
|
$updatedImage = $image;
|
|
|
|
if (isset($updateData['alt_text'])) {
|
|
$updatedImage = $updatedImage->withAltText($updateData['alt_text']);
|
|
}
|
|
|
|
if (isset($updateData['filename'])) {
|
|
$this->validateFilename($updateData['filename']);
|
|
$updatedImage = $updatedImage->withFilename($updateData['filename']);
|
|
}
|
|
|
|
// Save updated image
|
|
$this->imageRepository->save($updatedImage);
|
|
|
|
// Return updated image data
|
|
return new JsonResponse([
|
|
'ulid' => $updatedImage->getUlidString(),
|
|
'filename' => $updatedImage->filename,
|
|
'original_filename' => $updatedImage->originalFilename,
|
|
'url' => '/media/images/' . $updatedImage->path->toString(),
|
|
'thumbnail_url' => '/media/images/thumbnails/' . $updatedImage->path->toString(),
|
|
'alt_text' => $updatedImage->altText,
|
|
'dimensions' => [
|
|
'width' => $updatedImage->width,
|
|
'height' => $updatedImage->height,
|
|
'aspect_ratio' => $updatedImage->getAspectRatio(),
|
|
'orientation' => $updatedImage->getDimensions()->getOrientation()->value,
|
|
],
|
|
'mime_type' => $updatedImage->mimeType->value,
|
|
'file_size' => [
|
|
'bytes' => $updatedImage->fileSize->toBytes(),
|
|
'human_readable' => $updatedImage->getHumanReadableFileSize(),
|
|
],
|
|
'hash' => $updatedImage->hash->toString(),
|
|
'is_image' => $updatedImage->isImageFile(),
|
|
'created_at' => $updatedImage->ulid->getDateTime()->format('c'),
|
|
'variants' => array_map(fn ($variant) => [
|
|
'type' => $variant->variantType,
|
|
'width' => $variant->width,
|
|
'height' => $variant->height,
|
|
'path' => $variant->path,
|
|
'url' => '/media/images/' . $variant->path,
|
|
], $updatedImage->variants ?? []),
|
|
], Status::OK);
|
|
}
|
|
|
|
/**
|
|
* Parse update data from request body
|
|
*/
|
|
private function parseUpdateData(HttpRequest $request): array
|
|
{
|
|
$body = $request->parsedBody->toArray();
|
|
|
|
// Only allow specific fields to be updated
|
|
$allowedFields = ['alt_text', 'filename'];
|
|
$updateData = [];
|
|
|
|
foreach ($allowedFields as $field) {
|
|
if (array_key_exists($field, $body)) {
|
|
$updateData[$field] = $body[$field];
|
|
}
|
|
}
|
|
|
|
return $updateData;
|
|
}
|
|
|
|
/**
|
|
* Validate filename
|
|
*/
|
|
private function validateFilename(string $filename): void
|
|
{
|
|
if (empty(trim($filename))) {
|
|
throw FrameworkException::create(
|
|
ErrorCode::VAL_INVALID_FORMAT,
|
|
'Filename cannot be empty'
|
|
)->withData(['field' => 'filename']);
|
|
}
|
|
|
|
// Check for invalid characters
|
|
if (preg_match('/[\/\\\\:*?"<>|]/', $filename)) {
|
|
throw FrameworkException::create(
|
|
ErrorCode::VAL_INVALID_FORMAT,
|
|
'Filename contains invalid characters'
|
|
)->withData([
|
|
'field' => 'filename',
|
|
'value' => $filename,
|
|
'invalid_chars' => ['/', '\\', ':', '*', '?', '"', '<', '>', '|'],
|
|
]);
|
|
}
|
|
|
|
// Check length
|
|
if (strlen($filename) > 255) {
|
|
throw FrameworkException::create(
|
|
ErrorCode::VAL_INVALID_FORMAT,
|
|
'Filename is too long (maximum 255 characters)'
|
|
)->withData([
|
|
'field' => 'filename',
|
|
'length' => strlen($filename),
|
|
'max_length' => 255,
|
|
]);
|
|
}
|
|
}
|
|
}
|