* * @package Opentalent\OtTemplating\ViewHelpers */ final class CObjectViewHelper extends AbstractViewHelper { use CompileWithContentArgumentAndRenderStatic; /** * Disable escaping of child nodes' output * * @var bool */ protected $escapeChildren = false; /** * Disable escaping of this node's output * * @var bool */ protected $escapeOutput = false; public function initializeArguments(): void { $this->registerArgument('data', 'mixed', 'the data to be used for rendering the cObject. Can be an object, array or string. If this argument is not set, child nodes will be used'); $this->registerArgument('typoscriptObjectPath', 'string', 'the TypoScript setup path of the TypoScript object to render', true); $this->registerArgument('currentValueKey', 'string', 'currentValueKey'); $this->registerArgument('table', 'string', 'the table name associated with "data" argument. Typically tt_content or one of your custom tables. This argument should be set if rendering a FILES cObject where file references are used, or if the data argument is a database record.', false, ''); // <-- Additional paramter $this->registerArgument( 'settings', 'array', 'For each key in this array, the setup entry will be replaced by the corresponding value', false, [] ); } /** * Renders the TypoScript object in the given TypoScript setup path. * * @throws Exception */ public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext): string { $data = $renderChildrenClosure(); $typoscriptObjectPath = (string)$arguments['typoscriptObjectPath']; $currentValueKey = $arguments['currentValueKey']; $table = $arguments['table']; /** @var RenderingContext $renderingContext */ $request = $renderingContext->getRequest(); $contentObjectRenderer = self::getContentObjectRenderer($request); $contentObjectRenderer->setRequest($request); $tsfeBackup = null; if (!isset($GLOBALS['TSFE']) || !($GLOBALS['TSFE'] instanceof TypoScriptFrontendController)) { $tsfeBackup = self::simulateFrontendEnvironment(); } $currentValue = null; if (is_object($data)) { $data = ObjectAccess::getGettableProperties($data); } elseif (is_string($data) || is_numeric($data)) { $currentValue = (string)$data; $data = [$data]; } $contentObjectRenderer->start($data, $table); if ($currentValue !== null) { $contentObjectRenderer->setCurrentVal($currentValue); } elseif ($currentValueKey !== null && isset($data[$currentValueKey])) { $contentObjectRenderer->setCurrentVal($data[$currentValueKey]); } $pathSegments = GeneralUtility::trimExplode('.', $typoscriptObjectPath); $lastSegment = (string)array_pop($pathSegments); $setup = self::getConfigurationManager()->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT); foreach ($pathSegments as $segment) { if (!array_key_exists($segment . '.', $setup)) { throw new Exception( 'TypoScript object path "' . $typoscriptObjectPath . '" does not exist', 1253191023 ); } $setup = $setup[$segment . '.']; } if (!isset($setup[$lastSegment])) { throw new Exception( 'No Content Object definition found at TypoScript object path "' . $typoscriptObjectPath . '"', 1540246570 ); } // <---- Added by Opentalent to override the TS setup $setup = self::evalConfiguration($setup, $lastSegment, $arguments['settings']); // -----> $content = self::renderContentObject($contentObjectRenderer, $setup, $typoscriptObjectPath, $lastSegment); if (!isset($GLOBALS['TSFE']) || !($GLOBALS['TSFE'] instanceof TypoScriptFrontendController)) { self::resetFrontendEnvironment($tsfeBackup); } return $content; } /** * Renders single content object and increases time tracker stack pointer */ protected static function renderContentObject(ContentObjectRenderer $contentObjectRenderer, array $setup, string $typoscriptObjectPath, string $lastSegment): string { $timeTracker = GeneralUtility::makeInstance(TimeTracker::class); if ($timeTracker->LR) { $timeTracker->push('/f:cObject/', '<' . $typoscriptObjectPath); } $timeTracker->incStackPointer(); $content = $contentObjectRenderer->cObjGetSingle($setup[$lastSegment], $setup[$lastSegment . '.'] ?? [], $typoscriptObjectPath); $timeTracker->decStackPointer(); if ($timeTracker->LR) { $timeTracker->pull($content); } return $content; } protected static function getConfigurationManager(): ConfigurationManagerInterface { // @todo: this should be replaced by DI once Fluid can handle DI properly return GeneralUtility::getContainer()->get(ConfigurationManagerInterface::class); } protected static function getContentObjectRenderer(ServerRequestInterface $request): ContentObjectRenderer { if (($GLOBALS['TSFE'] ?? null) instanceof TypoScriptFrontendController) { $tsfe = $GLOBALS['TSFE']; } else { $site = $request->getAttribute('site'); if (!($site instanceof SiteInterface)) { $sites = GeneralUtility::makeInstance(SiteFinder::class)->getAllSites(); $site = reset($sites); } $language = $request->getAttribute('language') ?? $site->getDefaultLanguage(); $pageArguments = $request->getAttribute('routing') ?? new PageArguments(0, '0', []); $tsfe = GeneralUtility::makeInstance( TypoScriptFrontendController::class, GeneralUtility::makeInstance(Context::class), $site, $language, $pageArguments, GeneralUtility::makeInstance(FrontendUserAuthentication::class) ); } $contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class, $tsfe); $parent = $request->getAttribute('currentContentObject'); if ($parent instanceof ContentObjectRenderer) { $contentObjectRenderer->setParent($parent->data, $parent->currentRecord); } return $contentObjectRenderer; } /** * \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer->cObjGetSingle() relies on $GLOBALS['TSFE'] */ protected static function simulateFrontendEnvironment(): ?TypoScriptFrontendController { $tsfeBackup = $GLOBALS['TSFE'] ?? null; $GLOBALS['TSFE'] = new \stdClass(); $GLOBALS['TSFE']->cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class); return $tsfeBackup; } /** * Resets $GLOBALS['TSFE'] if it was previously changed by simulateFrontendEnvironment() */ protected static function resetFrontendEnvironment(?TypoScriptFrontendController $tsfeBackup): void { $GLOBALS['TSFE'] = $tsfeBackup; } /** * Explicitly set argument name to be used as content. */ public function resolveContentArgumentName(): string { return 'data'; } /** * -- Additional method -- * Recursively replace any {$var} value in the given configuration * if 'var' is a key in the 'overrideSetup' array * * @param array $setup * @param string $entry_name * @param array $override_settings * @return array */ private static function evalConfiguration(array $setup, string $entry_name, array $override_settings) { $entry_setup = $setup[$entry_name . '.']; foreach ($override_settings as $key => $val) { $override = []; $path = explode('.', $key); foreach (array_reverse($path) as $i => $segment) { if ($i == 0) { $override[$segment] = $val; } else { $override = [$segment . '.' => $override]; } } $entry_setup = self::merge($entry_setup, $override); } $setup[$entry_name . '.'] = $entry_setup; return $setup; } /** * -- Additional method -- * Similar to array_merge_recursive, except that the end nodes of the * base array is replaced and not merged. * * @param $base_array * @param $override * @return mixed */ private static function merge($base_array, $override) { foreach ($base_array as $key => $val) { if (!isset($override[$key])) { continue; } if (is_array($val)) { $base_array[$key] = self::merge($val, $override[$key]); } else { $base_array[$key] = $override[$key]; } } return $base_array; } }