vendor/contao/core-bundle/src/Twig/Loader/TemplateLocator.php line 68

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of Contao.
  5. *
  6. * (c) Leo Feyer
  7. *
  8. * @license LGPL-3.0-or-later
  9. */
  10. namespace Contao\CoreBundle\Twig\Loader;
  11. use Contao\CoreBundle\Exception\InvalidThemePathException;
  12. use Contao\CoreBundle\HttpKernel\Bundle\ContaoModuleBundle;
  13. use Doctrine\DBAL\Connection;
  14. use Doctrine\DBAL\Exception\DriverException;
  15. use Symfony\Component\Filesystem\Filesystem;
  16. use Symfony\Component\Filesystem\Path;
  17. use Symfony\Component\Finder\Finder;
  18. /**
  19. * @experimental
  20. */
  21. class TemplateLocator
  22. {
  23. public const FILE_MARKER_NAMESPACE_ROOT = '.twig-root';
  24. private string $projectDir;
  25. private ThemeNamespace $themeNamespace;
  26. private Connection $connection;
  27. private Filesystem $filesystem;
  28. /**
  29. * @var array<string,string>
  30. */
  31. private array $bundles;
  32. /**
  33. * @var array<string, array<string, string>>
  34. */
  35. private array $bundlesMetadata;
  36. public function __construct(string $projectDir, array $bundles, array $bundlesMetadata, ThemeNamespace $themeNamespace, Connection $connection)
  37. {
  38. $this->projectDir = $projectDir;
  39. $this->bundles = $bundles;
  40. $this->bundlesMetadata = $bundlesMetadata;
  41. $this->themeNamespace = $themeNamespace;
  42. $this->connection = $connection;
  43. $this->filesystem = new Filesystem();
  44. }
  45. /**
  46. * @return array<string, string>
  47. */
  48. public function findThemeDirectories(): array
  49. {
  50. $directories = [];
  51. // This code might run early during cache warmup where the 'tl_theme'
  52. // table couldn't exist, yet.
  53. try {
  54. // Note: We cannot use models or other parts of the Contao
  55. // framework here because this function will be called when the
  56. // container is built (see #3567)
  57. $themePaths = $this->connection->fetchFirstColumn("SELECT templates FROM tl_theme WHERE templates != ''");
  58. } catch (DriverException $e) {
  59. return [];
  60. }
  61. foreach ($themePaths as $themePath) {
  62. if (!is_dir($absolutePath = Path::join($this->projectDir, $themePath))) {
  63. continue;
  64. }
  65. try {
  66. $slug = $this->themeNamespace->generateSlug(Path::makeRelative($themePath, 'templates'));
  67. } catch (InvalidThemePathException $e) {
  68. trigger_deprecation('contao/core-bundle', '4.12', 'Using a theme path with invalid characters has been deprecated and will throw an exception in Contao 5.0.');
  69. continue;
  70. }
  71. $directories[$slug] = $absolutePath;
  72. }
  73. return $directories;
  74. }
  75. /**
  76. * @return array<string, array<string>>
  77. */
  78. public function findResourcesPaths(): array
  79. {
  80. $paths = [];
  81. $add = function (string $group, string $basePath) use (&$paths): void {
  82. $paths[$group] = array_merge($paths[$group] ?? [], $this->expandSubdirectories($basePath));
  83. };
  84. if (is_dir($path = Path::join($this->projectDir, 'contao/templates'))) {
  85. $add('App', $path);
  86. }
  87. if (is_dir($path = Path::join($this->projectDir, 'src/Resources/contao/templates'))) {
  88. $add('App', $path);
  89. }
  90. if (is_dir($path = Path::join($this->projectDir, 'app/Resources/contao/templates'))) {
  91. $add('App', $path);
  92. }
  93. foreach (array_reverse($this->bundles) as $name => $class) {
  94. if (ContaoModuleBundle::class === $class && is_dir($path = Path::join($this->bundlesMetadata[$name]['path'], 'templates'))) {
  95. $add($name, $path);
  96. } elseif (is_dir($path = Path::join($this->bundlesMetadata[$name]['path'], 'Resources/contao/templates'))) {
  97. $add($name, $path);
  98. } elseif (is_dir($path = Path::join($this->bundlesMetadata[$name]['path'], 'contao/templates'))) {
  99. $add($name, $path);
  100. }
  101. }
  102. return $paths;
  103. }
  104. /**
  105. * @return array<string, string>
  106. */
  107. public function findTemplates(string $path): array
  108. {
  109. if (!is_dir($path)) {
  110. return [];
  111. }
  112. $finder = (new Finder())
  113. ->files()
  114. ->in($path)
  115. ->name('/(\.twig|\.html5)$/')
  116. ->sortByName()
  117. ;
  118. if (!$this->isNamespaceRoot($path)) {
  119. $finder = $finder->depth('< 1');
  120. }
  121. $templates = [];
  122. foreach ($finder as $file) {
  123. $templates[Path::normalize($file->getRelativePathname())] = Path::canonicalize($file->getPathname());
  124. }
  125. return $templates;
  126. }
  127. /**
  128. * Return a list of all sub directories in $path that are not inside a
  129. * directory containing a namespace root marker file.
  130. */
  131. private function expandSubdirectories(string $path): array
  132. {
  133. $paths = [$path];
  134. if ($this->isNamespaceRoot($path)) {
  135. return $paths;
  136. }
  137. $namespaceRoots = [];
  138. $finder = (new Finder())
  139. ->directories()
  140. ->in($path)
  141. ->sortByName()
  142. ->filter(
  143. function (\SplFileInfo $info) use (&$namespaceRoots): bool {
  144. $path = $info->getPathname();
  145. foreach ($namespaceRoots as $directory) {
  146. if (Path::isBasePath($directory, $path)) {
  147. return false;
  148. }
  149. }
  150. if ($this->isNamespaceRoot($path)) {
  151. $namespaceRoots[] = $path;
  152. }
  153. return true;
  154. }
  155. )
  156. ;
  157. foreach ($finder as $item) {
  158. $paths[] = Path::canonicalize($item->getPathname());
  159. }
  160. return $paths;
  161. }
  162. private function isNamespaceRoot(string $path): bool
  163. {
  164. // Implicitly treat the global template directory as namespace root
  165. if (Path::join($this->projectDir, 'templates') === $path) {
  166. return true;
  167. }
  168. // Require a marker file everywhere else
  169. return $this->filesystem->exists(Path::join($path, self::FILE_MARKER_NAMESPACE_ROOT));
  170. }
  171. }