Skip to content

Commit

Permalink
[FEATURE] Introduce HANDLEBARSTEMPLATE content object
Browse files Browse the repository at this point in the history
  • Loading branch information
eliashaeussler committed Jan 15, 2025
1 parent 6546a4c commit 5c3d435
Show file tree
Hide file tree
Showing 3 changed files with 332 additions and 0 deletions.
169 changes: 169 additions & 0 deletions Classes/Frontend/ContentObject/HandlebarsTemplateContentObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<?php

declare(strict_types=1);

/*
* This file is part of the TYPO3 CMS extension "handlebars".
*
* Copyright (C) 2025 Elias Häußler <e.haeussler@familie-redlich.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

namespace Fr\Typo3Handlebars\Frontend\ContentObject;

use Fr\Typo3Handlebars\Renderer;
use TYPO3\CMS\Core;
use TYPO3\CMS\Frontend;

/**
* HandlebarsTemplateContentObject
*
* @author Elias Häußler <e.haeussler@familie-redlich.de>
* @license GPL-2.0-or-later
*/
final class HandlebarsTemplateContentObject extends Frontend\ContentObject\FluidTemplateContentObject
{
public function __construct(
Frontend\ContentObject\ContentDataProcessor $contentDataProcessor,
private readonly Renderer\Renderer $renderer,
private readonly Renderer\Template\Path\ContentObjectPathProvider $pathProvider,
private readonly Core\TypoScript\TypoScriptService $typoScriptService,
) {
parent::__construct($contentDataProcessor);
}

/**
* @param array<string, mixed> $conf
*/
public function render($conf = []): string
{
if (!is_array($conf)) {
$conf = [];
}

// Create handlebars view
$view = $this->createView($conf);

// Populate template paths
$this->pathProvider->push(
array_intersect_key(
$this->typoScriptService->convertTypoScriptArrayToPlainArray($conf),
[
'partialRootPath' => true,
'partialRootPaths' => true,
'templateRootPath' => true,
'templateRootPaths' => true,
],
),
);

$view->assignMultiple($this->resolveVariables($conf));

$this->renderPageAssetsIntoPageRenderer($conf, $view);

try {
$content = $this->renderer->render($view);
} finally {
// Remove current content object rendering from path provider stack
$this->pathProvider->pop();
}

return $this->applyStandardWrapToRenderedContent($content, $conf);
}

/**
* @param array<string, mixed> $config
*/
private function createView(array $config): Renderer\Template\View\HandlebarsView
{
$format = $this->cObj?->stdWrapValue('format', $config, null);
$view = new Renderer\Template\View\HandlebarsView();

if (is_string($format)) {
$view->setFormat($format);
}

if (isset($config['templateName']) || isset($config['templateName.'])) {
return $view->setTemplatePath(
(string)$this->cObj?->stdWrapValue('templateName', $config),
);
}

if (isset($config['template']) || isset($config['template.'])) {
return $view->setTemplateSource(
(string)$this->cObj?->stdWrapValue('template', $config),
);
}

if (isset($config['file']) || isset($config['file.'])) {
return $view->setTemplatePath(
(string)$this->cObj?->stdWrapValue('file', $config),
);
}

return $view;
}

/**
* @param array<string, mixed> $config
* @return array<string, mixed>
*/
private function resolveVariables(array $config): array
{
$variables = $this->getContentObjectVariables($config);

if ($this->cObj !== null) {
$variables = $this->contentDataProcessor->process($this->cObj, $config, $variables);
}

if (isset($config['settings.'])) {
$variables['settings'] = $this->typoScriptService->convertTypoScriptArrayToPlainArray($config['settings.']);
}

return $variables;
}

/**
* @param array<string, mixed> $config
*/
private function renderPageAssetsIntoPageRenderer(array $config, Renderer\Template\View\HandlebarsView $view): void
{
$headerAssets = $this->renderAssets($config['headerAssets.'] ?? [], $view);
$footerAssets = $this->renderAssets($config['footerAssets.'] ?? [], $view);

if (\trim($headerAssets) !== '') {
$this->getPageRenderer()->addHeaderData($headerAssets);
}

if (\trim($footerAssets) !== '') {
$this->getPageRenderer()->addFooterData($footerAssets);
}
}

/**
* @param array<string, mixed> $config
*/
private function renderAssets(array $config, Renderer\Template\View\HandlebarsView $baseView): string
{
if ($config === []) {
return '';
}

$view = $this->createView($config);
$view->assignMultiple($baseView->getVariables());

return $this->renderer->render($view);
}
}
157 changes: 157 additions & 0 deletions Classes/Renderer/Template/Path/ContentObjectPathProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
<?php

declare(strict_types=1);

/*
* This file is part of the TYPO3 CMS extension "handlebars".
*
* Copyright (C) 2025 Elias Häußler <e.haeussler@familie-redlich.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

namespace Fr\Typo3Handlebars\Renderer\Template\Path;

use TYPO3\CMS\Core;

/**
* ContentObjectPathProvider
*
* @author Elias Häußler <e.haeussler@familie-redlich.de>
* @license GPL-2.0-or-later
*
* @phpstan-type RootPathStackItem array{
* current: array<int, string>,
* merged: array<int, string>|null,
* }
*/
final class ContentObjectPathProvider implements PathProvider, Core\SingletonInterface
{
/**
* @var list<array{
* partialRootPaths: RootPathStackItem,
* templateRootPaths: RootPathStackItem,
* }>
*/
private array $stack = [];

private ?int $currentItem = null;

/**
* @param array<string, mixed> $configuration
*/
public function push(array $configuration): void
{
$partialRootPaths = [];
$templateRootPaths = [];

if (is_array($configuration['templateRootPaths'] ?? null)) {
$templateRootPaths = $configuration['templateRootPaths'];
}
if (is_array($configuration['partialRootPaths'] ?? null)) {
$partialRootPaths = $configuration['partialRootPaths'];
}

// A single root path receives highest priority
if (is_string($configuration['templateRootPath'] ?? null)) {
$templateRootPaths[PHP_INT_MAX] = $configuration['templateRootPath'];
}
if (is_string($configuration['partialRootPath'] ?? null)) {
$partialRootPaths[PHP_INT_MAX] = $configuration['partialRootPath'];
}

ksort($templateRootPaths);
ksort($partialRootPaths);

$this->stack[] = [
'partialRootPaths' => [
'current' => $partialRootPaths,
'merged' => null,
],
'templateRootPaths' => [
'current' => $templateRootPaths,
'merged' => null,
],
];

if ($this->currentItem === null) {
$this->currentItem = 0;
} else {
$this->currentItem++;
}
}

public function pop(): void
{
if ($this->stack === []) {
return;
}

array_pop($this->stack);

if ($this->stack === []) {
$this->currentItem = null;
} else {
$this->currentItem--;
}
}

public function getPartialRootPaths(): array
{
return $this->getMergedRootPathsFromStack('partialRootPaths');
}

public function getTemplateRootPaths(): array
{
return $this->getMergedRootPathsFromStack('templateRootPaths');
}

public function isCacheable(): bool
{
// Caching is done internally, based on the current stack
return false;
}

/**
* @param 'partialRootPaths'|'templateRootPaths' $type
* @return array<int, string>
*/
private function getMergedRootPathsFromStack(string $type): array
{
if ($this->currentItem === null) {
return [];
}

// Merge and cache root paths
if ($this->stack[$this->currentItem][$type]['merged'] === null) {
/* @phpstan-ignore offsetAccess.notFound */
$this->stack[$this->currentItem][$type]['merged'] = $this->stack[$this->currentItem][0]['current'];

for ($i = 1; $i < $this->currentItem; $i++) {
Core\Utility\ArrayUtility::mergeRecursiveWithOverrule(
$this->stack[$this->currentItem][$type]['merged'],
$this->stack[$i][$type]['current'],
);
}
}

/* @phpstan-ignore return.type */
return $this->stack[$this->currentItem][$type]['merged'];
}

public static function getPriority(): int
{
return 100;
}
}
6 changes: 6 additions & 0 deletions Configuration/Services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ services:
Fr\Typo3Handlebars\Renderer\Template\TemplateResolver:
alias: 'handlebars.template_resolver'

# Content object
Fr\Typo3Handlebars\Frontend\ContentObject\HandlebarsTemplateContentObject:
tags:
- name: frontend.contentobject
identifier: 'HANDLEBARSTEMPLATE'

# Template
handlebars.template_resolver:
class: 'Fr\Typo3Handlebars\Renderer\Template\HandlebarsTemplateResolver'
Expand Down

0 comments on commit 5c3d435

Please sign in to comment.