CObjectViewHelper.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. <?php
  2. declare(strict_types=1);
  3. namespace Opentalent\OtTemplating\ViewHelpers;
  4. use Psr\Http\Message\ServerRequestInterface;
  5. use TYPO3\CMS\Core\Context\Context;
  6. use TYPO3\CMS\Core\Routing\PageArguments;
  7. use TYPO3\CMS\Core\Site\Entity\SiteInterface;
  8. use TYPO3\CMS\Core\Site\SiteFinder;
  9. use TYPO3\CMS\Core\TimeTracker\TimeTracker;
  10. use TYPO3\CMS\Core\Utility\GeneralUtility;
  11. use TYPO3\CMS\Extbase\Configuration\ConfigurationManagerInterface;
  12. use TYPO3\CMS\Extbase\Reflection\ObjectAccess;
  13. use TYPO3\CMS\Fluid\Core\Rendering\RenderingContext;
  14. use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
  15. use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
  16. use TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController;
  17. use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
  18. use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
  19. use TYPO3Fluid\Fluid\Core\ViewHelper\Exception;
  20. use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithContentArgumentAndRenderStatic;
  21. /**
  22. * This view helper is a modified version of the TYPO3\CMS\Fluid\ViewHelpers\CObjectViewHelper class
  23. * that allow to override the TS setup of the object by given variables
  24. *
  25. * It accepts the same arguments as \TYPO3\CMS\Fluid\ViewHelpers\CObjectViewHelper,
  26. * and an additional argument 'settings'
  27. *
  28. * @see https://docs.typo3.org/p/georgringer/news/master/en-us/Introduction/Index.html
  29. *
  30. * example:
  31. *
  32. * {namespace ot=Opentalent\OtTemplating\ViewHelpers}
  33. *
  34. * <ot:cObject typoscriptObjectPath="lib.tx_ottemplating.widgets.news_list"
  35. * settings="{'settings.defaultPageUid': 1}" />
  36. *
  37. * @package Opentalent\OtTemplating\ViewHelpers
  38. */
  39. final class CObjectViewHelper extends AbstractViewHelper
  40. {
  41. use CompileWithContentArgumentAndRenderStatic;
  42. /**
  43. * Disable escaping of child nodes' output
  44. *
  45. * @var bool
  46. */
  47. protected $escapeChildren = false;
  48. /**
  49. * Disable escaping of this node's output
  50. *
  51. * @var bool
  52. */
  53. protected $escapeOutput = false;
  54. public function initializeArguments(): void
  55. {
  56. $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');
  57. $this->registerArgument('typoscriptObjectPath', 'string', 'the TypoScript setup path of the TypoScript object to render', true);
  58. $this->registerArgument('currentValueKey', 'string', 'currentValueKey');
  59. $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, '');
  60. // <-- Additional paramter
  61. $this->registerArgument(
  62. 'settings',
  63. 'array',
  64. 'For each key in this array, the setup entry will be replaced by the corresponding value',
  65. false,
  66. []
  67. );
  68. }
  69. /**
  70. * Renders the TypoScript object in the given TypoScript setup path.
  71. *
  72. * @throws Exception
  73. */
  74. public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext): string
  75. {
  76. $data = $renderChildrenClosure();
  77. $typoscriptObjectPath = (string)$arguments['typoscriptObjectPath'];
  78. $currentValueKey = $arguments['currentValueKey'];
  79. $table = $arguments['table'];
  80. /** @var RenderingContext $renderingContext */
  81. $request = $renderingContext->getRequest();
  82. $contentObjectRenderer = self::getContentObjectRenderer($request);
  83. $contentObjectRenderer->setRequest($request);
  84. $tsfeBackup = null;
  85. if (!isset($GLOBALS['TSFE']) || !($GLOBALS['TSFE'] instanceof TypoScriptFrontendController)) {
  86. $tsfeBackup = self::simulateFrontendEnvironment();
  87. }
  88. $currentValue = null;
  89. if (is_object($data)) {
  90. $data = ObjectAccess::getGettableProperties($data);
  91. } elseif (is_string($data) || is_numeric($data)) {
  92. $currentValue = (string)$data;
  93. $data = [$data];
  94. }
  95. $contentObjectRenderer->start($data, $table);
  96. if ($currentValue !== null) {
  97. $contentObjectRenderer->setCurrentVal($currentValue);
  98. } elseif ($currentValueKey !== null && isset($data[$currentValueKey])) {
  99. $contentObjectRenderer->setCurrentVal($data[$currentValueKey]);
  100. }
  101. $pathSegments = GeneralUtility::trimExplode('.', $typoscriptObjectPath);
  102. $lastSegment = (string)array_pop($pathSegments);
  103. $setup = self::getConfigurationManager()->getConfiguration(ConfigurationManagerInterface::CONFIGURATION_TYPE_FULL_TYPOSCRIPT);
  104. foreach ($pathSegments as $segment) {
  105. if (!array_key_exists($segment . '.', $setup)) {
  106. throw new Exception(
  107. 'TypoScript object path "' . $typoscriptObjectPath . '" does not exist',
  108. 1253191023
  109. );
  110. }
  111. $setup = $setup[$segment . '.'];
  112. }
  113. if (!isset($setup[$lastSegment])) {
  114. throw new Exception(
  115. 'No Content Object definition found at TypoScript object path "' . $typoscriptObjectPath . '"',
  116. 1540246570
  117. );
  118. }
  119. // <---- Added by Opentalent to override the TS setup
  120. $setup = self::evalConfiguration($setup, $lastSegment, $arguments['settings']);
  121. // ----->
  122. $content = self::renderContentObject($contentObjectRenderer, $setup, $typoscriptObjectPath, $lastSegment);
  123. if (!isset($GLOBALS['TSFE']) || !($GLOBALS['TSFE'] instanceof TypoScriptFrontendController)) {
  124. self::resetFrontendEnvironment($tsfeBackup);
  125. }
  126. return $content;
  127. }
  128. /**
  129. * Renders single content object and increases time tracker stack pointer
  130. */
  131. protected static function renderContentObject(ContentObjectRenderer $contentObjectRenderer, array $setup, string $typoscriptObjectPath, string $lastSegment): string
  132. {
  133. $timeTracker = GeneralUtility::makeInstance(TimeTracker::class);
  134. if ($timeTracker->LR) {
  135. $timeTracker->push('/f:cObject/', '<' . $typoscriptObjectPath);
  136. }
  137. $timeTracker->incStackPointer();
  138. $content = $contentObjectRenderer->cObjGetSingle($setup[$lastSegment], $setup[$lastSegment . '.'] ?? [], $typoscriptObjectPath);
  139. $timeTracker->decStackPointer();
  140. if ($timeTracker->LR) {
  141. $timeTracker->pull($content);
  142. }
  143. return $content;
  144. }
  145. protected static function getConfigurationManager(): ConfigurationManagerInterface
  146. {
  147. // @todo: this should be replaced by DI once Fluid can handle DI properly
  148. return GeneralUtility::getContainer()->get(ConfigurationManagerInterface::class);
  149. }
  150. protected static function getContentObjectRenderer(ServerRequestInterface $request): ContentObjectRenderer
  151. {
  152. if (($GLOBALS['TSFE'] ?? null) instanceof TypoScriptFrontendController) {
  153. $tsfe = $GLOBALS['TSFE'];
  154. } else {
  155. $site = $request->getAttribute('site');
  156. if (!($site instanceof SiteInterface)) {
  157. $sites = GeneralUtility::makeInstance(SiteFinder::class)->getAllSites();
  158. $site = reset($sites);
  159. }
  160. $language = $request->getAttribute('language') ?? $site->getDefaultLanguage();
  161. $pageArguments = $request->getAttribute('routing') ?? new PageArguments(0, '0', []);
  162. $tsfe = GeneralUtility::makeInstance(
  163. TypoScriptFrontendController::class,
  164. GeneralUtility::makeInstance(Context::class),
  165. $site,
  166. $language,
  167. $pageArguments,
  168. GeneralUtility::makeInstance(FrontendUserAuthentication::class)
  169. );
  170. }
  171. $contentObjectRenderer = GeneralUtility::makeInstance(ContentObjectRenderer::class, $tsfe);
  172. $parent = $request->getAttribute('currentContentObject');
  173. if ($parent instanceof ContentObjectRenderer) {
  174. $contentObjectRenderer->setParent($parent->data, $parent->currentRecord);
  175. }
  176. return $contentObjectRenderer;
  177. }
  178. /**
  179. * \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer->cObjGetSingle() relies on $GLOBALS['TSFE']
  180. */
  181. protected static function simulateFrontendEnvironment(): ?TypoScriptFrontendController
  182. {
  183. $tsfeBackup = $GLOBALS['TSFE'] ?? null;
  184. $GLOBALS['TSFE'] = new \stdClass();
  185. $GLOBALS['TSFE']->cObj = GeneralUtility::makeInstance(ContentObjectRenderer::class);
  186. return $tsfeBackup;
  187. }
  188. /**
  189. * Resets $GLOBALS['TSFE'] if it was previously changed by simulateFrontendEnvironment()
  190. */
  191. protected static function resetFrontendEnvironment(?TypoScriptFrontendController $tsfeBackup): void
  192. {
  193. $GLOBALS['TSFE'] = $tsfeBackup;
  194. }
  195. /**
  196. * Explicitly set argument name to be used as content.
  197. */
  198. public function resolveContentArgumentName(): string
  199. {
  200. return 'data';
  201. }
  202. /**
  203. * -- Additional method --
  204. * Recursively replace any {$var} value in the given configuration
  205. * if 'var' is a key in the 'overrideSetup' array
  206. *
  207. * @param array $setup
  208. * @param string $entry_name
  209. * @param array $override_settings
  210. * @return array
  211. */
  212. private static function evalConfiguration(array $setup, string $entry_name, array $override_settings) {
  213. $entry_setup = $setup[$entry_name . '.'];
  214. foreach ($override_settings as $key => $val) {
  215. $override = [];
  216. $path = explode('.', $key);
  217. foreach (array_reverse($path) as $i => $segment) {
  218. if ($i == 0) {
  219. $override[$segment] = $val;
  220. } else {
  221. $override = [$segment . '.' => $override];
  222. }
  223. }
  224. $entry_setup = self::merge($entry_setup, $override);
  225. }
  226. $setup[$entry_name . '.'] = $entry_setup;
  227. return $setup;
  228. }
  229. /**
  230. * -- Additional method --
  231. * Similar to array_merge_recursive, except that the end nodes of the
  232. * base array is replaced and not merged.
  233. *
  234. * @param $base_array
  235. * @param $override
  236. * @return mixed
  237. */
  238. private static function merge($base_array, $override) {
  239. foreach ($base_array as $key => $val) {
  240. if (!isset($override[$key])) {
  241. continue;
  242. }
  243. if (is_array($val)) {
  244. $base_array[$key] = self::merge($val, $override[$key]);
  245. } else {
  246. $base_array[$key] = $override[$key];
  247. }
  248. }
  249. return $base_array;
  250. }
  251. }