Migrate to 12.1.x-dev (current main)

This commit is contained in:
Daniel Siepmann 2022-12-06 08:18:42 +01:00
parent 295f564672
commit 23b9672e51
16 changed files with 244 additions and 136 deletions

View file

@ -21,62 +21,59 @@ declare(strict_types=1);
* 02110-1301, USA. * 02110-1301, USA.
*/ */
namespace DanielSiepmann\DsSite\Hooks\Backend; namespace DanielSiepmann\DsSite\EventListener;
use TYPO3\CMS\Backend\Controller\PageLayoutController; use TYPO3\CMS\Backend\Controller\Event\ModifyPageLayoutContentEvent;
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\View\StandaloneView; use TYPO3\CMS\Fluid\View\StandaloneView;
use TYPO3\CMS\Frontend\Resource\FileCollector; use TYPO3\CMS\Frontend\Resource\FileCollector;
class PageLayoutHeader class PageLayoutHeader
{ {
/** public function __invoke(ModifyPageLayoutContentEvent $event): void
* @var array {
*/ $request = $event->getRequest();
private $pageinfo = []; $pageinfo = BackendUtility::readPageAccess(
(int)($request->getParsedBody()['id'] ?? $request->getQueryParams()['id'] ?? 0),
public function render( ''
array $params, );
PageLayoutController $pageLayoutController
): string {
if (is_array($pageLayoutController->pageinfo)) {
$this->pageinfo = $pageLayoutController->pageinfo;
}
// TODO: Check whether two levels up is uid 2, which holds all blog posts // TODO: Check whether two levels up is uid 2, which holds all blog posts
// To prevent rendering on non blog posts // To prevent rendering on non blog posts
// Maybe we have rootline ? // Maybe we have rootline ?
$view = $this->getView(); $view = $this->getView();
$view->setRequest($request);
$view->assignMultiple([ $view->assignMultiple([
'record' => $this->pageinfo, 'record' => $pageinfo,
'metaInfo' => [ 'metaInfo' => [
[ [
'label' => 'meta-description', 'label' => 'meta-description',
'value' => $this->pageinfo['description'] ?? '', 'value' => $pageinfo['description'] ?? '',
'field' => 'description', 'field' => 'description',
'type' => 'string', 'type' => 'string',
], ],
[ [
'label' => 'introduction', 'label' => 'introduction',
'value' => $this->pageinfo['abstract'] ?? '', 'value' => $pageinfo['abstract'] ?? '',
'field' => 'abstract', 'field' => 'abstract',
'type' => 'string', 'type' => 'string',
], ],
[ [
'label' => 'published', 'label' => 'published',
'value' => $this->pageinfo['lastUpdated'] ?? '', 'value' => $pageinfo['lastUpdated'] ?? '',
'field' => 'lastUpdated', 'field' => 'lastUpdated',
'type' => 'date', 'type' => 'date',
], ],
[ [
'label' => 'updated', 'label' => 'updated',
'value' => $this->pageinfo['SYS_LASTCHANGED'] ?? '', 'value' => $pageinfo['SYS_LASTCHANGED'] ?? '',
'type' => 'date', 'type' => 'date',
], ],
[ [
'label' => 'media', 'label' => 'media',
'value' => $this->resolvePageMedia(), 'value' => $this->resolvePageMedia((int) $pageinfo['uid']),
'field' => 'media', 'field' => 'media',
'type' => 'files', 'type' => 'files',
], ],
@ -84,7 +81,7 @@ class PageLayoutHeader
]); ]);
$view->setTemplatePathAndFilename('EXT:ds_site/Resources/Private/Templates/Backend/Page/MetaInfo.html'); $view->setTemplatePathAndFilename('EXT:ds_site/Resources/Private/Templates/Backend/Page/MetaInfo.html');
return $view->render(); $event->addHeaderContent($view->render());
} }
private function getView(): StandaloneView private function getView(): StandaloneView
@ -92,11 +89,10 @@ class PageLayoutHeader
return GeneralUtility::makeInstance(StandaloneView::class); return GeneralUtility::makeInstance(StandaloneView::class);
} }
private function resolvePageMedia(): array private function resolvePageMedia(int $pageUid): array
{ {
$page = ['uid' => $this->pageinfo['uid'] ?? ''];
$files = new FileCollector(); $files = new FileCollector();
$files->addFilesFromRelation('pages', 'media', $page); $files->addFilesFromRelation('pages', 'media', ['uid' => $pageUid]);
return $files->getFiles(); return $files->getFiles();
} }

View file

@ -83,14 +83,9 @@ class SitemapDataProvider extends RecordsXmlSitemapDataProvider
) )
); );
$result = $queryBuilder->execute()->fetchAll();
if ($result === false) {
return [];
}
return array_map(function (array $row) { return array_map(function (array $row) {
return (int) $row['uid']; return (int) $row['uid'];
}, $result); }, $queryBuilder->execute()->fetchAll());
} }
private function createAdditionalWhereForPageUids(array $pageUids): string private function createAdditionalWhereForPageUids(array $pageUids): string

View file

@ -24,29 +24,129 @@ declare(strict_types=1);
namespace DanielSiepmann\DsSite\Frontend\RssFeed; namespace DanielSiepmann\DsSite\Frontend\RssFeed;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use TYPO3Fluid\Fluid\View\TemplateView;
use TYPO3\CMS\Core\Http\PropagateResponseException;
use TYPO3\CMS\Core\TypoScript\TypoScriptService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Fluid\Core\Rendering\RenderingContextFactory;
use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer;
use TYPO3\CMS\Seo\XmlSitemap\XmlSitemapRenderer as Typo3XmlSitemapRenderer; use TYPO3\CMS\Frontend\Controller\ErrorController;
use TYPO3\CMS\Seo\XmlSitemap\Exception\InvalidConfigurationException;
use TYPO3\CMS\Seo\XmlSitemap\XmlSitemapDataProviderInterface;
class XmlSitemapRenderer extends Typo3XmlSitemapRenderer class XmlSitemapRenderer
{ {
/**
* @var ContentObjectRenderer
*/
public $cObj;
private array $typoScriptConfiguration = []; private array $typoScriptConfiguration = [];
protected array $configuration;
private ContentObjectRenderer $contentObjectRenderer;
protected TemplateView $view;
public function __construct(
protected TypoScriptService $typoScriptService,
protected RenderingContextFactory $renderingContextFactory,
) {
}
public function setContentObjectRenderer(ContentObjectRenderer $contentObjectRenderer): void
{
$this->contentObjectRenderer = $contentObjectRenderer;
}
protected function initialize(array $fullConfiguration): void
{
$this->configuration = $this->typoScriptService->convertTypoScriptArrayToPlainArray($fullConfiguration['plugin.']['tx_seo.'] ?? []);
$renderingContext = $this->renderingContextFactory->create();
$templatePaths = $renderingContext->getTemplatePaths();
$templatePaths->setTemplateRootPaths($this->configuration['view']['templateRootPaths']);
$templatePaths->setLayoutRootPaths($this->configuration['view']['layoutRootPaths']);
$templatePaths->setPartialRootPaths($this->configuration['view']['partialRootPaths']);
$templatePaths->setFormat('xml');
$this->view = GeneralUtility::makeInstance(TemplateView::class, $renderingContext);
$this->view->assign('settings', $this->getSettings());
}
public function render(string $_, array $typoScriptConfiguration, ServerRequestInterface $request): string public function render(string $_, array $typoScriptConfiguration, ServerRequestInterface $request): string
{ {
$this->typoScriptConfiguration = $typoScriptConfiguration; $this->typoScriptConfiguration = $typoScriptConfiguration;
return parent::render($_, $typoScriptConfiguration, $request); $this->initialize($GLOBALS['TSFE']->tmpl->setup);
$this->view->assign('type', $GLOBALS['TSFE']->type);
$sitemapType = $typoScriptConfiguration['sitemapType'] ?? 'xmlSitemap';
if (!empty($sitemap = ($request->getQueryParams()['sitemap'] ?? null))) {
return $this->renderSitemap($request, $sitemap, $sitemapType);
}
return $this->renderIndex($request, $sitemapType);
} }
protected function initialize(array $fullConfiguration): void protected function renderIndex(ServerRequestInterface $request, string $sitemapType): string
{ {
parent::initialize($fullConfiguration); $sitemaps = [];
$this->view->assign('settings', $this->getSettings()); foreach ($this->configuration['config'][$sitemapType]['sitemaps'] ?? [] as $sitemap => $config) {
if (!empty($config['provider']) && is_string($config['provider'])
&& class_exists($config['provider'])
&& is_subclass_of($config['provider'], XmlSitemapDataProviderInterface::class)
) {
/** @var XmlSitemapDataProviderInterface $provider */
$provider = GeneralUtility::makeInstance(
$config['provider'],
$request,
$sitemap,
(array)($config['config'] ?? [])
);
$pages = $provider->getNumberOfPages();
for ($page = 0; $page < $pages; $page++) {
$sitemaps[] = [
'key' => $sitemap,
'page' => $page,
'lastMod' => $provider->getLastModified(),
];
}
}
}
$this->view->assign('sitemapType', $sitemapType);
$this->view->assign('sitemaps', $sitemaps);
return $this->view->render('Index');
}
protected function renderSitemap(ServerRequestInterface $request, string $sitemap, string $sitemapType): string
{
if (!empty($sitemapConfig = $this->configuration['config'][$sitemapType]['sitemaps'][$sitemap] ?? null)) {
if (class_exists($sitemapConfig['provider']) &&
is_subclass_of($sitemapConfig['provider'], XmlSitemapDataProviderInterface::class)) {
/** @var XmlSitemapDataProviderInterface $provider */
$provider = GeneralUtility::makeInstance(
$sitemapConfig['provider'],
$request,
$sitemap,
(array)($sitemapConfig['config'] ?? [])
);
$items = $provider->getItems();
$this->view->assign('items', $items);
$this->view->assign('sitemapType', $sitemapType);
$template = ($sitemapConfig['config']['template'] ?? false) ?: 'Sitemap';
return $this->view->render($template);
}
throw new InvalidConfigurationException('No valid provider set for ' . $sitemap, 1535578522);
}
throw new PropagateResponseException(
GeneralUtility::makeInstance(ErrorController::class)->pageNotFoundAction(
$request,
'No valid configuration found for sitemap ' . $sitemap
),
1535578569
);
} }
private function getSettings(): array private function getSettings(): array
@ -56,7 +156,7 @@ class XmlSitemapRenderer extends Typo3XmlSitemapRenderer
if (!is_string($variableName) || substr($variableName, -1) === '.') { if (!is_string($variableName) || substr($variableName, -1) === '.') {
continue; continue;
} }
$settings[$variableName] = $this->cObj->cObjGetSingle( $settings[$variableName] = $this->contentObjectRenderer->cObjGetSingle(
$this->typoScriptConfiguration['userFunc.']['variables.'][$variableName] ?? '', $this->typoScriptConfiguration['userFunc.']['variables.'][$variableName] ?? '',
$this->typoScriptConfiguration['userFunc.']['variables.'][$variableName . '.'] ?? [] $this->typoScriptConfiguration['userFunc.']['variables.'][$variableName . '.'] ?? []
); );

View file

@ -35,13 +35,18 @@ class CodeHighlighting
/** /**
* @var ContentObjectRenderer * @var ContentObjectRenderer
*/ */
public $cObj; private $contentObjectRenderer;
public function setContentObjectRenderer(ContentObjectRenderer $contentObjectRenderer): void
{
$this->contentObjectRenderer = $contentObjectRenderer;
}
public function preTag(string $content, array $config): string public function preTag(string $content, array $config): string
{ {
$highlighter = new Highlighter(); $highlighter = new Highlighter();
$highlighter->setClassPrefix(''); $highlighter->setClassPrefix('');
$code = $this->cObj->data[$this->cObj->currentValKey]; $code = $this->contentObjectRenderer->data[$this->contentObjectRenderer->currentValKey];
$code = htmlspecialchars_decode(trim($code)); $code = htmlspecialchars_decode(trim($code));
try { try {

View file

@ -1,76 +1,69 @@
# Load default processing options # Load default processing options
imports: imports:
- {resource: "EXT:rte_ckeditor/Configuration/RTE/Processing.yaml"} - {resource: "EXT:rte_ckeditor/Configuration/RTE/Processing.yaml"}
- {resource: "EXT:rte_ckeditor/Configuration/RTE/Editor/Base.yaml"} - {resource: "EXT:rte_ckeditor/Configuration/RTE/Editor/Base.yaml"}
editor: editor:
config: config:
contentsCss: "EXT:ds_site/Resources/Public/Css/index.css"
height: 500
format_tags: "p;pre"
# Ensure that special characters are not converted to HTML entities # Ensure that special characters are not converted to HTML entities
entities_latin: false style:
entities: false definitions:
stylesSet: - {name: "Inline Code", element: "code", classes: ['']}
- {name: "Inline code", element: "code"}
toolbarGroups: heading:
# - {name: insert} options:
- {name: basicstyles, groups: [basicstyles, align, cleanup]} - {model: 'paragraph', title: 'Paragraph'}
- {name: paragraph, groups: [list, indent, blocks, align, bidi]} - {model: 'formatted', view: 'pre', title: 'Code Block'}
- {name: links}
- {name: editing, groups: [find, selection, spellchecker]}
- {name: specialcharacters, groups: [insertcharacters]}
- "/"
- {name: styles}
- "/"
- {name: tools}
- {name: document, groups: [mode, document, doctools]}
# - {name: 'document', items: ['Source']} toolbar:
# - {name: 'clipboard', items: ['Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', 'Undo', 'Redo']} items:
# - '/' - bold
# - {name: 'basicstyles', items: ['Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript', 'CopyFormatting', 'RemoveFormat']} - italic
# - {name: 'paragraph', items: ['NumberedList', 'BulletedList', 'Outdent', 'Indent', 'Blockquote']} - underline
# - {name: 'links', items: ['Link', 'Unlink', 'Anchor']} - strikethrough
# - {name: 'insert', items: ['HorizontalRule', 'SpecialChar']} - subscript
# - '/' - superscript
# - {name: 'styles', items: ['Styles', 'Format']} - '|'
# - {name: 'tools', items: ['Maximize', 'ShowBlocks']} - removeFormat
# - {name: 'about', items: ['About']} - '|'
- bulletedList
- numberedList
- '|'
- indent
- outdent
- '|'
- blockQuote
- '|'
- link
- '-'
- style
- heading
- '-'
- sourceEditing
externalPlugins: importModules:
typo3link: { resource: "EXT:rte_ckeditor/Resources/Public/JavaScript/Plugins/typo3link.js", route: "rteckeditor_wizard_browse_links" } - '@typo3/rte-ckeditor/plugin/typo3-link.js'
showbrokenlinks: { resource: "EXT:rte_ckeditor/Resources/Public/JavaScript/Plugins/showbrokenlinks/plugin.js"}
# This is a plugin, found here: https://github.com/ufdada/quicktable externalPlugins:
# quicktable: { resource: "EXT:rte_ckeditor/Resources/Public/JavaScript/Plugins/quicktable/plugin.js" } typo3link: {route: "rteckeditor_wizard_browse_links"}
autolinking: { resource: "EXT:rte_ckeditor/Resources/Public/JavaScript/Plugins/autolinking.js" }
# softhyphen plugin for adding ctrl+dash support to insert a conditional word break
# softhyphen:
# resource: "EXT:rte_ckeditor/Resources/Public/JavaScript/Plugins/softhyphen/"
# enableShortcut: true
processing: processing:
allowAttributes: [] allowAttributes: []
HTMLparser_db: HTMLparser_db:
tags: tags:
ol: ol:
allowedAttribs: [] allowedAttribs: []
ul: ul:
allowedAttribs: [] allowedAttribs: []
code: code:
allowedAttribs: [] allowedAttribs: []
pre: pre:
allowedAttribs: [] allowedAttribs: []
allowTagsOutside: allowTagsOutside:
- pre - pre
- blockquote - blockquote
- ul - ul
- ol - ol
- li - li
- br - br
# blockElementList:
# - P
# - PRE
# - BLOCKQUOTE

View file

@ -72,3 +72,8 @@ services:
identifier: 'AddFurtherMetadataToFile' identifier: 'AddFurtherMetadataToFile'
event: TYPO3\CMS\Core\Resource\Event\EnrichFileMetaDataEvent event: TYPO3\CMS\Core\Resource\Event\EnrichFileMetaDataEvent
after: 'languageAndWorkspaceOverlay' after: 'languageAndWorkspaceOverlay'
DanielSiepmann\DsSite\EventListener\PageLayoutHeader:
tags:
- name: event.listener
event: TYPO3\CMS\Backend\Controller\Event\ModifyPageLayoutContentEvent

View file

@ -19,8 +19,7 @@
], ],
'url' => [ 'url' => [
'config' => [ 'config' => [
'renderType' => 'inputLink', 'type' => 'link',
'softref' => 'typolink',
], ],
], ],
'lastUpdated' => [ 'lastUpdated' => [

View file

@ -15,13 +15,11 @@
], ],
'poster' => [ 'poster' => [
'label' => $languagePath . 'poster', 'label' => $languagePath . 'poster',
'config' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig( 'config' => [
'poster', 'type' => 'file',
[ 'maxitems' => 1,
'maxitems' => 1, 'allowed' => $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'],
], ],
$GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']
),
], ],
], ],
]); ]);

View file

@ -28,7 +28,8 @@
'columns' => [ 'columns' => [
'header' => [ 'header' => [
'config' => [ 'config' => [
'eval' => 'required, trim', 'eval' => 'trim',
'required' => true,
], ],
], ],
'CType' => [ 'CType' => [

View file

@ -1,3 +1,5 @@
@import "../Frontend/variables";
.typo3-login { .typo3-login {
.panel, .panel,
.panel-footer { .panel-footer {
@ -31,3 +33,23 @@
max-width: 100%; max-width: 100%;
} }
} }
.ck-content {
padding: {
top: $spacer * 2;
bottom: $spacer * 2;
}
pre {
background: $black-dark;
color: $white-dark;
padding: 0.5em;
}
}
.ck.ck-content code {
background: $black-dark;
color: $white-dark;
}
.ck-editor__editable_inline {
height: 500px;
}

View file

@ -1,3 +1,5 @@
$spacer: 1rem !default;
$black: #5D5D5D; $black: #5D5D5D;
$black-dark: #000000; $black-dark: #000000;
$red: #E09690; $red: #E09690;

View file

@ -1,4 +1,4 @@
body.cke_editable { .ck-content {
padding: { padding: {
top: $spacer * 2; top: $spacer * 2;
bottom: $spacer * 2; bottom: $spacer * 2;

View file

@ -13,6 +13,5 @@
@import "components/codeHighlighting"; @import "components/codeHighlighting";
@import "components/adminpanel"; @import "components/adminpanel";
@import "components/ckeditor";
@import "components/admonition"; @import "components/admonition";
@import "components/video"; @import "components/video";

View file

@ -5,14 +5,14 @@
<f:then> <f:then>
<title>Daniel Siepmann - Coding is Art - Blog Posts {settings.categoryTitle}</title> <title>Daniel Siepmann - Coding is Art - Blog Posts {settings.categoryTitle}</title>
<description>List of {settings.categoryTitle} blog posts at daniel-siepmann.de</description> <description>List of {settings.categoryTitle} blog posts at daniel-siepmann.de</description>
<link>{f:uri.page(pageUid: 11, additionalParams: {topic_uid: settings.categoryId}, absolute: 1)}</link> <link>{f:uri.typolink(parameter: 't3://page?uid=11', additionalParams: '&topic_uid={settings.categoryId}', absolute: 1)}</link>
<atom:link href="{f:uri.page(pageUid: 1. pageType: 1533906435, additionalParams: {sitemap: 'blog-posts', category_uid: settings.categoryId}, absolute: 1)}" rel="self" type="application/rss+xml" /> <atom:link href="{f:uri.typolink(parameter: 't3://page?uid=1&type=1533906435', additionalParams: '&sitemap=blog-posts&category_uid={settings.categoryId}', absolute: 1)}" rel="self" type="application/rss+xml" />
</f:then> </f:then>
<f:else> <f:else>
<title>Daniel Siepmann - Coding is Art - All Blog Posts</title> <title>Daniel Siepmann - Coding is Art - All Blog Posts</title>
<description>List of blog posts at daniel-siepmann.de</description> <description>List of blog posts at daniel-siepmann.de</description>
<link>{f:uri.page(pageUid: 1, absolute: 1)}</link> <link>{f:uri.typolink(parameter: 't3://page?uid=1', absolute: 1)}</link>
<atom:link href="{f:uri.page(pageUid: 1. pageType: 1533906435, additionalParams: {sitemap: 'blog-posts'}, absolute: 1)}" rel="self" type="application/rss+xml" /> <atom:link href="{f:uri.typolink(parameter: 't3://page?uid=1&type=1533906435', additionalParams: '&sitemap=blog-posts', absolute: 1)}" rel="self" type="application/rss+xml" />
</f:else> </f:else>
</f:if> </f:if>
<lastBuildDate>{f:format.date(date: 'now', format: 'D, d M Y H:i:s O')}</lastBuildDate> <lastBuildDate>{f:format.date(date: 'now', format: 'D, d M Y H:i:s O')}</lastBuildDate>
@ -32,8 +32,8 @@
<item> <item>
<title>{item.title}</title> <title>{item.title}</title>
<description>{item.abstract}</description> <description>{item.abstract}</description>
<link>{f:uri.page(pageUid: item.uid, absolute: 1)}</link> <link>{f:uri.typolink(parameter: 't3://page?uid={item.uid}', absolute: 1)}</link>
<pubDate>{f:format.date(date: item.lastUpdated, format: 'D, d M Y H:i:s O')}</pubDate> <pubDate>{f:format.date(date: item.lastUpdated, format: 'D, d M Y H:i:s O')}</pubDate>
<guid isPermaLink="true">{f:uri.page(pageUid: item.uid, absolute: 1)}</guid> <guid isPermaLink="true">{f:uri.typolink(parameter: 't3://page?uid={item.uid}', absolute: 1)}</guid>
</item> </item>
</f:section> </f:section>

View file

@ -1 +1 @@
.typo3-login .panel,.typo3-login .panel-footer{background-color:rgba(0,0,0,.2)}.typo3-login .card-login,.typo3-login input{background-color:#2e3436}.typo3-login input{color:#9cd9f0;border-color:#9cd9f0}.typo3-login .form-control:focus,.typo3-login .form-control:hover{color:#9cd9f0;border-color:#9cd9f0;background-color:#2e3436}.typo3-login h2{color:#fff}.typo3-login a{color:#218693}.exampleContent video{width:40rem;max-width:100%} .typo3-login .panel,.typo3-login .panel-footer{background-color:rgba(0,0,0,.2)}.typo3-login .card-login,.typo3-login input{background-color:#2e3436}.typo3-login input{color:#9cd9f0;border-color:#9cd9f0}.typo3-login .form-control:focus,.typo3-login .form-control:hover{color:#9cd9f0;border-color:#9cd9f0;background-color:#2e3436}.typo3-login h2{color:#fff}.typo3-login a{color:#218693}.exampleContent video{width:40rem;max-width:100%}.ck-content{padding-top:2rem;padding-bottom:2rem}.ck-content pre{padding:.5em}.ck-content pre,.ck.ck-content code{background:#000;color:#b0b0b0}.ck-editor__editable_inline{height:500px}

View file

@ -7,13 +7,6 @@
'default' => 'EXT:ds_site/Configuration/RTE/Default.yaml', 'default' => 'EXT:ds_site/Configuration/RTE/Default.yaml',
], ],
], ],
'SC_OPTIONS' => [
'cms/layout/db_layout.php' => [
'drawHeaderHook' => [
$extKey => \DanielSiepmann\DsSite\Hooks\Backend\PageLayoutHeader::class . '->render',
],
],
],
'SYS' => [ 'SYS' => [
'fluid' => [ 'fluid' => [
'namespaces' => [ 'namespaces' => [