'; private string $formTag = ''; private FlashMessageQueue $flashMessageQueue; private DocHeaderComponent $docHeaderComponent; /** * Init PageRenderer and properties. */ public function __construct( protected readonly PageRenderer $pageRenderer, protected readonly IconFactory $iconFactory, protected readonly UriBuilder $uriBuilder, protected readonly ModuleProvider $moduleProvider, protected readonly FlashMessageService $flashMessageService, protected readonly ExtensionConfiguration $extensionConfiguration, protected readonly ViewInterface $view, protected readonly ComponentFactory $componentFactory, protected readonly ServerRequestInterface $request, ) { $module = $request->getAttribute('module'); if ($module instanceof ModuleInterface) { // third level, needs the second level in order to keep highlighting in the module menu if ($module->getParentModule()?->getParentModule()) { $this->setModuleName($module->getParentModule()->getIdentifier()); } else { $this->setModuleName($module->getIdentifier()); } $this->setModuleName($module->getIdentifier()); } else { $this->setModuleName($request->getAttribute('route')?->getOption('_identifier') ?? ''); } $this->flashMessageQueue = $flashMessageService->getMessageQueueByIdentifier(); $this->docHeaderComponent = GeneralUtility::makeInstance(DocHeaderComponent::class); $this->setUpBasicPageRendererForBackend($pageRenderer, $extensionConfiguration, $request, $this->getLanguageService()); } /** * Add a variable to the view data collection. */ public function assign(string $key, mixed $value): self { $this->view->assign($key, $value); return $this; } /** * Add multiple variables to the view data collection. */ public function assignMultiple(array $values): self { $this->view->assignMultiple($values); return $this; } /** * Render the module. */ public function render(string $templateFileName = ''): string { $this->prepareRender($templateFileName); return $this->pageRenderer->render($this->request); } /** * Render the module and create an HTML 200 response from it. This is a * lazy shortcut so controllers don't need to take care of this in the backend. */ public function renderResponse(string $templateFileName = ''): ResponseInterface { $this->prepareRender($templateFileName); return $this->pageRenderer->renderResponse($this->request); } private function prepareRender(string $templateFileName): void { if ($templateFileName === '') { $extbaseRequestMessage = ''; /** @var ExtbaseRequestParameters|null $extbaseRequestParameters */ $extbaseRequestParameters = $this->request->getAttribute('extbase'); if ($extbaseRequestParameters) { // This extbase specific code is a helper for a more detailed exception // message, and a tribute to extbase backend extensions being upgraded. // Introduced with v13, it could potentially vanish at some point again. $templateFileName = $extbaseRequestParameters->getControllerName() . '/' . ucfirst($extbaseRequestParameters->getControllerActionName()); $extbaseRequestMessage = ' Expected template filename is "' . $templateFileName . '".'; } throw new \InvalidArgumentException('A template filename must be provided.' . $extbaseRequestMessage, 1732184506); } $this->assignMultiple([ 'docHeader' => $this->docHeaderComponent->docHeaderContent($this->request), 'moduleId' => $this->moduleId, 'moduleName' => $this->moduleName, 'moduleClass' => $this->moduleClass, 'moduleLayout' => $this->moduleLayout->value, 'uiBlock' => $this->uiBlock, 'flashMessageQueueIdentifier' => $this->flashMessageQueue->getIdentifier(), 'formTag' => $this->formTag, ]); $this->pageRenderer->getJavaScriptRenderer()->includeAllImports(); $this->pageRenderer->loadJavaScriptModule('bootstrap'); $this->pageRenderer->loadJavaScriptModule('@typo3/backend/dropdown.js'); $this->pageRenderer->loadJavaScriptModule('@typo3/backend/context-help.js'); $this->pageRenderer->loadJavaScriptModule('@typo3/backend/global-event-handler.js'); $this->pageRenderer->loadJavaScriptModule('@typo3/backend/action-dispatcher.js'); $this->pageRenderer->loadJavaScriptModule('@typo3/backend/element/immediate-action-element.js'); $this->pageRenderer->loadJavaScriptModule('@typo3/backend/hotkeys.js'); $this->pageRenderer->addBodyContent($this->bodyTag . $this->view->render($templateFileName)); $this->pageRenderer->setTitle($this->title); $updateSignalDetails = BackendUtility::getUpdateSignalDetails(); if (!empty($updateSignalDetails['html'])) { $this->pageRenderer->addHeaderData(implode("\n", $updateSignalDetails['html'])); } $this->dispatchNotificationMessages(); } /** * @internal */ public function setLayout(ModuleLayout $moduleLayout): self { $this->moduleLayout = $moduleLayout; return $this; } /** * Set to something like '' when a special body tag is needed. */ public function setBodyTag(string $bodyTag): self { $this->bodyTag = $bodyTag; return $this; } /** * Title string of the module: "My module · Edit view" */ public function setTitle(string $title, string $context = ''): self { $titleComponents = [$title]; if ($context !== '') { $titleComponents[] = $context; } $this->title = implode(' · ', $titleComponents); return $this; } /** * Get the DocHeader. Can be used in controllers to add custom * buttons / menus / ... to the doc header. */ public function getDocHeaderComponent(): DocHeaderComponent { return $this->docHeaderComponent; } /** * A "
" tag encapsulating the entire module, including doc-header. */ public function setForm(string $formTag = ''): self { $this->formTag = $formTag; return $this; } /** * Optional 'data-module-id="{moduleId}"' on first
in body. * Can be helpful in JavaScript. */ public function setModuleId(string $moduleId): self { $this->moduleId = $moduleId; return $this; } /** * Optional 'data-module-name="{moduleName}"' on first
in body. * Can be helpful in JavaScript. */ public function setModuleName(string $moduleName): self { $this->moduleName = $moduleName; return $this; } /** * Optional 'class="module {moduleClass}"' on first
in body. * Can be helpful styling modules. */ public function setModuleClass(string $moduleClass): self { $this->moduleClass = $moduleClass; return $this; } /** * Creates a message object and adds it to the FlashMessageQueue. * These messages are automatically rendered when the view is rendered. */ public function addFlashMessage(string $messageBody, string $messageTitle = '', ContextualFeedbackSeverity $severity = ContextualFeedbackSeverity::OK, bool $storeInSession = true): self { $flashMessage = new FlashMessage($messageBody, $messageTitle, $severity, $storeInSession); $this->flashMessageQueue->enqueue($flashMessage); return $this; } /** * ModuleTemplate by default uses queue 'core.template.flashMessages'. Modules * may want to maintain an own queue. Use this method to render flash messages * of a non-default queue at the default position in module HTML output. Call * this method *before* adding single messages with addFlashMessage(). */ public function setFlashMessageQueue(FlashMessageQueue $flashMessageQueue): self { $this->flashMessageQueue = $flashMessageQueue; return $this; } /** * UI block is a spinner shown during browser rendering phase of the module, * automatically removed when rendering finished. This is done by default, * but the UI block can be turned off when needed for whatever reason. */ public function setUiBlock(bool $uiBlock): self { $this->uiBlock = $uiBlock; return $this; } /** * Generates a module actions dropdown in the docheader button bar. * * Creates a dropdown button on the LEFT side (group 0) containing navigation to * submodules or module actions. The button label shows the currently active module/action. */ public function makeDocHeaderModuleMenu(array $additionalQueryParams = []): self { $currentModule = $this->request->getAttribute('module'); if (!($currentModule instanceof ModuleInterface)) { // Early return in case the current request does not provide a module return $this; } if ($currentModule->getParentModule()?->hasParentModule()) { $menuModule = $this->moduleProvider->getModuleForMenu($currentModule->getParentIdentifier(), $this->getBackendUser()); } else { // This is a fallback in case a second level module is called here $menuModule = $this->moduleProvider->getModuleForMenu($currentModule->getIdentifier(), $this->getBackendUser()); } if ($menuModule === null || !$menuModule->hasSubModules()) { return $this; } $itemCount = 0; $dropdownButton = $this->componentFactory->createDropDownButton() ->setLabel($this->getLanguageService()->sL('backend.messages:moduleMenu.dropdown.label')) ->setShowActiveLabelText(true) ->setShowLabelText(true); // Add "Overview" link if exists if ($menuModule->hasSubmoduleOverview()) { $isActive = $menuModule->getIdentifier() === $currentModule->getIdentifier(); $overviewLabel = $this->getLanguageService()->sL('backend.messages:moduleMenu.dropdown.overview'); $dropdownItem = $this->componentFactory->createDropDownRadio() ->setHref((string)$this->uriBuilder->buildUriFromRoute($menuModule->getIdentifier(), $additionalQueryParams)) ->setLabel($overviewLabel) ->setActive($isActive); $dropdownButton->addItem($dropdownItem); $itemCount++; } // Add all submodules foreach ($menuModule->getSubModules() as $module) { $isActive = $module->getIdentifier() === $currentModule->getIdentifier(); $moduleTitle = $this->getLanguageService()->sL($module->getTitle()); $dropdownItem = $this->componentFactory->createDropDownRadio() ->setHref((string)$this->uriBuilder->buildUriFromRoute($module->getIdentifier(), $additionalQueryParams)) ->setLabel($moduleTitle) ->setActive($isActive); $dropdownButton->addItem($dropdownItem); $itemCount++; } // Only add dropdown if there's more than one item if ($itemCount > 1) { // Add to button bar at LEFT, group 0 (first position) $this->getDocHeaderComponent()->getButtonBar()->addButton($dropdownButton, ButtonBar::BUTTON_POSITION_LEFT, 0); } return $this; } /** * Shorthand method to add a new button to the button bar */ public function addButtonToButtonBar( ButtonInterface $button, string $buttonPosition = ButtonBar::BUTTON_POSITION_LEFT, int $buttonGroup = 1 ): self { $this->getDocHeaderComponent()->getButtonBar()->addButton($button, $buttonPosition, $buttonGroup); return $this; } /** * Dispatches all messages in a special FlashMessageQueue to the PageRenderer to be rendered as inline notifications */ private function dispatchNotificationMessages(): void { $notificationQueue = $this->flashMessageService->getMessageQueueByIdentifier(FlashMessageQueue::NOTIFICATION_QUEUE); foreach ($notificationQueue->getAllMessagesAndFlush() as $message) { $notificationInstruction = JavaScriptModuleInstruction::create('@typo3/backend/notification.js'); $notificationInstruction->invoke('showMessage', $message->getTitle(), $message->getMessage(), $message->getSeverity()); $this->pageRenderer->getJavaScriptRenderer()->addJavaScriptModuleInstruction($notificationInstruction); } } private function getLanguageService(): LanguageService { return $GLOBALS['LANG']; } private function getBackendUser(): BackendUserAuthentication { return $GLOBALS['BE_USER']; } }