Files
VITEC-website/packages/evo_megamenu_json/Classes/UserFunc/MegamenuJsonRenderer.php
Oliver Rasche 5bb4e374b4 Add evo_megamenu_json extension and seed the megamenu
- new project-neutral extension evo_megamenu_json (Evomedien): a Megamenu
  plugin holding entries, columns, items, teaser cards and a featured slot
  as nested records; presentation settings travel with the payload so the
  front end reads behaviour instead of hard-coding it; published as
  page.10.fields.megaMenu, driven by the site settings megamenu.contentUid
  and megamenu.storagePid
- vitec:seed-megamenu fills one element from the live structure: 5 entries,
  26 columns, 95 items, 4 story cards; items without a page yet are flagged
  pending so the front end falls back to the column link
- vitec:debug-sets prints the resolved site set order
- read-only diagnostics: check_megamenu_schema.php, check_typoscript_templates.php
- docs: schema updates run via extension:setup (database:updateschema no
  longer exists in v14), and a JSON Accept header activates headless-mixed,
  which strips every set-added page field
2026-09-11 11:44:20 +02:00

91 lines
3.3 KiB
PHP

<?php
declare(strict_types=1);
namespace Evomedien\EvoMegamenuJson\UserFunc;
use Doctrine\DBAL\ParameterType;
use Evomedien\EvoMegamenuJson\Service\MenuBuilder;
use TYPO3\CMS\Core\Attribute\AsAllowedCallable;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
/**
* Emits one megamenu as JSON. Serves both wiring points:
*
* - as a field of the plugin's own content element (no configuration -
* the element renders itself),
* - as a page-level field for the site header, configured with either
* `contentUid` (a specific plugin element) or `storagePid` (the first
* megamenu element found on that folder).
*
* TYPO3 v14 hands the ContentObjectRenderer in through the setter rather
* than the constructor; without it $this->cObj stays null and the element
* path cannot see which record it is rendering.
*/
final class MegamenuJsonRenderer
{
private const CTYPE = 'evomegamenujson_megamenu';
protected ?ContentObjectRenderer $cObj = null;
public function setContentObjectRenderer(ContentObjectRenderer $cObj): void
{
$this->cObj = $cObj;
}
/**
* @param array<string,mixed> $conf TypoScript: contentUid, storagePid
*/
#[AsAllowedCallable]
public function render(string $content, array $conf): string
{
try {
$cObj = $this->cObj ?? GeneralUtility::makeInstance(ContentObjectRenderer::class);
$row = null;
$uid = (int)($conf['contentUid'] ?? 0);
if ($uid <= 0) {
$data = is_array($cObj->data ?? null) ? $cObj->data : [];
if ((string)($data['CType'] ?? '') === self::CTYPE) {
// rendering the element itself
$row = $data;
$uid = (int)($data['uid'] ?? 0);
}
}
if ($uid <= 0 && (int)($conf['storagePid'] ?? 0) > 0) {
$uid = $this->firstElementOn((int)$conf['storagePid']);
}
if ($uid <= 0) {
return '';
}
$menu = GeneralUtility::makeInstance(MenuBuilder::class)->build($uid, $cObj, $row);
return (string)json_encode($menu, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
} catch (\Throwable) {
// A broken menu must never take the page payload down with it.
return '';
}
}
/** uid of the first megamenu element on a storage folder, 0 when none. */
private function firstElementOn(int $pid): int
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tt_content');
$queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class));
$uid = $queryBuilder->select('uid')->from('tt_content')
->where(
$queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($pid, ParameterType::INTEGER)),
$queryBuilder->expr()->eq('CType', $queryBuilder->createNamedParameter(self::CTYPE))
)
->orderBy('sorting', 'ASC')
->setMaxResults(1)
->executeQuery()->fetchOne();
return is_numeric($uid) ? (int)$uid : 0;
}
}