cObj` dynamically. The renderer is * handed over through this setter, and only when the method exists at all - * ContentObjectRenderer::callUserFunction() duck-types it with * `is_callable([$classObj, 'setContentObjectRenderer'])`. Without the method * `$this->cObj` stays null and the userFunc never sees its own record. */ public function setContentObjectRenderer(ContentObjectRenderer $cObj): void { $this->cObj = $cObj; } #[AsAllowedCallable] public function render(string $content, array $conf): string { try { $row = is_array($this->cObj?->data ?? null) ? $this->cObj->data : null; if ($row === null) { return ''; } $uid = (int)($row['uid'] ?? 0); if ($uid <= 0) { return ''; } // No shortcut on the tx_vitec_bg_image counter: sys_file_reference is // the single source of truth, and a counter that is stale or missing // (schema not updated yet) would silently swallow a set image. One // indexed query per container is cheaper than that class of bug. $image = GeneralUtility::makeInstance(UsecaseSerializer::class) ->image($uid, 'tx_vitec_bg_image', 'tt_content'); if ($image === null) { return ''; } return (string)json_encode([ 'image' => $image, 'size' => $this->resolveSize($row), 'position' => $this->resolvePosition($row), ]); } catch (\Throwable $e) { return ''; } } /** * The stored size, resolved into something the front end can hand straight * to CSS. `custom` is the only value that needs a second field; turning it * into "80%" here keeps the promise that `size` is always usable as-is, * instead of making every consumer look up a companion field. * * A custom size without a usable percentage falls back to `auto` - the CSS * default - rather than emitting "0%" or the meaningless word "custom". * * @param array $row */ private function resolveSize(array $row): string { $size = (string)($row['tx_vitec_bg_size'] ?? 'cover'); if ($size !== 'custom') { return $size; } $percent = (int)($row['tx_vitec_bg_size_percent'] ?? 0); return $percent > 0 ? $percent . '%' : 'auto'; } /** * The stored position, again resolved into one CSS-ready value. * * The four custom fields are edge offsets, but CSS cannot take a top AND a * bottom offset at once, so they are folded into the percentage form. That * form says exactly the same thing: in `background-position` a percentage * aligns the same point of image and container, so "10% from the right edge" * IS `90%`. Nothing is lost in the translation. * * Per axis the near edge wins when both are filled - `left` over `right`, * `top` over `bottom` - because two opposing offsets contradict each other * and silently picking one beats emitting something invalid. * * @param array $row */ private function resolvePosition(array $row): string { $position = (string)($row['tx_vitec_bg_position'] ?? 'center center'); if ($position !== 'custom') { return $position; } $x = $this->axisPercent($row, 'tx_vitec_bg_pos_left', 'tx_vitec_bg_pos_right'); $y = $this->axisPercent($row, 'tx_vitec_bg_pos_top', 'tx_vitec_bg_pos_bottom'); return $x . '% ' . $y . '%'; } /** * One axis as a percentage. An empty field means "centred on this axis" - * which is why the columns are nullable: 0 is a legitimate offset (flush * against that edge) and has to stay distinguishable from "not set". * * @param array $row */ private function axisPercent(array $row, string $nearField, string $farField): int { $near = $row[$nearField] ?? null; if ($near !== null && $near !== '') { return (int)$near; } $far = $row[$farField] ?? null; if ($far !== null && $far !== '') { return 100 - (int)$far; } return 50; } }