Skip to content

Commit

Permalink
(Twig,Drupal) Refactor twig driver system to allow namespaces and alw…
Browse files Browse the repository at this point in the history
…ays reload templates - #89, #94
  • Loading branch information
rbayliss committed Oct 30, 2017
1 parent bffcaef commit 71a0f4f
Show file tree
Hide file tree
Showing 14 changed files with 457 additions and 249 deletions.
76 changes: 25 additions & 51 deletions src/Drupal/Driver/DrupalTwigDriver.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,16 @@
namespace LastCall\Mannequin\Drupal\Driver;

use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use LastCall\Mannequin\Drupal\Drupal\MannequinDateFormatter;
use LastCall\Mannequin\Drupal\Drupal\MannequinDrupalTwigExtension;
use LastCall\Mannequin\Drupal\Drupal\MannequinExtensionDiscovery;
use LastCall\Mannequin\Drupal\Drupal\MannequinRenderer;
use LastCall\Mannequin\Drupal\Drupal\MannequinThemeManager;
use LastCall\Mannequin\Drupal\Drupal\MannequinUrlGenerator;
use LastCall\Mannequin\Twig\Driver\SimpleTwigDriver;
use LastCall\Mannequin\Twig\Twig\Lexer;
use LastCall\Mannequin\Twig\Twig\MannequinExtension;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\NullAdapter;

/**
* Creates a Drupal-like Twig_Environment.
Expand All @@ -36,80 +32,58 @@
*/
class DrupalTwigDriver extends SimpleTwigDriver
{
private $booted;
private $drupalRoot;
private $cache;
private $twigOptions;
private $discovery;

public function __construct(string $drupalRoot, array $twigOptions = [], CacheItemPoolInterface $cache = null)
private $detectedNamespaces = null;

public function __construct(string $drupalRoot, ExtensionDiscovery $discovery, array $twigOptions = [], array $namespaces = [])
{
if (!is_dir($drupalRoot)) {
throw new \InvalidArgumentException(sprintf('Drupal root %s does not exist', $drupalRoot));
}
if (!file_exists(sprintf('%s/core/includes/bootstrap.inc', $drupalRoot))) {
throw new \InvalidArgumentException(sprintf('Directory %s does not look like a Drupal installation', $drupalRoot));
}
$this->drupalRoot = $drupalRoot;
$this->twigOptions = $twigOptions;
$this->cache = $cache ?: new NullAdapter();
}
parent::__construct($drupalRoot, $twigOptions, $namespaces);

/**
* {@inheritdoc}
*/
public function getTwigRoot(): string
{
return $this->drupalRoot;
$this->drupalRoot = $drupalRoot;
$this->discovery = $discovery;
}

/**
* {@inheritdoc}
*/
protected function createTwig(): \Twig_Environment
protected function initialize(\Twig_Environment $twig)
{
$this->boot();
$twig = new \Twig_Environment($this->createLoader(), $this->twigOptions);
parent::initialize($twig);
$extension = new MannequinDrupalTwigExtension(
$this->getRenderer(),
$this->getGenerator(),
$this->getThemeManager(),
$this->getDateFormatter()
);
$twig->addExtension($extension);
$twig->addExtension(new MannequinExtension());
$twig->setLexer(new Lexer($twig));

return $twig;
}

private function createLoader()
protected function getNamespaces(): array
{
$this->boot();
if (null === $this->detectedNamespaces) {
require_once sprintf('%s/core/includes/bootstrap.inc', $this->drupalRoot);

$loader = new \Twig_Loader_Filesystem(['./'], $this->drupalRoot);
$discovery = new MannequinExtensionDiscovery($this->drupalRoot, $this->cache);
foreach ($discovery->scan('module', false) as $key => $extension) {
$dir = sprintf('%s/templates', $extension->getPath());
if (is_dir(sprintf('%s/%s', $this->drupalRoot, $dir))) {
$loader->addPath($dir, $key);
$this->detectedNamespaces = [];
foreach ($this->discovery->scan('module', false) as $key => $extension) {
$dir = sprintf('%s/templates', $extension->getPath());
if (is_dir(sprintf('%s/%s', $this->drupalRoot, $dir))) {
$this->detectedNamespaces[$key][] = $dir;
}
}
}
foreach ($discovery->scan('theme', false) as $key => $extension) {
$dir = sprintf('%s/templates', $extension->getPath());
if (is_dir(sprintf('%s/%s', $this->drupalRoot, $dir))) {
$loader->addPath($dir, $key);
foreach ($this->discovery->scan('theme', false) as $key => $extension) {
$dir = sprintf('%s/templates', $extension->getPath());
if (is_dir(sprintf('%s/%s', $this->drupalRoot, $dir))) {
$this->detectedNamespaces[$key][] = $dir;
}
}
}

return $loader;
}

private function boot()
{
if (!$this->booted) {
$this->booted = true;
require_once sprintf('%s/core/includes/bootstrap.inc', $this->drupalRoot);
}
return parent::getNamespaces() + $this->detectedNamespaces;
}

private function getRenderer(): RendererInterface
Expand Down
8 changes: 7 additions & 1 deletion src/Drupal/DrupalExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use Drupal\Core\Template\Attribute;
use LastCall\Mannequin\Core\Mannequin;
use LastCall\Mannequin\Drupal\Driver\DrupalTwigDriver;
use LastCall\Mannequin\Drupal\Drupal\MannequinExtensionDiscovery;
use LastCall\Mannequin\Twig\AbstractTwigExtension;
use LastCall\Mannequin\Twig\Driver\TwigDriverInterface;
use Symfony\Component\ExpressionLanguage\ExpressionFunction;
Expand All @@ -27,13 +28,15 @@ class DrupalExtension extends AbstractTwigExtension implements ExpressionFunctio
private $iterator;
private $drupalRoot;
private $twigOptions;
private $twigNamespaces = [];
private $driver;

public function __construct(array $config = [])
{
$this->iterator = $config['finder'] ?: new \ArrayIterator([]);
$this->drupalRoot = $config['drupal_root'] ?? getcwd();
$this->twigOptions = $config['twig_options'] ?? [];
$this->twigNamespaces = $config['twig_namespaces'] ?? [];
}

/**
Expand Down Expand Up @@ -64,13 +67,16 @@ protected function getTemplateFilenameIterator(): \Traversable
protected function getDriver(): TwigDriverInterface
{
if (!$this->driver) {
$discovery = new MannequinExtensionDiscovery($this->drupalRoot, $this->mannequin->getCache());

if (!isset($this->twigOptions['cache'])) {
$this->twigOptions['cache'] = $this->mannequin->getCacheDir();
}
$this->driver = new DrupalTwigDriver(
$this->drupalRoot,
$discovery,
$this->twigOptions,
$this->mannequin->getCache()
$this->twigNamespaces
);
}

Expand Down
67 changes: 0 additions & 67 deletions src/Drupal/Tests/Driver/BareDrupalDriverTest.php

This file was deleted.

68 changes: 68 additions & 0 deletions src/Drupal/Tests/Driver/DrupalTwigDriverTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

/*
* This file is part of Mannequin.
*
* (c) 2017 Last Call Media, Rob Bayliss <rob@lastcallmedia.com>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace LastCall\Mannequin\Drupal\Tests\Driver;

use LastCall\Mannequin\Drupal\Driver\DrupalTwigDriver;
use LastCall\Mannequin\Drupal\Drupal\MannequinExtensionDiscovery;
use LastCall\Mannequin\Drupal\Tests\UsesTestDrupalRoot;
use LastCall\Mannequin\Twig\Driver\TwigDriverInterface;
use LastCall\Mannequin\Twig\Tests\Driver\DriverTestCase;

/**
* This test is very slow because of the extension scanning.
*/
class DrupalTwigDriverTest extends DriverTestCase
{
use UsesTestDrupalRoot;

public static function setUpBeforeClass()
{
self::requireDrupalClasses();
}

protected function getDriver(): TwigDriverInterface
{
$discovery = new MannequinExtensionDiscovery($this->getDrupalRoot());

return new DrupalTwigDriver($this->getDrupalRoot(), $discovery, [], [
'foo' => ['core/misc'],
]);
}

/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage does not look like a Drupal installation
*/
public function testCreationWithInvalidDrupalRootFails()
{
$discovery = new MannequinExtensionDiscovery(__DIR__);
new DrupalTwigDriver(__DIR__, $discovery);
}

public function testHasTemplateNameMapper()
{
$mapper = parent::testHasTemplateNameMapper();
$details = new \SplFileInfo($this->getDrupalRoot().'/core/modules/system/templates/details.html.twig');
$this->assertEquals(['@system/details.html.twig'], $mapper($details));

return $mapper;
}

/**
* @depends testHasTemplateNameMapper
*/
public function testCanAddAdditionalNamespaces(callable $mapper)
{
$bar = new \SplFileInfo($this->getDrupalRoot().'/core/misc/bar');
$this->assertEquals(['@foo/bar'], $mapper($bar));
}
}
36 changes: 10 additions & 26 deletions src/Twig/AbstractTwigExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public function getDiscoverers(): array
{
return [
new TwigDiscovery(
$this->getDriver(), $this->getTemplateNameIterator()
$this->getDriver(), $this->getIterator()
),
];
}
Expand Down Expand Up @@ -62,34 +62,18 @@ public function subscribe(EventDispatcherInterface $dispatcher)
* Return an iterator that contains a list of twig template names that we
* want to treat as components.
*
* This is formulated by taking an iterator of template filenames, and
* adding the template name mapper in a way that it gets invoked for each
* name in turn.
* This is formulated by taking an iterator of template filenames and
* allowing the driver to modify them using a mapping callback. The mapping
* callback will return the name the driver knows the template by.
*
* @return \LastCall\Mannequin\Core\Iterator\MappingCallbackIterator
*/
protected function getTemplateNameIterator()
{
$iterator = $this->getTemplateFilenameIterator();
$mapper = $this->getTemplateNameMapper();

return new MappingCallbackIterator($iterator, $mapper);
}

/**
* Return a callable that knows how to map a filename to a template name.
*
* @see \LastCall\Mannequin\Twig\TemplateNameMapper
* @return \Traversable
*/
protected function getTemplateNameMapper()
protected function getIterator(): \Traversable
{
$driver = $this->getDriver();
$mapper = new TemplateNameMapper($driver->getTwigRoot());
foreach ($driver->getNamespaces() as $namespace => $paths) {
$mapper->addNamespace($namespace, $paths);
}

return $mapper;
return new MappingCallbackIterator(
$this->getTemplateFilenameIterator(),
$this->getDriver()->getTemplateNameMapper()
);
}

/**
Expand Down
Loading

0 comments on commit 71a0f4f

Please sign in to comment.