@@ -90,9 +90,9 @@ public function __construct(CacheItemPoolInterface $cachePool, TemplateLocator $
9090// Restore hierarchy from cache
9191$ hierarchyItem =$ cachePool ->getItem (self ::CACHE_KEY_HIERARCHY );
9292
93- if ($ hierarchyItem ->isHit ()) {
94- $ this ->hierarchy =$ hierarchyItem -> get () ;
95- $ this ->hierarchyHash =$ this ->createHash ($ this -> hierarchy );
93+ if ($ hierarchyItem ->isHit () && null !== ( $ hierarchy = $ hierarchyItem -> get ()) ) {
94+ $ this ->hierarchy =$ hierarchy ;
95+ $ this ->hierarchyHash =$ this ->createHash ($ hierarchy );
9696 }
9797 }
9898
@@ -103,10 +103,14 @@ public function __construct(CacheItemPoolInterface $cachePool, TemplateLocator $
103103 * that should be available in the Contao template hierarchy.
104104 *
105105 * @param string $path A path where to look for templates
106- * @param string $namespace A path namespace
106+ * @param string $namespace A"Contao" or "Contao_*" path namespace
107107 */
108- public function addPath ($ path ,$ namespace =self :: MAIN_NAMESPACE ,bool $ trackTemplates =false ):void
108+ public function addPath ($ path ,$ namespace =' Contao ' ,bool $ trackTemplates =false ):void
109109 {
110+ if (null ===$ this ->parseName ("@ $ namespace " )) {
111+ throw new LoaderError ("Tried to register an invalid Contao namespace ' $ namespace'. " );
112+ }
113+
110114try {
111115parent ::addPath ($ path ,$ namespace );
112116 }catch (LoaderError $ error ) {
@@ -127,17 +131,26 @@ public function addPath($path, $namespace = self::MAIN_NAMESPACE, bool $trackTem
127131 * Prepends a path where templates are stored (if it exists).
128132 *
129133 * @param string $path A path where to look for templates
130- * @param string $namespace A path namespace
134+ * @param string $namespace A"Contao" or "Contao_*" path namespace
131135 */
132- public function prependPath ($ path ,$ namespace =self :: MAIN_NAMESPACE ):void
136+ public function prependPath ($ path ,$ namespace =' Contao ' ):void
133137 {
138+ if (null ===$ this ->parseName ("@ $ namespace " )) {
139+ throw new LoaderError ("Tried to register an invalid Contao namespace ' $ namespace'. " );
140+ }
141+
134142try {
135143parent ::prependPath ($ path ,$ namespace );
136144 }catch (LoaderError $ error ) {
137145// Ignore
138146 }
139147 }
140148
149+ public function getPaths ($ namespace ='Contao ' ):array
150+ {
151+ return parent ::getPaths ($ namespace );
152+ }
153+
141154/**
142155 * Clears all registered template paths.
143156 */
@@ -178,11 +191,13 @@ public function persist(): void
178191 */
179192public function getCacheKey ($ name ):string
180193 {
181- // We're basically cache busting `@Contao` and `@Contao_*` namespaced
182- // templates by appending a hash that changes whenever the registered
183- // hierarchy changes
184- $ suffix =null !==$ this ->hierarchyHash &&1 ===preg_match ('%^(@Contao(_.*)?)/% ' ,$ name ) ?
185- "_ $ this ->hierarchyHash " :'' ;
194+ // We're basically cache busting templates by appending a hash that
195+ // changes whenever the registered hierarchy changes
196+ if (null ===$ this ->hierarchyHash ) {
197+ $ this ->buildHierarchy ();
198+ }
199+
200+ $ suffix ="_ $ this ->hierarchyHash " ;
186201
187202if (null !== ($ themeTemplateName =$ this ->getThemeTemplateName ($ name ))) {
188203return parent ::getCacheKey ($ themeTemplateName ).$ suffix ;
@@ -295,9 +310,20 @@ public function buildHierarchy(): void
295310$ this ->hierarchyHash =$ this ->createHash ($ hierarchy );
296311 }
297312
298- private function createHash (array $ array ):string
313+ /**
314+ * Split a Contao name into [namespace, short name]. The short name part
315+ * will be null if $name is only a namespace.
316+ *
317+ * If parsing fails - i.e. if the given name does not describe a "Contao"
318+ * or "Contao_*" namespace - null is returned instead.
319+ */
320+ private function parseName (string $ logicalNameOrNamespace ): ?array
299321 {
300- return substr (md5 (json_encode ($ array ,JSON_THROW_ON_ERROR )),0 ,6 );
322+ if (1 ===preg_match ('%^@(Contao(?:_[A-za-z0-9]+)?)(?:/(.*))?$% ' ,$ logicalNameOrNamespace ,$ matches )) {
323+ return [$ matches [1 ],$ matches [2 ] ??null ];
324+ }
325+
326+ return null ;
301327 }
302328
303329private function getIdentifier (string $ shortName ):string
@@ -306,21 +332,26 @@ private function getIdentifier(string $shortName): string
306332return preg_replace ('/(.*)(\.html5|\.html.twig)/ ' ,'$1 ' ,$ shortName );
307333 }
308334
335+ private function createHash (array $ array ):string
336+ {
337+ return substr (md5 (json_encode ($ array ,JSON_THROW_ON_ERROR )),0 ,6 );
338+ }
339+
309340/**
310341 * Returns the template name of a theme specific variant of the given name
311342 * or null if not applicable.
312343 */
313344private function getThemeTemplateName (string $ name ): ?string
314345 {
315- if (1 !== preg_match ( ' %^@Contao/(.*)% ' , $ name , $ matches ) ) {
346+ if (null === ( $ parts = $ this -> parseName ( $ name )) || ' Contao ' !== $ parts [ 0 ] ) {
316347return null ;
317348 }
318349
319350if (false === ($ themeSlug =$ this ->currentThemeSlug ??$ this ->getThemeSlug ())) {
320351return null ;
321352 }
322353
323- $ template ="@Contao_Theme_ $ themeSlug/ $ matches [1 ]" ;
354+ $ template ="@Contao_Theme_ $ themeSlug/ $ parts [1 ]" ;
324355
325356return $ this ->exists ($ template ) ?$ template :null ;
326357 }