chore: complete update
This commit is contained in:
82
.archive/Media/Controllers/MediaController.php
Normal file
82
.archive/Media/Controllers/MediaController.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Media\Controllers;
|
||||
|
||||
use App\Framework\Attributes\Route;
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\Http\Headers;
|
||||
use App\Framework\Http\HttpResponse;
|
||||
use App\Framework\Http\Method;
|
||||
use App\Framework\Http\Status;
|
||||
use App\Framework\Router\Result\FileResult;
|
||||
use Media\Entities\Image;
|
||||
use Media\Services\ImageService;
|
||||
|
||||
class MediaController
|
||||
{
|
||||
public function __construct(
|
||||
private ImageService $imageService,
|
||||
private PathProvider $pathProvider,
|
||||
) {}
|
||||
|
||||
#[Route('/media/{path}-{filename}', method: Method::GET)]
|
||||
public function serveMedia(string $path, string $filename): FileResult
|
||||
{
|
||||
$image = $this->imageService->resolveFromUrlId($path);
|
||||
|
||||
$imagePath = $this->pathProvider->resolvePath('storage' . $image->uploadPath . '/' . $filename);
|
||||
|
||||
#debug($imagePath);
|
||||
|
||||
// Prüfen ob Datei existiert
|
||||
if (!file_exists($imagePath)) {
|
||||
die('oh');
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
// Lese Datei und sende sie zurück
|
||||
$content = file_get_contents($imagePath);
|
||||
$mimeType = $this->getMimeTypeFromFormat($image->mimeType);
|
||||
|
||||
$headers = new Headers([
|
||||
'Content-Type' => $mimeType,
|
||||
'Content-Length' => filesize($imagePath),
|
||||
'Cache-Control' => 'public, max-age=31536000', // 1 Jahr cachen
|
||||
'ETag' => '"' . md5_file($imagePath) . '"',
|
||||
]);
|
||||
|
||||
return new FileResult($imagePath);
|
||||
|
||||
return new HttpResponse(
|
||||
Status::OK,
|
||||
$headers,
|
||||
$content
|
||||
);
|
||||
}
|
||||
|
||||
private function constructImagePath(Image $image, string $variant, string $format): string
|
||||
{
|
||||
return $this->pathProvider->resolvePath('storage' . $image->uploadPath . '/' . $variant . '.' . $format);
|
||||
}
|
||||
|
||||
private function getMimeTypeFromFormat(string $format): string
|
||||
{
|
||||
return match ($format) {
|
||||
'jpg' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
'gif' => 'image/gif',
|
||||
'webp' => 'image/webp',
|
||||
'avif' => 'image/avif',
|
||||
default => 'application/octet-stream'
|
||||
};
|
||||
}
|
||||
|
||||
private function notFound(): HttpResponse
|
||||
{
|
||||
return new HttpResponse(
|
||||
Status::NOT_FOUND,
|
||||
new Headers(['Content-Type' => 'text/plain']),
|
||||
'Bild nicht gefunden'
|
||||
);
|
||||
}
|
||||
}
|
||||
57
.archive/Media/Entities/Image.php
Normal file
57
.archive/Media/Entities/Image.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Media\Entities;
|
||||
|
||||
use App\Framework\Database\Attributes\Entity;
|
||||
|
||||
#[Entity(tableName: 'images')]
|
||||
class Image
|
||||
{
|
||||
public function __construct(
|
||||
public ?int $id = null,
|
||||
public string $filename = '',
|
||||
public string $originalFilename = '',
|
||||
public string $mimeType = '',
|
||||
public int $fileSize = 0,
|
||||
public int $width = 0,
|
||||
public int $height = 0,
|
||||
public string $hash = '',
|
||||
public string $uploadPath = '',
|
||||
public \DateTimeImmutable $createdAt = new \DateTimeImmutable(),
|
||||
public ?\DateTimeImmutable $updatedAt = null,
|
||||
) {}
|
||||
|
||||
public function getUploadDirectory(): string
|
||||
{
|
||||
return sprintf(
|
||||
'/uploads/%s/%s/%s',
|
||||
$this->createdAt->format('Y'),
|
||||
$this->createdAt->format('m'),
|
||||
$this->createdAt->format('d')
|
||||
);
|
||||
}
|
||||
|
||||
public function getFilePathPattern(): string
|
||||
{
|
||||
$idStr = str_pad((string)$this->id, 9, '0', STR_PAD_LEFT);
|
||||
return sprintf(
|
||||
'%s/%s/%s/%s',
|
||||
substr($idStr, 0, 3),
|
||||
substr($idStr, 3, 3),
|
||||
substr($idStr, 6, 3),
|
||||
$idStr
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generiert eine eindeutige URL-freundliche ID ohne Slashes
|
||||
*/
|
||||
public function getUrlId(): string
|
||||
{
|
||||
// Kombiniert Datum und ID zu einem eindeutigen String
|
||||
$dateStr = $this->createdAt->format('Ymd');
|
||||
$idStr = str_pad((string)$this->id, 8, '0', STR_PAD_LEFT);
|
||||
return $dateStr . $idStr;
|
||||
}
|
||||
|
||||
}
|
||||
32
.archive/Media/Entities/ImageVariant.php
Normal file
32
.archive/Media/Entities/ImageVariant.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Media\Entities;
|
||||
|
||||
use App\Framework\Database\Attributes\Entity;
|
||||
|
||||
#[Entity(tableName: 'image_variants')]
|
||||
class ImageVariant
|
||||
{
|
||||
public function __construct(
|
||||
public ?int $id = null,
|
||||
public int $imageId = 0,
|
||||
public string $variant = '', // 'thumbnail', 'small', 'medium', 'large'
|
||||
public string $format = '', // 'jpg', 'webp', 'avif'
|
||||
public int $width = 0,
|
||||
public int $height = 0,
|
||||
public int $fileSize = 0,
|
||||
public string $filename = '',
|
||||
public \DateTimeImmutable $createdAt = new \DateTimeImmutable(),
|
||||
) {}
|
||||
|
||||
public function getFullPath(Image $image): string
|
||||
{
|
||||
return sprintf(
|
||||
'%s/%s/%s.%s',
|
||||
$image->getUploadDirectory(),
|
||||
$image->getFilePathPattern(),
|
||||
$this->variant,
|
||||
$this->format
|
||||
);
|
||||
}
|
||||
}
|
||||
106
.archive/Media/Repositories/ImageRepository.php
Normal file
106
.archive/Media/Repositories/ImageRepository.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Media\Repositories;
|
||||
|
||||
use App\Framework\Database\Connection;
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use Media\Entities\Image;
|
||||
|
||||
class ImageRepository
|
||||
{
|
||||
public function __construct(
|
||||
private ConnectionInterface $db
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
public function save(Image $image): Image
|
||||
{
|
||||
$query = "INSERT INTO images "
|
||||
. "(filename, original_filename, mime_type, file_size, width, height, hash, upload_path, created_at) "
|
||||
. "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);";
|
||||
|
||||
$this->db->execute(
|
||||
$query,
|
||||
[
|
||||
$image->filename,
|
||||
$image->originalFilename,
|
||||
$image->mimeType,
|
||||
$image->fileSize,
|
||||
$image->width,
|
||||
$image->height,
|
||||
$image->hash,
|
||||
$image->uploadPath,
|
||||
$image->createdAt->format('Y-m-d H:i:s'),
|
||||
]
|
||||
);
|
||||
|
||||
$image->id = (int)$this->db->lastInsertId();
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
public function update(Image $image): void
|
||||
{
|
||||
$query = "UPDATE images SET "
|
||||
. "filename = ?, original_filename = ?, mime_type = ?, file_size = ?, "
|
||||
. "width = ?, height = ?, hash = ?, upload_path = ?, updated_at = CURRENT_TIMESTAMP "
|
||||
. "WHERE id = ?;";
|
||||
|
||||
$this->db->execute(
|
||||
$query,
|
||||
[
|
||||
$image->filename,
|
||||
$image->originalFilename,
|
||||
$image->mimeType,
|
||||
$image->fileSize,
|
||||
$image->width,
|
||||
$image->height,
|
||||
$image->hash,
|
||||
$image->uploadPath,
|
||||
$image->id,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function findById(int $id): ?Image
|
||||
{
|
||||
$query = "SELECT * FROM images WHERE id = ?;";
|
||||
$result = $this->db->query($query, [$id])->fetch();
|
||||
|
||||
if (null === $result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->mapToEntity($result);
|
||||
}
|
||||
|
||||
public function findByHash(string $hash): ?Image
|
||||
{
|
||||
$query = "SELECT * FROM images WHERE hash = ?;";
|
||||
$result = $this->db->query($query, [$hash])->fetch();
|
||||
|
||||
if (!$result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->mapToEntity($result);
|
||||
}
|
||||
|
||||
private function mapToEntity(array $data): Image
|
||||
{
|
||||
return new Image(
|
||||
id: (int)$data['id'],
|
||||
filename: $data['filename'],
|
||||
originalFilename: $data['original_filename'],
|
||||
mimeType: $data['mime_type'],
|
||||
fileSize: (int)$data['file_size'],
|
||||
width: (int)$data['width'],
|
||||
height: (int)$data['height'],
|
||||
hash: $data['hash'],
|
||||
uploadPath: $data['upload_path'],
|
||||
createdAt: new \DateTimeImmutable($data['created_at']),
|
||||
updatedAt: $data['updated_at'] ? new \DateTimeImmutable($data['updated_at']) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
74
.archive/Media/Repositories/ImageVariantRepository.php
Normal file
74
.archive/Media/Repositories/ImageVariantRepository.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Media\Repositories;
|
||||
|
||||
use App\Framework\Database\Connection;
|
||||
use App\Framework\Database\ConnectionInterface;
|
||||
use Media\Entities\ImageVariant;
|
||||
|
||||
class ImageVariantRepository
|
||||
{
|
||||
public function __construct(
|
||||
private ConnectionInterface $db
|
||||
) {}
|
||||
|
||||
public function save(ImageVariant $variant): ImageVariant
|
||||
{
|
||||
$query = "INSERT INTO image_variants "
|
||||
. "(image_id, variant, format, width, height, file_size, filename, created_at) "
|
||||
. "VALUES (?, ?, ?, ?, ?, ?, ?, ?)";
|
||||
|
||||
$this->db->execute(
|
||||
$query,
|
||||
[
|
||||
$variant->imageId,
|
||||
$variant->variant,
|
||||
$variant->format,
|
||||
$variant->width,
|
||||
$variant->height,
|
||||
$variant->fileSize,
|
||||
$variant->filename,
|
||||
$variant->createdAt->format('Y-m-d H:i:s'),
|
||||
]
|
||||
);
|
||||
|
||||
$variant->id = (int)$this->db->lastInsertId();
|
||||
|
||||
return $variant;
|
||||
}
|
||||
|
||||
public function findByImageId(int $imageId): array
|
||||
{
|
||||
$query = "SELECT * FROM image_variants WHERE image_id = ?;";
|
||||
$results = $this->db->query($query, [$imageId]);
|
||||
|
||||
return array_map(fn($data) => $this->mapToEntity($data), $results);
|
||||
}
|
||||
|
||||
public function findByImageIdAndVariant(int $imageId, string $variant, string $format): ?ImageVariant
|
||||
{
|
||||
$query = "SELECT * FROM image_variants WHERE image_id = ? AND variant = ? AND format = ?;";
|
||||
$result = $this->db->query($query, [$imageId, $variant, $format]);
|
||||
|
||||
if (empty($result)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->mapToEntity($result[0]);
|
||||
}
|
||||
|
||||
private function mapToEntity(array $data): ImageVariant
|
||||
{
|
||||
return new ImageVariant(
|
||||
id: (int)$data['id'],
|
||||
imageId: (int)$data['image_id'],
|
||||
variant: $data['variant'],
|
||||
format: $data['format'],
|
||||
width: (int)$data['width'],
|
||||
height: (int)$data['height'],
|
||||
fileSize: (int)$data['file_size'],
|
||||
filename: $data['filename'],
|
||||
createdAt: new \DateTimeImmutable($data['created_at']),
|
||||
);
|
||||
}
|
||||
}
|
||||
265
.archive/Media/Services/ImageService.php
Normal file
265
.archive/Media/Services/ImageService.php
Normal file
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
namespace Media\Services;
|
||||
|
||||
use App\Framework\Core\PathProvider;
|
||||
use App\Framework\Http\UploadedFile;
|
||||
use Media\Entities\Image;
|
||||
use Media\Entities\ImageVariant;
|
||||
use Media\Repositories\ImageRepository;
|
||||
use Media\Repositories\ImageVariantRepository;
|
||||
use function filesize;
|
||||
use function getimagesize;
|
||||
use function imageavif;
|
||||
use function imagecopyresampled;
|
||||
use function imagecreatefromstring;
|
||||
use function imagecreatetruecolor;
|
||||
use function imagedestroy;
|
||||
use function imagejpeg;
|
||||
use function imagesx;
|
||||
use function imagesy;
|
||||
use function imagewebp;
|
||||
|
||||
class ImageService
|
||||
{
|
||||
private const array VARIANTS = [
|
||||
'thumbnail' => ['width' => 150, 'height' => 150, 'crop' => true],
|
||||
'small' => ['width' => 400, 'height' => 400, 'crop' => false],
|
||||
'medium' => ['width' => 800, 'height' => 800, 'crop' => false],
|
||||
'large' => ['width' => 1200, 'height' => 1200, 'crop' => false],
|
||||
];
|
||||
|
||||
private const array FORMATS = ['jpg', 'webp', 'avif'];
|
||||
|
||||
public function __construct(
|
||||
private PathProvider $pathProvider,
|
||||
private ImageRepository $imageRepository,
|
||||
private ImageVariantRepository $variantRepository,
|
||||
) {}
|
||||
|
||||
public function uploadImage(UploadedFile $file): Image
|
||||
{
|
||||
// Validierung
|
||||
if (!$file->isValid()) {
|
||||
throw new \InvalidArgumentException('Ungültige Datei');
|
||||
}
|
||||
|
||||
if (!$this->isValidImageType($file->getMimeType())) {
|
||||
throw new \InvalidArgumentException('Ungültiger Bildtyp');
|
||||
}
|
||||
|
||||
// Basis-Image-Informationen erfassen
|
||||
$imageInfo = getimagesize($file->tmpName);
|
||||
$hash = \hash_file('sha256', $file->tmpName);
|
||||
|
||||
// Prüfen ob ein Bild mit gleichem Hash bereits existiert
|
||||
$existingImage = $this->imageRepository->findByHash($hash);
|
||||
if ($existingImage) {
|
||||
return $existingImage; // Duplikat gefunden, vorhandenes Bild zurückgeben
|
||||
}
|
||||
|
||||
// Image Entity erstellen
|
||||
$image = new Image(
|
||||
filename: $this->generateSecureFilename($file->name),
|
||||
originalFilename: $file->name,
|
||||
mimeType: $file->getMimeType(),
|
||||
fileSize: $file->size,
|
||||
width: $imageInfo[0],
|
||||
height: $imageInfo[1],
|
||||
hash: $hash,
|
||||
uploadPath: '',
|
||||
);
|
||||
|
||||
// In Datenbank speichern um ID zu erhalten
|
||||
$image = $this->imageRepository->save($image);
|
||||
|
||||
// Upload-Pfad setzen
|
||||
$image->uploadPath = $image->getUploadDirectory() . '/' . $image->getFilePathPattern();
|
||||
|
||||
$this->imageRepository->update($image);
|
||||
|
||||
// Ordnerstruktur erstellen
|
||||
$fullUploadPath = $this->pathProvider->resolvePath('storage' . $image->uploadPath);
|
||||
$this->createDirectoryStructure($fullUploadPath);
|
||||
|
||||
// Original-Datei speichern
|
||||
$originalPath = $fullUploadPath . '/original.' . $this->getFileExtension($file->getMimeType());
|
||||
$file->moveTo($originalPath);
|
||||
|
||||
// Varianten erstellen
|
||||
$this->createImageVariants($image, $originalPath);
|
||||
|
||||
// Image in Datenbank aktualisieren
|
||||
#$this->imageRepository->update($image);
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
private function createImageVariants(Image $image, string $originalPath): void
|
||||
{
|
||||
foreach (self::VARIANTS as $variantName => $config) {
|
||||
foreach (self::FORMATS as $format) {
|
||||
$variant = $this->createImageVariant(
|
||||
$image,
|
||||
$originalPath,
|
||||
$variantName,
|
||||
$format,
|
||||
$config
|
||||
);
|
||||
|
||||
// In Datenbank speichern
|
||||
$this->variantRepository->save($variant);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function createImageVariant(
|
||||
Image $image,
|
||||
string $originalPath,
|
||||
string $variantName,
|
||||
string $format,
|
||||
array $config
|
||||
): ImageVariant {
|
||||
$outputPath = $this->pathProvider->resolvePath('storage' . $image->uploadPath . '/' . $variantName . '.' . $format);
|
||||
|
||||
// Bild verarbeiten
|
||||
$resizedImage = $this->resizeImage($originalPath, $config['width'], $config['height'], $config['crop'] ?? false);
|
||||
$this->saveImageInFormat($resizedImage, $outputPath, $format);
|
||||
|
||||
// Neue Dateigröße und Dimensionen ermitteln
|
||||
$newImageInfo = getimagesize($outputPath);
|
||||
$fileSize = filesize($outputPath);
|
||||
|
||||
return new ImageVariant(
|
||||
imageId: $image->id,
|
||||
variant: $variantName,
|
||||
format: $format,
|
||||
width: $newImageInfo[0],
|
||||
height: $newImageInfo[1],
|
||||
fileSize: $fileSize,
|
||||
filename: $variantName . '.' . $format
|
||||
);
|
||||
}
|
||||
|
||||
private function resizeImage(string $sourcePath, int $maxWidth, int $maxHeight, bool $crop = false): \GdImage
|
||||
{
|
||||
$sourceImage = imagecreatefromstring(\file_get_contents($sourcePath));
|
||||
$sourceWidth = imagesx($sourceImage);
|
||||
$sourceHeight = imagesy($sourceImage);
|
||||
|
||||
if ($crop) {
|
||||
// Crop-Logik für quadratische Thumbnails
|
||||
$ratio = max($maxWidth / $sourceWidth, $maxHeight / $sourceHeight);
|
||||
$newWidth = (int)round($sourceWidth * $ratio);
|
||||
$newHeight = (int)round($sourceHeight * $ratio);
|
||||
|
||||
$tempImage = imagecreatetruecolor($newWidth, $newHeight);
|
||||
imagecopyresampled($tempImage, $sourceImage, 0, 0, 0, 0, $newWidth, $newHeight, $sourceWidth, $sourceHeight);
|
||||
|
||||
$finalImage = imagecreatetruecolor($maxWidth, $maxHeight);
|
||||
$cropX = (int)floor(($newWidth - $maxWidth) / 2);
|
||||
$cropY = (int)floor(($newHeight - $maxHeight) / 2);
|
||||
\imagecopy($finalImage, $tempImage, 0, 0, $cropX, $cropY, $maxWidth, $maxHeight);
|
||||
|
||||
imagedestroy($tempImage);
|
||||
} else {
|
||||
// Proportionales Skalieren
|
||||
$ratio = min($maxWidth / $sourceWidth, $maxHeight / $sourceHeight);
|
||||
$newWidth = (int)round($sourceWidth * $ratio);
|
||||
$newHeight = (int)round($sourceHeight * $ratio);
|
||||
|
||||
$finalImage = imagecreatetruecolor($newWidth, $newHeight);
|
||||
imagecopyresampled($finalImage, $sourceImage, 0, 0, 0, 0, $newWidth, $newHeight, $sourceWidth, $sourceHeight);
|
||||
}
|
||||
|
||||
imagedestroy($sourceImage);
|
||||
return $finalImage;
|
||||
}
|
||||
|
||||
private function saveImageInFormat(\GdImage $image, string $path, string $format): void
|
||||
{
|
||||
switch ($format) {
|
||||
case 'jpg':
|
||||
imagejpeg($image, $path, 85);
|
||||
break;
|
||||
case 'webp':
|
||||
imagewebp($image, $path, 85);
|
||||
break;
|
||||
case 'avif':
|
||||
if (\function_exists('imageavif')) {
|
||||
imageavif($image, $path, 85);
|
||||
} else {
|
||||
// Fallback auf WebP wenn AVIF nicht verfügbar
|
||||
imagewebp($image, $path, 85);
|
||||
}
|
||||
break;
|
||||
}
|
||||
imagedestroy($image);
|
||||
}
|
||||
|
||||
private function createDirectoryStructure(string $path): void
|
||||
{
|
||||
if (!is_dir($path)) {
|
||||
mkdir($path, 0755, true);
|
||||
}
|
||||
}
|
||||
|
||||
private function isValidImageType(string $mimeType): bool
|
||||
{
|
||||
return in_array($mimeType, [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif',
|
||||
'image/webp'
|
||||
]);
|
||||
}
|
||||
|
||||
private function getFileExtension(string $mimeType): string
|
||||
{
|
||||
return match ($mimeType) {
|
||||
'image/jpeg' => 'jpg',
|
||||
'image/png' => 'png',
|
||||
'image/gif' => 'gif',
|
||||
'image/webp' => 'webp',
|
||||
default => 'jpg'
|
||||
};
|
||||
}
|
||||
|
||||
private function generateSecureFilename(string $originalName): string
|
||||
{
|
||||
$extension = pathinfo($originalName, PATHINFO_EXTENSION);
|
||||
return uniqid('img_', true) . '.' . strtolower($extension);
|
||||
}
|
||||
|
||||
public function getImageUrl(Image $image, string $variant = 'medium', string $format = 'jpg'): string
|
||||
{
|
||||
return '/media/' . $image->getUrlId() . '-' . $variant . '.' . $format;
|
||||
}
|
||||
|
||||
public function findById(int $id): ?Image
|
||||
{
|
||||
return $this->imageRepository->findById($id);
|
||||
}
|
||||
|
||||
public function getVariants(Image $image): array
|
||||
{
|
||||
return $this->variantRepository->findByImageId($image->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Löst eine URL-ID zu einem Image-Objekt auf
|
||||
*/
|
||||
public function resolveFromUrlId(string $urlId): ?Image
|
||||
{
|
||||
// URL-ID format: YYYYMMDDXXXXXXXX (Datum + 8-stellige ID)
|
||||
if (strlen($urlId) !== 16) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$idPart = substr($urlId, 8); // Die letzten 8 Zeichen
|
||||
$id = (int)ltrim($idPart, '0'); // Führende Nullen entfernen
|
||||
|
||||
return $this->imageRepository->findById($id);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user