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:
1 parent
eae40f4286
commit
a739dd8fe3
51 files changed
+1598
-303
No files matched your search
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Controller;
|
||||
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
|
||||
|
||||
/**
|
||||
* FormController — Extbase registration target for the VITEC form plugins
|
||||
* (Contactform, Demoform, Helpdeskform). In headless mode the JSON output
|
||||
* is produced by FormsJsonRenderer; submissions are handled by
|
||||
* FormSubmissionMiddleware.
|
||||
*/
|
||||
class FormController extends ActionController
|
||||
{
|
||||
public function listAction(): ResponseInterface
|
||||
{
|
||||
return $this->htmlResponse();
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,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\Connection;
|
||||
@@ -117,6 +118,9 @@ final class ContainerChildrenProcessor implements DataProcessorInterface
|
||||
'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'],
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Forms\Delivery;
|
||||
|
||||
/**
|
||||
* Delivery strategy for VITEC form submissions.
|
||||
* Implementations MUST throw on failure — the middleware logs the error
|
||||
* on the stored submission and keeps the data safe.
|
||||
*/
|
||||
interface DeliveryInterface
|
||||
{
|
||||
/**
|
||||
* @param array<string,string> $payload validated, filtered field values
|
||||
* @param array<string,mixed> $settings FlexForm settings of the form plugin
|
||||
*/
|
||||
public function deliver(string $formKey, array $payload, array $settings): void;
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Forms\Delivery;
|
||||
|
||||
use TYPO3\CMS\Core\Mail\MailMessage;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Sends the submission as a plain-text mail to the FlexForm-configured
|
||||
* recipient(s). Sender = system default (MAIL settings); reply-to = the
|
||||
* submitter's email when present.
|
||||
*/
|
||||
final class EmailDelivery implements DeliveryInterface
|
||||
{
|
||||
public function deliver(string $formKey, array $payload, array $settings): void
|
||||
{
|
||||
$recipients = GeneralUtility::trimExplode(',', (string)($settings['recipient'] ?? ''), true);
|
||||
if ($recipients === []) {
|
||||
throw new \RuntimeException('No recipient configured in the form plugin (FlexForm "Recipient").');
|
||||
}
|
||||
|
||||
$subject = trim((string)($settings['subject'] ?? ''));
|
||||
if ($subject === '') {
|
||||
$subject = 'VITEC website form: ' . $formKey;
|
||||
}
|
||||
|
||||
$lines = [];
|
||||
foreach ($payload as $field => $value) {
|
||||
$lines[] = str_pad($field . ':', 16) . $value;
|
||||
}
|
||||
|
||||
$mail = GeneralUtility::makeInstance(MailMessage::class);
|
||||
$mail->to(...$recipients)
|
||||
->subject($subject)
|
||||
->text("Form: $formKey\n\n" . implode("\n", $lines));
|
||||
|
||||
$submitterEmail = trim((string)($payload['email'] ?? ''));
|
||||
if ($submitterEmail !== '' && filter_var($submitterEmail, FILTER_VALIDATE_EMAIL)) {
|
||||
$mail->replyTo($submitterEmail);
|
||||
}
|
||||
|
||||
if (!$mail->send()) {
|
||||
throw new \RuntimeException('MailMessage::send() reported failure.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Forms\Delivery;
|
||||
|
||||
/**
|
||||
* Salesforce delivery — PREPARED STUB.
|
||||
*
|
||||
* The old website posted the contact form to Salesforce Web-to-Lead
|
||||
* (custom field ids like 00N3z00000DCiyV). When credentials / the target
|
||||
* setup are decided, implement deliver() here (Web-to-Lead POST or REST
|
||||
* API) including the camelCase -> Salesforce field mapping. Until then any
|
||||
* submission with delivery=salesforce is stored with status "failed" and
|
||||
* this message — no data is lost.
|
||||
*/
|
||||
final class SalesforceDelivery implements DeliveryInterface
|
||||
{
|
||||
public function deliver(string $formKey, array $payload, array $settings): void
|
||||
{
|
||||
throw new \RuntimeException(
|
||||
'Salesforce delivery is prepared but not configured yet — submission kept in the log.'
|
||||
);
|
||||
}
|
||||
}
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Forms;
|
||||
|
||||
/**
|
||||
* Single source of truth for the VITEC form plugins.
|
||||
*
|
||||
* The SAME definition drives (a) the JSON the React frontend renders the
|
||||
* form from and (b) the server-side validation of submissions in
|
||||
* FormSubmissionMiddleware. Field names are camelCase (React contract);
|
||||
* mapping to Salesforce field ids happens inside the Salesforce delivery.
|
||||
*
|
||||
* Field spec: name, type (text|email|tel|select|textarea), label, required,
|
||||
* options (select only) OR optionsSource (frontend-supplied list, e.g. the
|
||||
* ISO country list).
|
||||
*/
|
||||
final class FormDefinitions
|
||||
{
|
||||
public const HONEYPOT_FIELD = '_website';
|
||||
|
||||
/** CType => form key */
|
||||
public const CTYPE_MAP = [
|
||||
'vitec_contactform' => 'contact',
|
||||
'vitec_demoform' => 'demo',
|
||||
'vitec_helpdeskform' => 'helpdesk',
|
||||
];
|
||||
|
||||
private const SOLUTION_OPTIONS = [
|
||||
'IPTV Distribution',
|
||||
'Digital Signage',
|
||||
'Video Streaming',
|
||||
'Video Contribution',
|
||||
'Situational Awareness and ISR',
|
||||
'Remote Production',
|
||||
'Encoders & Decoders',
|
||||
'Custom Design Service',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return array<string,mixed>|null
|
||||
*/
|
||||
public static function get(string $formKey): ?array
|
||||
{
|
||||
$all = self::all();
|
||||
return $all[$formKey] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,array<string,mixed>>
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
$contactFields = [
|
||||
self::text('firstName', 'First Name', true),
|
||||
self::text('lastName', 'Last Name', true),
|
||||
self::text('company', 'Company', true),
|
||||
self::field('email', 'email', 'Email', true),
|
||||
self::field('phone', 'tel', 'Phone', true),
|
||||
self::text('street', 'Street', false),
|
||||
self::text('city', 'City', false),
|
||||
self::text('zip', 'Zip / Postal Code', false),
|
||||
self::countrySelect(),
|
||||
self::select('state', 'State', false, [], 'usStates'),
|
||||
self::select('solution', 'Solution of Interest', false, self::SOLUTION_OPTIONS),
|
||||
self::field('message', 'textarea', 'Message', false),
|
||||
];
|
||||
|
||||
return [
|
||||
'contact' => [
|
||||
'formKey' => 'contact',
|
||||
'title' => 'Contact VITEC',
|
||||
'fields' => $contactFields,
|
||||
],
|
||||
'demo' => [
|
||||
'formKey' => 'demo',
|
||||
'title' => 'Request a Demo',
|
||||
'fields' => $contactFields,
|
||||
],
|
||||
'helpdesk' => [
|
||||
'formKey' => 'helpdesk',
|
||||
'title' => 'Request Access to Online HelpDesk',
|
||||
'fields' => [
|
||||
self::text('firstName', 'First Name', true),
|
||||
self::text('lastName', 'Last Name', true),
|
||||
self::text('company', 'Company', true),
|
||||
self::field('phone', 'tel', 'Phone Number', false),
|
||||
self::field('email', 'email', 'Email Address', true),
|
||||
self::countrySelect(true),
|
||||
self::text('product', 'Product', false),
|
||||
self::text('serialNumber', 'Serial Number', false),
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a submission payload against the definition.
|
||||
*
|
||||
* @param array<string,mixed> $payload
|
||||
* @return array<string,string> field => error (empty = valid)
|
||||
*/
|
||||
public static function validate(string $formKey, array $payload): array
|
||||
{
|
||||
$definition = self::get($formKey);
|
||||
if ($definition === null) {
|
||||
return ['_form' => 'Unknown form'];
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
foreach ($definition['fields'] as $field) {
|
||||
$name = $field['name'];
|
||||
$value = trim((string)($payload[$name] ?? ''));
|
||||
|
||||
if (($field['required'] ?? false) && $value === '') {
|
||||
$errors[$name] = 'This field is required.';
|
||||
continue;
|
||||
}
|
||||
if ($value === '') {
|
||||
continue;
|
||||
}
|
||||
if ($field['type'] === 'email' && !filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
$errors[$name] = 'Please enter a valid email address.';
|
||||
}
|
||||
if (mb_strlen($value) > 5000) {
|
||||
$errors[$name] = 'Value is too long.';
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduce a raw payload to the defined fields (drops everything unknown).
|
||||
*
|
||||
* @param array<string,mixed> $payload
|
||||
* @return array<string,string>
|
||||
*/
|
||||
public static function filterPayload(string $formKey, array $payload): array
|
||||
{
|
||||
$definition = self::get($formKey);
|
||||
if ($definition === null) {
|
||||
return [];
|
||||
}
|
||||
$out = [];
|
||||
foreach ($definition['fields'] as $field) {
|
||||
$out[$field['name']] = trim((string)($payload[$field['name']] ?? ''));
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------ field DSL
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private static function field(string $name, string $type, string $label, bool $required): array
|
||||
{
|
||||
return ['name' => $name, 'type' => $type, 'label' => $label, 'required' => $required];
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private static function text(string $name, string $label, bool $required): array
|
||||
{
|
||||
return self::field($name, 'text', $label, $required);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int,string> $options
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private static function select(string $name, string $label, bool $required, array $options, string $optionsSource = ''): array
|
||||
{
|
||||
$field = self::field($name, 'select', $label, $required);
|
||||
if ($options !== []) {
|
||||
$field['options'] = $options;
|
||||
}
|
||||
if ($optionsSource !== '') {
|
||||
// The frontend supplies this list (e.g. ISO countries) — keeps
|
||||
// the definition lean and the lists consistent app-wide.
|
||||
$field['optionsSource'] = $optionsSource;
|
||||
}
|
||||
return $field;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private static function countrySelect(bool $required = false): array
|
||||
{
|
||||
return self::select('country', 'Country', $required, [], 'countries');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Middleware;
|
||||
|
||||
use Evomedien\Vitec\Forms\Delivery\DeliveryInterface;
|
||||
use Evomedien\Vitec\Forms\Delivery\EmailDelivery;
|
||||
use Evomedien\Vitec\Forms\Delivery\SalesforceDelivery;
|
||||
use Evomedien\Vitec\Forms\FormDefinitions;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Server\RequestHandlerInterface;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Http\JsonResponse;
|
||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Headless form endpoint: POST /api/vitec/form/<formKey>
|
||||
*
|
||||
* Accepts JSON (or form-encoded) submissions from the React frontend for
|
||||
* the VITEC form plugins (contact | demo | helpdesk):
|
||||
*
|
||||
* 1. honeypot check (silently accepted, nothing stored)
|
||||
* 2. server-side validation against FormDefinitions (single source of truth)
|
||||
* 3. submission stored in tx_vitec_form_submission (backup log)
|
||||
* 4. delivery via strategy: email (active) or salesforce (prepared stub) —
|
||||
* configured in the FlexForm of the form's plugin element
|
||||
*
|
||||
* A stored-but-undelivered submission still responds success:true — the
|
||||
* data is safe in the log and the delivery status is visible in the backend.
|
||||
*/
|
||||
final class FormSubmissionMiddleware implements MiddlewareInterface
|
||||
{
|
||||
private const PATH_PATTERN = '#^/api/vitec/form/([a-z]+)/?$#';
|
||||
|
||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
|
||||
{
|
||||
$path = $request->getUri()->getPath();
|
||||
if (preg_match(self::PATH_PATTERN, $path, $m) !== 1) {
|
||||
return $handler->handle($request);
|
||||
}
|
||||
$formKey = $m[1];
|
||||
|
||||
if (strtoupper($request->getMethod()) !== 'POST') {
|
||||
return new JsonResponse(['success' => false, 'errors' => ['_form' => 'POST only']], 405);
|
||||
}
|
||||
if (FormDefinitions::get($formKey) === null) {
|
||||
return new JsonResponse(['success' => false, 'errors' => ['_form' => 'Unknown form']], 404);
|
||||
}
|
||||
|
||||
try {
|
||||
$payload = $this->parseBody($request);
|
||||
|
||||
// Honeypot: pretend success, store nothing.
|
||||
if (trim((string)($payload[FormDefinitions::HONEYPOT_FIELD] ?? '')) !== '') {
|
||||
return new JsonResponse(['success' => true]);
|
||||
}
|
||||
|
||||
$errors = FormDefinitions::validate($formKey, $payload);
|
||||
if ($errors !== []) {
|
||||
return new JsonResponse(['success' => false, 'errors' => $errors], 422);
|
||||
}
|
||||
|
||||
$clean = FormDefinitions::filterPayload($formKey, $payload);
|
||||
$settings = $this->pluginSettings($formKey);
|
||||
$method = (string)($settings['delivery'] ?? 'email');
|
||||
|
||||
$submissionUid = $this->store($formKey, $clean, $method);
|
||||
|
||||
try {
|
||||
$this->delivery($method)->deliver($formKey, $clean, $settings);
|
||||
$this->updateStatus($submissionUid, 'sent', '');
|
||||
} catch (\Throwable $e) {
|
||||
$this->updateStatus($submissionUid, 'failed', $e->getMessage());
|
||||
}
|
||||
|
||||
// Data is safely logged either way.
|
||||
return new JsonResponse(['success' => true]);
|
||||
} catch (\Throwable $e) {
|
||||
return new JsonResponse(['success' => false, 'errors' => ['_form' => 'Unexpected error']], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private function parseBody(ServerRequestInterface $request): array
|
||||
{
|
||||
$contentType = $request->getHeaderLine('Content-Type');
|
||||
if (str_contains($contentType, 'application/json')) {
|
||||
$decoded = json_decode((string)$request->getBody(), true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
$parsed = $request->getParsedBody();
|
||||
return is_array($parsed) ? $parsed : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delivery settings come from the FlexForm of the (first) plugin element
|
||||
* of this form type — one configuration per form type.
|
||||
*
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function pluginSettings(string $formKey): array
|
||||
{
|
||||
try {
|
||||
$ctype = array_search($formKey, FormDefinitions::CTYPE_MAP, true);
|
||||
if ($ctype === false) {
|
||||
return [];
|
||||
}
|
||||
$qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
|
||||
$row = $qb
|
||||
->select('pi_flexform')
|
||||
->from('tt_content')
|
||||
->where(
|
||||
$qb->expr()->eq('CType', $qb->createNamedParameter($ctype)),
|
||||
$qb->expr()->eq('deleted', 0),
|
||||
$qb->expr()->eq('hidden', 0)
|
||||
)
|
||||
->orderBy('uid', 'ASC')
|
||||
->setMaxResults(1)
|
||||
->executeQuery()
|
||||
->fetchAssociative();
|
||||
if (!$row) {
|
||||
return [];
|
||||
}
|
||||
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
|
||||
$data = $flexFormService->convertFlexFormContentToArray((string)$row['pi_flexform']);
|
||||
return $data['settings'] ?? [];
|
||||
} catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private function delivery(string $method): DeliveryInterface
|
||||
{
|
||||
return $method === 'salesforce'
|
||||
? GeneralUtility::makeInstance(SalesforceDelivery::class)
|
||||
: GeneralUtility::makeInstance(EmailDelivery::class);
|
||||
}
|
||||
|
||||
/** @param array<string,string> $payload */
|
||||
private function store(string $formKey, array $payload, string $method): int
|
||||
{
|
||||
$conn = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionForTable('tx_vitec_form_submission');
|
||||
$conn->insert('tx_vitec_form_submission', [
|
||||
'pid' => 0,
|
||||
'crdate' => time(),
|
||||
'tstamp' => time(),
|
||||
'form_key' => $formKey,
|
||||
'payload' => (string)json_encode($payload, JSON_UNESCAPED_UNICODE),
|
||||
'delivery_method' => $method,
|
||||
'delivery_status' => 'pending',
|
||||
]);
|
||||
return (int)$conn->lastInsertId();
|
||||
}
|
||||
|
||||
private function updateStatus(int $uid, string $status, string $error): void
|
||||
{
|
||||
GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionForTable('tx_vitec_form_submission')
|
||||
->update(
|
||||
'tx_vitec_form_submission',
|
||||
['delivery_status' => $status, 'delivery_error' => mb_substr($error, 0, 1000), 'tstamp' => time()],
|
||||
['uid' => $uid]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 [
|
||||
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\Tca;
|
||||
|
||||
/**
|
||||
* Zentrale cropVariants-Definition fuer das "erste Bild" der Karten-faehigen
|
||||
* Modelle (Product, Success Story, Market, Solution).
|
||||
*
|
||||
* Liefert drei Zuschnitt-Varianten, die im Backend-Image-Editor je einen
|
||||
* eigenen Tab bekommen:
|
||||
* - default : freier/allgemeiner Zuschnitt (Detail-/Hero-/Listen-Nutzung)
|
||||
* - card : 4:3 fuer die normale Card-Darstellung
|
||||
* - largeCard : 16:9 fuer die grosse/Feature-Card-Darstellung
|
||||
*
|
||||
* Ein freier Zuschnitt ("Free") ist bei jeder Variante zusaetzlich moeglich;
|
||||
* die genannte Ratio ist lediglich der vorgegebene Standard-Rahmen.
|
||||
*
|
||||
* Wird aus den TCA-Dateien der o.g. Modelle heraus referenziert:
|
||||
* 'cropVariants' => \Evomedien\Vitec\Tca\CropVariants::firstImage(),
|
||||
*/
|
||||
final class CropVariants
|
||||
{
|
||||
/**
|
||||
* @return array<string,array<string,mixed>>
|
||||
*/
|
||||
public static function firstImage(): array
|
||||
{
|
||||
return [
|
||||
'default' => [
|
||||
'title' => 'Default',
|
||||
'allowedAspectRatios' => [
|
||||
'NaN' => ['title' => 'Free', 'value' => 0.0],
|
||||
'16:9' => ['title' => '16:9', 'value' => 16 / 9],
|
||||
'4:3' => ['title' => '4:3', 'value' => 4 / 3],
|
||||
'1:1' => ['title' => '1:1', 'value' => 1.0],
|
||||
],
|
||||
],
|
||||
'card' => [
|
||||
'title' => 'Card (4:3)',
|
||||
'allowedAspectRatios' => [
|
||||
'4:3' => ['title' => '4:3', 'value' => 4 / 3],
|
||||
'NaN' => ['title' => 'Free', 'value' => 0.0],
|
||||
],
|
||||
],
|
||||
'largeCard' => [
|
||||
'title' => 'Large Card (16:9)',
|
||||
'allowedAspectRatios' => [
|
||||
'16:9' => ['title' => '16:9', 'value' => 16 / 9],
|
||||
'NaN' => ['title' => 'Free', 'value' => 0.0],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -204,13 +204,13 @@ class CustomerlogosJsonRenderer
|
||||
foreach (self::IMAGE_WIDTHS as $width) {
|
||||
$variant = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => $width, 'crop' => $ref['crop'] ?? null]
|
||||
['width' => $width, 'crop' => $ref['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
$srcset[] = ['url' => $imageService->getImageUri($variant), 'width' => $width, 'descriptor' => $width . 'w'];
|
||||
}
|
||||
$default = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => 400, 'crop' => $ref['crop'] ?? null]
|
||||
['width' => 400, 'crop' => $ref['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
@@ -338,7 +338,7 @@ class DatasheetsJsonRenderer
|
||||
|
||||
$default = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => 400, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => 400, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
@@ -211,7 +211,7 @@ class DownloadcardcollectionJsonRenderer
|
||||
foreach ([400, 800, 1200, 1600] as $width) {
|
||||
$variant = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => $width, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => $width, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
$srcset[] = [
|
||||
'url' => $imageService->getImageUri($variant),
|
||||
@@ -222,7 +222,7 @@ class DownloadcardcollectionJsonRenderer
|
||||
|
||||
$default = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => 800, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => 800, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
@@ -247,7 +247,7 @@ class EventlistJsonRenderer
|
||||
foreach ([400, 800] as $width) {
|
||||
$variant = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => $width, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => $width, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
$srcset[] = [
|
||||
'url' => $imageService->getImageUri($variant),
|
||||
@@ -258,7 +258,7 @@ class EventlistJsonRenderer
|
||||
|
||||
$default = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => 400, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => 400, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\UserFunc;
|
||||
|
||||
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||
|
||||
/**
|
||||
* Headless JSON renderer for the site-wide favicon / app-icon set.
|
||||
*
|
||||
* Emits a stable `favicons` object into every page response so the React
|
||||
* frontend can render the <link>/<meta> head tags from the CMS instead of
|
||||
* hard-coding them. The icon files live under public/fileadmin/icons/ and are
|
||||
* referenced root-relative — identical convention to the image URLs emitted by
|
||||
* UsecaseSerializer::image() (getImageUri also returns /fileadmin/... paths).
|
||||
* If the frontend ever runs on a separate origin from the CMS, it must prefix
|
||||
* these hrefs with the CMS base URL, exactly as it does for image URLs.
|
||||
*
|
||||
* Static by nature (same for every page) — no DB access, exception-free.
|
||||
*/
|
||||
final class FaviconsJsonRenderer
|
||||
{
|
||||
private const BASE = '/fileadmin/icons';
|
||||
|
||||
/** Browser UI / address-bar tint (VITEC navy). */
|
||||
private const THEME_COLOR = '#26358C';
|
||||
|
||||
#[AsAllowedCallable]
|
||||
public function render(string $content, array $conf): string
|
||||
{
|
||||
$payload = [
|
||||
'themeColor' => self::THEME_COLOR,
|
||||
'manifest' => self::BASE . '/site.webmanifest',
|
||||
// Ready-to-render <link> descriptors, ordered most- to least-specific.
|
||||
'links' => [
|
||||
['rel' => 'icon', 'type' => 'image/x-icon', 'href' => self::BASE . '/favicon.ico'],
|
||||
['rel' => 'icon', 'type' => 'image/png', 'sizes' => '32x32', 'href' => self::BASE . '/favicon-32.png'],
|
||||
['rel' => 'icon', 'type' => 'image/png', 'sizes' => '16x16', 'href' => self::BASE . '/favicon-16.png'],
|
||||
['rel' => 'apple-touch-icon', 'sizes' => '180x180', 'href' => self::BASE . '/apple-touch-icon.png'],
|
||||
['rel' => 'manifest', 'href' => self::BASE . '/site.webmanifest'],
|
||||
],
|
||||
];
|
||||
|
||||
return json_encode($payload) ?: '';
|
||||
}
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Evomedien\Vitec\UserFunc;
|
||||
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Evomedien\Vitec\Forms\FormDefinitions;
|
||||
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Service\FlexFormService;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* UserFunc: render ONE of the VITEC form plugins as JSON (headless).
|
||||
* Serves vitec_contactform, vitec_demoform and vitec_helpdeskform — the
|
||||
* CType picks the definition from FormDefinitions.
|
||||
*
|
||||
* Output under content.form:
|
||||
* { "formKey": "contact", "title": "…", "endpoint": "/api/vitec/form/contact",
|
||||
* "honeypot": "_website", "fields": [ … ] }
|
||||
*
|
||||
* The React frontend renders the fields generically and POSTs JSON to the
|
||||
* endpoint (handled by FormSubmissionMiddleware). Exception-safe.
|
||||
*/
|
||||
class FormsJsonRenderer
|
||||
{
|
||||
#[AsAllowedCallable]
|
||||
public function render(string $content, array $conf): string
|
||||
{
|
||||
$row = is_array($this->cObj->data ?? null) ? $this->cObj->data : null;
|
||||
if ($row && isset(FormDefinitions::CTYPE_MAP[(string)($row['CType'] ?? '')])) {
|
||||
return $this->renderForRecord($row);
|
||||
}
|
||||
|
||||
$pageId = 0;
|
||||
$request = $GLOBALS['TYPO3_REQUEST'] ?? null;
|
||||
if ($request !== null) {
|
||||
$pageInfo = $request->getAttribute('frontend.page.information');
|
||||
if ($pageInfo !== null) {
|
||||
$pageId = (int)$pageInfo->getId();
|
||||
}
|
||||
}
|
||||
if ($pageId <= 0) {
|
||||
$pageId = (int)($GLOBALS['TSFE']->id ?? 0);
|
||||
}
|
||||
if ($pageId <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$qb = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
|
||||
$ces = $qb
|
||||
->select('*')
|
||||
->from('tt_content')
|
||||
->where(
|
||||
$qb->expr()->eq('pid', $qb->createNamedParameter($pageId, ParameterType::INTEGER)),
|
||||
$qb->expr()->in('CType', $qb->createNamedParameter(
|
||||
array_keys(FormDefinitions::CTYPE_MAP),
|
||||
\TYPO3\CMS\Core\Database\Connection::PARAM_STR_ARRAY
|
||||
)),
|
||||
$qb->expr()->eq('deleted', 0),
|
||||
$qb->expr()->eq('hidden', 0)
|
||||
)
|
||||
->executeQuery()
|
||||
->fetchAllAssociative();
|
||||
|
||||
if (empty($ces)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->renderForRecord($ces[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $contentElement
|
||||
*/
|
||||
public function renderForRecord(array $contentElement): string
|
||||
{
|
||||
try {
|
||||
$formKey = FormDefinitions::CTYPE_MAP[(string)($contentElement['CType'] ?? '')] ?? '';
|
||||
$definition = FormDefinitions::get($formKey);
|
||||
if ($definition === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$flexFormService = GeneralUtility::makeInstance(FlexFormService::class);
|
||||
$flexFormData = $flexFormService->convertFlexFormContentToArray($contentElement['pi_flexform'] ?? '');
|
||||
$settings = $flexFormData['settings'] ?? [];
|
||||
$debugMode = (bool)($settings['debug'] ?? false);
|
||||
|
||||
$response = [
|
||||
'formKey' => $definition['formKey'],
|
||||
'title' => $definition['title'],
|
||||
'endpoint' => '/api/vitec/form/' . $definition['formKey'],
|
||||
'honeypot' => FormDefinitions::HONEYPOT_FIELD,
|
||||
'fields' => $definition['fields'],
|
||||
];
|
||||
|
||||
if ($debugMode) {
|
||||
$response['debug'] = ['settings' => $settings];
|
||||
}
|
||||
|
||||
return (string)json_encode($response);
|
||||
} catch (\Throwable $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,7 +201,7 @@ class MarketShowJsonRenderer
|
||||
foreach ([400, 800, 1200, 1600] as $width) {
|
||||
$variant = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => $width, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => $width, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
$srcset[] = [
|
||||
'url' => $imageService->getImageUri($variant),
|
||||
@@ -212,7 +212,7 @@ class MarketShowJsonRenderer
|
||||
|
||||
$default = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => 800, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => 800, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
@@ -157,8 +157,8 @@ class ModelcardJsonRenderer
|
||||
'slug' => (string)($row['slug'] ?? ''),
|
||||
'subtitle' => (string)($row['subtitle'] ?? ''),
|
||||
'teaser' => (string)($row['teaser'] ?? ''),
|
||||
'image' => $serializer->image($uid, 'image', 'tx_vitec_domain_model_product')
|
||||
?? $serializer->image($uid, 'productimage', 'tx_vitec_domain_model_product'),
|
||||
'image' => $serializer->image($uid, 'image', 'tx_vitec_domain_model_product', true)
|
||||
?? $serializer->image($uid, 'productimage', 'tx_vitec_domain_model_product', true),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ class ModelcardJsonRenderer
|
||||
'subtitle' => (string)($row['subtitle'] ?? ''),
|
||||
'teaser' => (string)($row['teaser'] ?? ''),
|
||||
'description' => (string)($row['description'] ?? ''),
|
||||
'image' => $serializer->image($uid, 'image', self::MODEL_TABLES[$modelType]),
|
||||
'image' => $serializer->image($uid, 'image', self::MODEL_TABLES[$modelType], true),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -596,7 +596,7 @@ final class NewsJsonRenderer
|
||||
foreach ($rows as $row) {
|
||||
try {
|
||||
$fileReference = $resourceFactory->getFileReferenceObject((int)$row['uid']);
|
||||
$processed = $imageService->applyProcessingInstructions($fileReference, ['width' => 1400]);
|
||||
$processed = $imageService->applyProcessingInstructions($fileReference, ['width' => 1400, 'fileExtension' => 'webp']);
|
||||
|
||||
$files[] = [
|
||||
'uid' => (int)$row['uid'],
|
||||
|
||||
@@ -246,7 +246,8 @@ class ProductListJsonRenderer
|
||||
[
|
||||
'width' => $dimensions['width'],
|
||||
'height' => $dimensions['height'],
|
||||
'crop' => $fileRefData['crop'] ?? null
|
||||
'crop' => $fileRefData['crop'] ?? null,
|
||||
'fileExtension' => 'webp'
|
||||
]
|
||||
);
|
||||
|
||||
@@ -260,7 +261,7 @@ class ProductListJsonRenderer
|
||||
|
||||
$defaultProcessed = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => 800, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => 800, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
$images[] = [
|
||||
@@ -318,7 +319,7 @@ class ProductListJsonRenderer
|
||||
|
||||
$processed = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => 1200, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => 1200, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
@@ -327,14 +327,14 @@ class ProductShowJsonRenderer
|
||||
|
||||
$processedImage = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => '1874c', 'height' => '625c']
|
||||
['width' => '1874c', 'height' => '625c', 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
$srcset = [];
|
||||
foreach ([400, 800, 1200, 1600] as $width) {
|
||||
$processedVariant = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => $width . 'c', 'height' => (int)($width / 3) . 'c']
|
||||
['width' => $width . 'c', 'height' => (int)($width / 3) . 'c', 'fileExtension' => 'webp']
|
||||
);
|
||||
$srcset[] = [
|
||||
'url' => $imageService->getImageUri($processedVariant),
|
||||
@@ -399,7 +399,7 @@ class ProductShowJsonRenderer
|
||||
|
||||
$processedImage = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => 1200]
|
||||
['width' => 1200, 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
@@ -201,7 +201,7 @@ class SolutionShowJsonRenderer
|
||||
foreach ([400, 800, 1200, 1600] as $width) {
|
||||
$variant = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => $width, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => $width, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
$srcset[] = [
|
||||
'url' => $imageService->getImageUri($variant),
|
||||
@@ -212,7 +212,7 @@ class SolutionShowJsonRenderer
|
||||
|
||||
$default = $imageService->applyProcessingInstructions(
|
||||
$fileReference,
|
||||
['width' => 800, 'crop' => $fileRefData['crop'] ?? null]
|
||||
['width' => 800, 'crop' => $fileRefData['crop'] ?? null, 'fileExtension' => 'webp']
|
||||
);
|
||||
|
||||
return [
|
||||
|
||||
Reference in new issue
Block a user