diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 843787b..cc31191 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,8 +9,8 @@ jobs: strategy: fail-fast: false matrix: - php: [ 7.4, 8.0, 8.1 ] - contao: [ 4.9.*, 4.13.* ] + php: [ 7.4, 8.0, 8.1, 8.2, 8.3 ] + contao: [ 4.13.* ] steps: - name: Setup PHP diff --git a/.gitignore b/.gitignore index a041232..6c827af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ composer.lock .idea -vendor +/vendor .phpunit.result.cache .ddev build diff --git a/composer.json b/composer.json index 5dbb3f6..f4cdd0c 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "license": "LGPL-3.0-or-later", "require": { "php": "^7.4 || ^8.0", - "contao/core-bundle": "^4.9", + "contao/core-bundle": "^4.13", "ext-json": "*", "symfony/cache": "^4.4||^5.4", "symfony/config": "^4.4||^5.4", diff --git a/src/Filesystem/TwigTemplateLocator.php b/src/Filesystem/TwigTemplateLocator.php index 112aedc..6e1082e 100644 --- a/src/Filesystem/TwigTemplateLocator.php +++ b/src/Filesystem/TwigTemplateLocator.php @@ -9,24 +9,23 @@ namespace HeimrichHannot\TwigSupportBundle\Filesystem; use Contao\CoreBundle\Config\ResourceFinderInterface; -use Contao\CoreBundle\ContaoCoreBundle; use Contao\CoreBundle\Framework\ContaoFramework; use Contao\CoreBundle\Routing\ScopeMatcher; +use Contao\CoreBundle\Twig\Loader\TemplateLocator; use Contao\PageModel; use Contao\ThemeModel; use Contao\Validator; use HeimrichHannot\TwigSupportBundle\Cache\TemplateCache; use HeimrichHannot\TwigSupportBundle\Exception\TemplateNotFoundException; use Symfony\Component\Cache\Adapter\FilesystemAdapter; +use Symfony\Component\Filesystem\Path; use Symfony\Component\Finder\Exception\DirectoryNotFoundException; use Symfony\Component\Finder\Finder; use Symfony\Component\Finder\SplFileInfo; use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Bundle\BundleInterface; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Stopwatch\Stopwatch; -use Webmozart\PathUtil\Path; class TwigTemplateLocator { @@ -39,8 +38,18 @@ class TwigTemplateLocator protected Stopwatch $stopwatch; protected FilesystemAdapter $templateCache; private ContaoFramework $contaoFramework; - - public function __construct(KernelInterface $kernel, ResourceFinderInterface $contaoResourceFinder, RequestStack $requestStack, ScopeMatcher $scopeMatcher, Stopwatch $stopwatch, FilesystemAdapter $templateCache, ContaoFramework $contaoFramework) + private TemplateLocator $templateLocator; + + public function __construct( + KernelInterface $kernel, + ResourceFinderInterface $contaoResourceFinder, + RequestStack $requestStack, + ScopeMatcher $scopeMatcher, + Stopwatch $stopwatch, + FilesystemAdapter $templateCache, + ContaoFramework $contaoFramework, + TemplateLocator $templateLocator + ) { $this->kernel = $kernel; $this->contaoResourceFinder = $contaoResourceFinder; @@ -49,6 +58,7 @@ public function __construct(KernelInterface $kernel, ResourceFinderInterface $co $this->stopwatch = $stopwatch; $this->templateCache = $templateCache; $this->contaoFramework = $contaoFramework; + $this->templateLocator = $templateLocator; } /** @@ -83,7 +93,7 @@ public function getTemplateContext(string $templateName, array $options = []): T /* @var PageModel $objPage */ global $objPage; - if ('' != $objPage->templateGroup) { + if ($objPage && '' != $objPage->templateGroup) { if (Validator::isInsecurePath($objPage->templateGroup)) { throw new \RuntimeException('Invalid path '.$objPage->templateGroup); } @@ -107,7 +117,7 @@ public function getTemplateContext(string $templateName, array $options = []): T $pathLength = \strlen($themeFolder); foreach ($template['paths'] as $path) { - if ($themeFolder === substr($path, 0, $pathLength)) { + if (str_starts_with($path, '@Contao_Theme_'.$themeFolder)) { return new TemplateContext($templateName, $path, $template['pathInfo'][$path]); } } @@ -119,9 +129,15 @@ public function getTemplateContext(string $templateName, array $options = []): T } } - $path = end($template['paths']); + $key = array_key_last($template['paths']); + while (isset($template['paths'][$key])) { + if (!str_starts_with($template['paths'][$key], '@Contao_Theme_')) { + return new TemplateContext($templateName, $template['paths'][$key], $template['pathInfo'][$template['paths'][$key]]); + } + $key--; + } - return new TemplateContext($templateName, $path, $template['pathInfo'][$path]); + throw new TemplateNotFoundException(sprintf('Unable to find template "%s".', $templateName)); } /** @@ -278,7 +294,8 @@ public function getTemplates(bool $extension = false, bool $disableCache = false $cacheItem->set($this->generateContaoTwigTemplatePaths($extension)); $this->templateCache->save($cacheItem); } - $cachedTemplates = $this->templateCache->getItem($cacheKey)->get(); + + $cachedTemplates = $cacheItem->get(); if (!\is_array($cachedTemplates)) { // clean invalid cache entry @@ -342,6 +359,9 @@ public function getTwigTemplatesInPath($dir, ?string $twigKey = null, bool $exte * - extension: (bool) Add extension to filename (array key) * * @param iterable|string $dir + * + * @deprecated Use Contao\CoreBundle\Twig\Loader\TemplateLocator::findTemplates() + * @codeCoverageIgnore */ public function getTemplatesInPath($dir, ?BundleInterface $bundle = null, array $options = []): array { @@ -407,9 +427,14 @@ public function getTemplatesInPath($dir, ?BundleInterface $bundle = null, array */ protected function generateContaoTwigTemplatePaths(bool $extension = false): array { + $stopwatchname = 'TwigTemplateLocator::generateContaoTwigTemplatePaths()'; + $this->stopwatch->start($stopwatchname); + + $contaoResourcePaths = $this->templateLocator->findResourcesPaths(); + $contaoThemePaths = $this->templateLocator->findThemeDirectories(); $bundles = $this->kernel->getBundles(); - $twigFiles = []; + $resourcePaths = []; if (\is_array($bundles)) { foreach ($bundles as $key => $bundle) { $path = $bundle->getPath(); @@ -419,37 +444,95 @@ protected function generateContaoTwigTemplatePaths(bool $extension = false): arr continue; } - $twigFiles = array_merge_recursive($twigFiles, $this->getTemplatesInPath($dir, $bundle, ['extension' => $extension])); + $resourcePaths[$key][] = $dir; + } + if (isset($contaoResourcePaths[$key])) { + $resourcePaths[$key] = array_merge(($resourcePaths[$key] ?? []), $contaoResourcePaths[$key]); } } } - $bundle = null; - if (version_compare(\VERSION, '4.12', '>=')) { - $bundle = new class extends Bundle { - public function __construct() - { - $this->name = 'Contao'; - } - }; + if (isset($contaoResourcePaths['App'])) { + $resourcePaths['App'] = $contaoResourcePaths['App']; + if (!in_array($this->kernel->getProjectDir().'/templates', $resourcePaths['App'])) { + $resourcePaths['App'][] = $this->kernel->getProjectDir().'/templates'; + } } - // Bundle template folders - $twigFiles = array_merge_recursive($twigFiles, $this->getTemplatesInPath( - $this->contaoResourceFinder->findIn('templates')->name('*.twig')->getIterator(), - $bundle, - ['extension' => $extension])); - - // Project template folders - $twigFiles = array_merge_recursive( - $twigFiles, - $this->getTemplatesInPath($this->kernel->getProjectDir().'/contao/templates', $bundle, ['extension' => $extension]) - ); - $twigFiles = array_merge_recursive( - $twigFiles, - $this->getTemplatesInPath($this->kernel->getProjectDir().'/templates', null, ['extension' => $extension]) - ); + $twigFiles = []; + foreach ($resourcePaths as $bundle => $paths) { + foreach ($paths as $path) { + $path = Path::canonicalize($path); + $templates = $this->templateLocator->findTemplates($path); + if (empty($templates)) { + continue; + } + + if ('App' === $bundle) { + if (str_contains($path, '/contao/templates')) { + $namespace = 'Contao_App'; + } else { + $namespace = ''; + } + } else { + if (str_contains($path, '/contao/templates')) { + $namespace = 'Contao_'.$bundle; + } else { + $namespace = preg_replace('/Bundle$/', '', $bundle); + } + } + + foreach ($templates as $name => $templatePath) { + if (str_ends_with($name, '.html5')) { + continue; + } + + $prefix = $namespace; + + if (empty($namespace) && str_contains($name, '/')) { + $parts = explode('/', $name); + if (isset($contaoThemePaths[$parts[0]]) + && (Path::getLongestCommonBasePath($contaoThemePaths[$parts[0]], $templatePath)) === $contaoThemePaths[$parts[0]]) { + $prefix = 'Contao_Theme_'.$parts[0]; + $name = Path::makeRelative($templatePath, $contaoThemePaths[$parts[0]]); + } + } + + $twigPath = ($prefix ? "@$prefix/" : '').$name; + + if (!$extension) { + if (str_ends_with($name, '.html.twig')) { + $name = substr($name, 0, -10); + } + } + + $this->addPath($twigFiles, $name, $twigPath, $bundle, $path); + + // check for modern contao template paths and legacy fallback + if (!str_contains($name, '/')) { + continue; + } + + $file = new \SplFileInfo($templatePath); + $name = $file->getBasename(); + if (str_ends_with($name, '.html.twig')) { + $name = substr($name, 0, -10); + } + $this->addPath($twigFiles, $name, $twigPath, $bundle, $path, true); + } + } + } + + $this->stopwatch->stop($stopwatchname); return $twigFiles; } + + private function addPath(array &$pathData, string $name, string $twigPath, ?string $bundleName, string $absolutePath, bool $deprecated = false): void + { + $pathData[$name]['paths'][] = $twigPath; + $pathData[$name]['pathInfo'][$twigPath]['bundle'] = $bundleName; + $pathData[$name]['pathInfo'][$twigPath]['pathname'] = $absolutePath; + $pathData[$name]['pathInfo'][$twigPath]['deprecatedPath'] = $deprecated; + } } diff --git a/tests/Filesystem/TwigTemplateLocatorTest.php b/tests/Filesystem/TwigTemplateLocatorTest.php index f733c98..7d6c57d 100644 --- a/tests/Filesystem/TwigTemplateLocatorTest.php +++ b/tests/Filesystem/TwigTemplateLocatorTest.php @@ -12,11 +12,15 @@ use Contao\CoreBundle\Config\ResourceFinderInterface; use Contao\CoreBundle\ContaoCoreBundle; use Contao\CoreBundle\Routing\ScopeMatcher; +use Contao\CoreBundle\Twig\Loader\TemplateLocator; +use Contao\CoreBundle\Twig\Loader\ThemeNamespace; use Contao\TestCase\ContaoTestCase; use Contao\ThemeModel; +use Doctrine\DBAL\Connection; use HeimrichHannot\TestUtilitiesBundle\Mock\ModelMockTrait; use HeimrichHannot\TwigSupportBundle\Filesystem\TwigTemplateLocator; use PHPUnit\Framework\MockObject\MockBuilder; +use Psr\Cache\CacheItemInterface; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; @@ -68,6 +72,10 @@ public function createTestInstance(array $parameter = [], ?MockBuilder $mockBuil ThemeModel::class => $this->mockAdapter(['findAll']), ]); + if (!isset($parameter['locator'])) { + $parameter['locator'] = $this->createMock(TemplateLocator::class); + } + if ($mockBuilder) { return $mockBuilder->setConstructorArgs([ $parameter['kernel'], @@ -77,9 +85,11 @@ public function createTestInstance(array $parameter = [], ?MockBuilder $mockBuil $this->createMock(Stopwatch::class), $parameter['cache'], $contaoFramework, + $parameter['locator'], ])->getMock(); } + return new TwigTemplateLocator( $parameter['kernel'], $parameter['resource_finder'], @@ -87,7 +97,8 @@ public function createTestInstance(array $parameter = [], ?MockBuilder $mockBuil $parameter['scope_matcher'], $this->createMock(Stopwatch::class), $parameter['cache'], - $contaoFramework + $contaoFramework, + $parameter['locator'] ); } @@ -181,66 +192,97 @@ public function testGetTemplateGroup() ], $instance->getTemplateGroup('prefix')); } - public function testGenerateContaoTwigTemplatePathsEmpty() + public function testGetTemplatePath() { - $kernel = $this->createMock(Kernel::class); - $kernel->method('getBundles')->willReturn([]); - $kernel->method('getProjectDir')->willReturn(__DIR__.'/../Fixtures/templateLocator/empty'); + $instance = $this->createTestInstance($this->prepareTemplateLoader([])); + $this->assertSame('@Contao_App/content_element/text.html.twig', $instance->getTemplatePath('text', ['disableCache' => true])); + $this->assertSame('@Contao_App/content_element/text.html.twig', $instance->getTemplatePath('content_element/text', ['disableCache' => true])); + $this->assertSame('@Contao_App/form_text.html.twig', $instance->getTemplatePath('form_text', ['disableCache' => true])); + $this->assertSame('ce_text.html.twig', $instance->getTemplatePath('ce_text', ['disableCache' => true])); - $resourceFinder = $this->getMockBuilder(ResourceFinderInterface::class)->setMethods(['find', 'findIn', 'name', 'getIterator'])->getMock(); - $resourceFinder->method('findIn')->willReturnSelf(); - $resourceFinder->method('name')->willReturnSelf(); - $resourceFinder->method('getIterator')->willReturn([]); + $parameters = $this->prepareTemplateLoader([]); + $scopeMather = $this->createMock(ScopeMatcher::class); + $scopeMather->method('isFrontendRequest')->willReturn(true); + $parameters['scope_matcher'] = $scopeMather; + $instance = $this->createTestInstance($parameters); - $instance = $this->createTestInstance([ - 'kernel' => $kernel, - 'resource_finder' => $resourceFinder, - ]); - $this->assertEmpty($instance->getTemplates(false, true)); + $this->assertSame('ce_text.html.twig', $instance->getTemplatePath('ce_text', ['disableCache' => true])); + $this->assertSame('@Contao_App/ce_headline.html.twig', $instance->getTemplatePath('ce_headline', ['disableCache' => true])); + $this->assertSame('@Contao_a/ce_html.html.twig', $instance->getTemplatePath('ce_html', ['disableCache' => true])); + + $GLOBALS['objPage'] = (object) ['templateGroup' => 'customtheme']; + $this->assertSame('@Contao_Theme_customtheme/ce_text.html.twig', $instance->getTemplatePath('ce_text', ['disableCache' => true])); + $this->assertSame('@Contao_Theme_customtheme/ce_headline.html.twig', $instance->getTemplatePath('ce_headline', ['disableCache' => true])); + $this->assertSame('@Contao_a/ce_html.html.twig', $instance->getTemplatePath('ce_html', ['disableCache' => true])); + + $GLOBALS['objPage'] = (object) ['templateGroup' => 'anothertheme']; + $this->assertSame('@Contao_Theme_anothertheme/ce_text.html.twig', $instance->getTemplatePath('ce_text', ['disableCache' => true])); + $this->assertSame('@Contao_App/ce_headline.html.twig', $instance->getTemplatePath('ce_headline', ['disableCache' => true])); + $this->assertSame('@Contao_Theme_anothertheme/ce_html.html.twig', $instance->getTemplatePath('ce_html', ['disableCache' => true])); + + unset($GLOBALS['objPage']); } - public function testGenerateContaoTwigTemplatePathsBundles() + public function testGetTemplates() { - [$kernel, $resourceFinder] = $this->buildKernelAndResourceFinderForBundlesAndPath(['dolarBundle', 'ipsumBundle'], 'bundles'); + $mock = $this->getMockBuilder(TwigTemplateLocator::class)->onlyMethods(['generateContaoTwigTemplatePaths']); + $parameters = $this->prepareTemplateLoader([]); - $instance = $this->createTestInstance([ - 'kernel' => $kernel, - 'resource_finder' => $resourceFinder, - ]); - $templates = $instance->getTemplates(false, true); - $this->assertNotEmpty($templates); - $this->assertArrayHasKey('ce_text', $templates); + $cacheItem = $this->createMock(CacheItemInterface::class); + $cacheItem->method('isHit')->willReturnOnConsecutiveCalls(false, true); + $cacheItem->method('get')->willReturn([]); + + $cache = $this->createMock(FilesystemAdapter::class); + $cache->method('getItem')->willReturn($cacheItem); + $parameters['cache'] = $cache; - $templates = $instance->getTemplates(true, true); - $this->assertNotEmpty($templates); - $this->assertArrayHasKey('ce_text.html.twig', $templates); + $instance = $this->createTestInstance($parameters, $mock); + $instance->expects($this->once())->method('generateContaoTwigTemplatePaths')->willReturn([]); + + $instance->getTemplates(); + $instance->getTemplates(); } - public function testGetTemplatePath() + private function prepareTemplateLoader(array $parameters): array { - [$kernel, $resourceFinder] = $this->buildKernelAndResourceFinderForBundlesAndPath(['dolarBundle', 'ipsumBundle'], 'bundles'); - $scopeMather = $this->createMock(ScopeMatcher::class); - $scopeMather->method('isFrontendRequest')->willReturn(false); + $projectDir = __DIR__.'/../Fixtures/TwigTemplateLocator'; + $kernel = $this->createMock(Kernel::class); + $bundles = []; + $bundleMetaData = []; + foreach (['a', 'b'] as $bundle) { + $currentBundle = $this->createMock(BundleInterface::class); + $bundlePath = $projectDir.'/vendor/example/'.$bundle; + $currentBundle->method('getPath')->willReturn($bundlePath); + $currentBundle->method('getName')->willReturn($bundle); + $kernelBundles[$bundle] = $currentBundle; + $bundles[$bundle] = BundleInterface::class; + $bundleMetaData[$bundle] = ['path' => $bundlePath]; + } + + $kernel->method('getBundles')->willReturn($kernelBundles); + $kernel->method('getProjectDir')->willReturn($projectDir); - $instance = $this->createTestInstance([ - 'kernel' => $kernel, - 'resource_finder' => $resourceFinder, - 'scope_matcher' => $scopeMather, + $connection = $this->createMock(Connection::class); + $connection->method('fetchFirstColumn')->willReturn([ + 'templates/customtheme', + 'templates/anothertheme', ]); - $this->assertSame('@ipsum/ce_text.html.twig', $instance->getTemplatePath('ce_text', ['disableCache' => true])); - [$kernel, $resourceFinder] = $this->buildKernelAndResourceFinderForBundlesAndPath(['dolarBundle', 'ipsumBundle'], 'mixed'); - $scopeMather = $this->createMock(ScopeMatcher::class); - $scopeMather->method('isFrontendRequest')->willReturn(false); + $templateLocator = new TemplateLocator( + $projectDir, + $bundles, + $bundleMetaData, + new ThemeNamespace(), + $connection + ); - $instance = $this->createTestInstance([ - 'kernel' => $kernel, - 'resource_finder' => $resourceFinder, - 'scope_matcher' => $scopeMather, - ]); - $this->assertSame('ce_text.html.twig', $instance->getTemplatePath('ce_text', ['disableCache' => true])); + $parameters['kernel'] = $kernel; + $parameters['locator'] = $templateLocator; + + return $parameters; } + protected function buildKernelAndResourceFinderForBundlesAndPath(array $bundles, string $subpath) { $kernel = $this->createMock(Kernel::class); @@ -264,6 +306,13 @@ protected function buildKernelAndResourceFinderForBundlesAndPath(array $bundles, $resourceFinder = new ResourceFinder($resourcePaths); - return [$kernel, $resourceFinder]; + + $bundleMetaData = []; + foreach ($kernelBundles as $bundle) { + $bundleMetaData[$bundle->getName()] = ['path' => $bundle->getPath()]; + } + $templateLocator = new TemplateLocator(__DIR__.'/../Fixtures/templateLocator/'.$subpath, [], [], new ThemeNamespace(), $this->createMock(Connection::class)); + + return [$kernel, $resourceFinder, $templateLocator]; } } diff --git a/tests/Fixtures/TwigTemplateLocator/contao/templates/element/ce_headline.html.twig b/tests/Fixtures/TwigTemplateLocator/contao/templates/element/ce_headline.html.twig new file mode 100644 index 0000000..e69de29 diff --git a/tests/Fixtures/TwigTemplateLocator/contao/templates/twig/.twig-root b/tests/Fixtures/TwigTemplateLocator/contao/templates/twig/.twig-root new file mode 100644 index 0000000..e69de29 diff --git a/tests/Fixtures/TwigTemplateLocator/contao/templates/twig/content_element/text.html.twig b/tests/Fixtures/TwigTemplateLocator/contao/templates/twig/content_element/text.html.twig new file mode 100644 index 0000000..e69de29 diff --git a/tests/Fixtures/TwigTemplateLocator/contao/templates/widget/form_captcha.html5 b/tests/Fixtures/TwigTemplateLocator/contao/templates/widget/form_captcha.html5 new file mode 100644 index 0000000..e69de29 diff --git a/tests/Fixtures/TwigTemplateLocator/contao/templates/widget/form_text.html.twig b/tests/Fixtures/TwigTemplateLocator/contao/templates/widget/form_text.html.twig new file mode 100644 index 0000000..e69de29 diff --git a/tests/Fixtures/TwigTemplateLocator/templates/anothertheme/ce_html.html.twig b/tests/Fixtures/TwigTemplateLocator/templates/anothertheme/ce_html.html.twig new file mode 100644 index 0000000..e69de29 diff --git a/tests/Fixtures/TwigTemplateLocator/templates/anothertheme/ce_text.html.twig b/tests/Fixtures/TwigTemplateLocator/templates/anothertheme/ce_text.html.twig new file mode 100644 index 0000000..e69de29 diff --git a/tests/Fixtures/TwigTemplateLocator/templates/ce_text.html.twig b/tests/Fixtures/TwigTemplateLocator/templates/ce_text.html.twig new file mode 100644 index 0000000..e69de29 diff --git a/tests/Fixtures/TwigTemplateLocator/templates/customtheme/ce_headline.html.twig b/tests/Fixtures/TwigTemplateLocator/templates/customtheme/ce_headline.html.twig new file mode 100644 index 0000000..e69de29 diff --git a/tests/Fixtures/TwigTemplateLocator/templates/customtheme/ce_text.html.twig b/tests/Fixtures/TwigTemplateLocator/templates/customtheme/ce_text.html.twig new file mode 100644 index 0000000..e69de29 diff --git a/tests/Fixtures/TwigTemplateLocator/vendor/example/a/contao/templates/ce_html.html.twig b/tests/Fixtures/TwigTemplateLocator/vendor/example/a/contao/templates/ce_html.html.twig new file mode 100644 index 0000000..e69de29 diff --git a/tests/Fixtures/TwigTemplateLocator/vendor/example/b/src/Resources/views/ce_headline.html.twig b/tests/Fixtures/TwigTemplateLocator/vendor/example/b/src/Resources/views/ce_headline.html.twig new file mode 100644 index 0000000..e69de29 diff --git a/tests/Fixtures/templateLocator/project/contao/templates/twig/.twig-root b/tests/Fixtures/templateLocator/project/contao/templates/twig/.twig-root new file mode 100644 index 0000000..e69de29 diff --git a/tests/Fixtures/templateLocator/project/contao/templates/twig/content_element/text.html.twig b/tests/Fixtures/templateLocator/project/contao/templates/twig/content_element/text.html.twig new file mode 100644 index 0000000..e69de29 diff --git a/tests/Fixtures/templateLocator/project/contao/templates/widget/form_captcha.html5 b/tests/Fixtures/templateLocator/project/contao/templates/widget/form_captcha.html5 new file mode 100644 index 0000000..e69de29 diff --git a/tests/Fixtures/templateLocator/project/contao/templates/widget/form_text.html.twig b/tests/Fixtures/templateLocator/project/contao/templates/widget/form_text.html.twig new file mode 100644 index 0000000..e69de29 diff --git a/tests/Fixtures/templateLocator/project/templates/ce_text.html.twig b/tests/Fixtures/templateLocator/project/templates/ce_text.html.twig new file mode 100644 index 0000000..e69de29 diff --git a/tools/rector/.gitignore b/tools/rector/.gitignore new file mode 100644 index 0000000..49ce3c1 --- /dev/null +++ b/tools/rector/.gitignore @@ -0,0 +1 @@ +/vendor \ No newline at end of file