Formular-Infrastruktur: Contact/Demo/Helpdesk mit E-Mail- und Salesforce-Delivery, JSON-Renderer-Erweiterungen, Crop-Variants

Build-Artefakte in public/_frontend/assets nicht enthalten.
This commit is contained in:
orasche committed 2026-07-30 13:12:45 +02:00
1 parent eae40f4286
commit a739dd8fe3
51 files changed
+1598 -303

No files matched your search

@@ -17,6 +17,7 @@ use Evomedien\Vitec\UserFunc\DatasheetsJsonRenderer;
use Evomedien\Vitec\UserFunc\EventlistJsonRenderer;
use Evomedien\Vitec\UserFunc\LocationsJsonRenderer;
use Evomedien\Vitec\UserFunc\CustomerlogosJsonRenderer;
use Evomedien\Vitec\UserFunc\FormsJsonRenderer;
use Evomedien\Vitec\UserFunc\ModelcardJsonRenderer;
use Evomedien\Vitec\UserFunc\NewsJsonRenderer;
use TYPO3\CMS\Core\Database\ConnectionPool;
@@ -82,6 +83,9 @@ final class ContentElementResolver
'vitec_locationlist' => [LocationsJsonRenderer::class, 'locations'],
'vitec_customerlogos' => [CustomerlogosJsonRenderer::class, 'customerlogos'],
'vitec_modelcard' => [ModelcardJsonRenderer::class, 'card'],
'vitec_contactform' => [FormsJsonRenderer::class, 'form'],
'vitec_demoform' => [FormsJsonRenderer::class, 'form'],
'vitec_helpdeskform' => [FormsJsonRenderer::class, 'form'],
'news_pi1' => [NewsJsonRenderer::class, 'news'],
'news_newsliststicky' => [NewsJsonRenderer::class, 'news'],
'news_newsselectedlist' => [NewsJsonRenderer::class, 'news'],
@@ -9,6 +9,7 @@ use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Resource\ResourceFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Service\ImageService;
use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
/**
* Serialises a Success Story (tx_vitec_domain_model_usecase) DB row to the
@@ -43,7 +44,7 @@ final class UsecaseSerializer
'slug' => (string)($u['slug'] ?? ''),
'subtitle' => (string)($u['subtitle'] ?? ''),
'teaser' => (string)($u['teaser'] ?? ''),
'cardImage' => $this->image($uid, 'card_image'),
'cardImage' => $this->image($uid, 'card_image', null, true),
'customerLogo' => $this->image($uid, 'customer_logo'),
'markets' => $this->relationMulti('tx_vitec_usecase_market_mm', 'tx_vitec_domain_model_market', $uid),
'categories' => $this->categories($uid),
@@ -100,7 +101,7 @@ final class UsecaseSerializer
*
* @return array<string,mixed>|null
*/
public function image(int $recordUid, string $fieldName, ?string $table = null): ?array
public function image(int $recordUid, string $fieldName, ?string $table = null, bool $withCropVariants = false): ?array
{
try {
$table = $table ?? self::TABLE;
@@ -142,43 +143,92 @@ final class UsecaseSerializer
];
}
$cropString = (string)($ref['crop'] ?? '');
$payload = $this->processImagePayload($imageService, $fileReference, $cropString !== '' ? $cropString : null);
$srcset = [];
foreach (self::IMAGE_WIDTHS as $width) {
$variant = $imageService->applyProcessingInstructions(
$fileReference,
['width' => $width, 'crop' => $ref['crop'] ?? null]
);
$srcset[] = [
'url' => $imageService->getImageUri($variant),
'width' => $width,
'descriptor' => $width . 'w',
];
}
$default = $imageService->applyProcessingInstructions(
$fileReference,
['width' => 800, 'crop' => $ref['crop'] ?? null]
);
return [
$result = [
'uid' => (int)$ref['uid'],
'url' => $imageService->getImageUri($default),
'url' => $payload['url'],
'title' => (string)($ref['title'] ?? ''),
'alternative' => (string)($ref['alternative'] ?? ''),
'description' => (string)($ref['description'] ?? ''),
'srcset' => $srcset,
'srcset' => $payload['srcset'],
'properties' => [
'width' => $fileReference->getProperty('width'),
'height' => $fileReference->getProperty('height'),
'mimeType' => $fileReference->getProperty('mime_type'),
],
];
// Zusatz-Zuschnitte (Card 4:3 / Large Card 16:9) nur fuer das "erste Bild"
// der karten-faehigen Modelle; faellt auf das Vollbild zurueck, wenn keine
// Variante gesetzt ist.
if ($withCropVariants) {
$result['crops'] = $this->cropVariantPayloads($imageService, $fileReference, $cropString);
}
return $result;
} catch (\Throwable $e) {
return null;
}
}
/**
* Build {url, srcset} for one crop input (crop JSON string, absolute Area
* or null = full image). srcset steps whose processing would fall back to
* the original file are skipped (avoids raw-/403-leaks for sources that are
* smaller than the requested width).
*
* @param string|\TYPO3\CMS\Core\Imaging\ImageManipulation\Area|null $crop
* @return array{url:string,srcset:array<int,array<string,mixed>>}
*/
private function processImagePayload(ImageService $imageService, \TYPO3\CMS\Core\Resource\FileReference $fileReference, $crop): array
{
$srcset = [];
foreach (self::IMAGE_WIDTHS as $width) {
$variant = $imageService->applyProcessingInstructions(
$fileReference,
['width' => $width, 'crop' => $crop, 'fileExtension' => 'webp']
);
if ($variant->usesOriginalFile()) {
continue;
}
$srcset[] = [
'url' => $imageService->getImageUri($variant),
'width' => $width,
'descriptor' => $width . 'w',
];
}
$default = $imageService->applyProcessingInstructions(
$fileReference,
['width' => 800, 'crop' => $crop, 'fileExtension' => 'webp']
);
return [
'url' => $imageService->getImageUri($default),
'srcset' => $srcset,
];
}
/**
* card + largeCard crop-variant payloads for a model's first image.
* Each variant falls back to the full image when no explicit crop is set.
*
* @return array<string,array{url:string,srcset:array<int,array<string,mixed>>}>
*/
private function cropVariantPayloads(ImageService $imageService, \TYPO3\CMS\Core\Resource\FileReference $fileReference, string $cropString): array
{
$collection = CropVariantCollection::create($cropString);
$out = [];
foreach (['card', 'largeCard'] as $variantKey) {
$area = $collection->getCropArea($variantKey);
$crop = $area->isEmpty() ? null : $area->makeAbsoluteBasedOnFile($fileReference);
$out[$variantKey] = $this->processImagePayload($imageService, $fileReference, $crop);
}
return $out;
}
/**
* All FAL images of one field (multi-reference), same shape as image().
*
@@ -260,13 +310,16 @@ final class UsecaseSerializer
foreach (self::IMAGE_WIDTHS as $width) {
$variant = $imageService->applyProcessingInstructions(
$fileReference,
['width' => $width, 'crop' => $ref['crop'] ?? null]
['width' => $width, 'crop' => $ref['crop'] ?? null, 'fileExtension' => 'webp']
);
if ($variant->usesOriginalFile()) {
continue; // Quelle zu klein fuer diese Breite: kein Raw-/403-Leak
}
$srcset[] = ['url' => $imageService->getImageUri($variant), 'width' => $width, 'descriptor' => $width . 'w'];
}
$default = $imageService->applyProcessingInstructions(
$fileReference,
['width' => 800, 'crop' => $ref['crop'] ?? null]
['width' => 800, 'crop' => $ref['crop'] ?? null, 'fileExtension' => 'webp']
);
return [