问题
My question is similar to this question but slightly different and since that question wasn't answered, I thought I would try again.
I know Symfony will look first in app/Resources/FooBundle/views
and then second in FooBundle/Resources/views
for templates. I would like to "inject" another location between the two, e.g.
app/Resources/FooBundle/views
BarBundle/Resources/FooBundle/views
FooBundle/Resources/views
I've tried overriding the \Symfony\Component\HttpKernel\Config\FileLocator
service and added a path there, but this didn't work. I've also tried the answer here in the controller::render method without success. (the 'native' method suggested there doesn't work because the path comes from a dynamic setting.)
As a secondary note, I need to be able to add this path later (maybe onRequest) instead of at container creation. So an EventListener, custom service method or call in the controller would be appropriate.
回答1:
Part of the challenge in solving this problem was realizing that (at least in Symfony 2.7) there are at least two ways to specify a template name: the 'old way' (for lack of a better name) and the 'name-spaced' method which was implemented later. e.g.
class MyController extends \Symfony\Bundle\FrameworkBundle\Controller\Controller
{
// the 'old way'
public function barAction()
{
return $this->render('FooBundle:User:foo.html.twig');
}
// namespaced
public function fooAction()
{
return $this->render('@Foo/User/foo.html.twig');
}
}
These two methods result in different template location searches and therefore code had to be modified in two different places.
In order to modify the 'old' way, I overrode the Kernel::locateResource() method
public function locateResource($name, $dir = null, $first = true)
{
$customBundle = $this->container->get('my_custom_bundle');
$locations = parent::locateResource($name, $dir, false);
if ($locations && (false !== strpos($locations[0], $dir))) {
// if found in $dir (typically app/Resources) return it immediately.
return $locations[0];
}
// add custom path to template locator
// this method functions if the controller uses `@Template` or `FooBundle:Foo:index.html.twig` naming scheme
if ($customBundle && (false === strpos($name, $customBundle->getName()))) {
// do not add override path to files from same bundle
$customPath = $customBundle->getPath() . '/Resources';
return parent::locateResource($name, $customPath, true);
}
return $locations[0];
}
In order to modify the namespace way, I added a ControllerListener
class OverrideListener implements EventSubscriberInterface
{
private $loader;
private $customBundle;
function __construct(\Twig_Loader_Filesystem $loader, $customBundle)
{
$this->loader = $loader;
$this->customBundle = $customBundle;
}
/**
* @param FilterControllerEvent $event
* @throws \Twig_Error_Loader
*/
public function setUpOverrides(FilterControllerEvent $event)
{
// add custom path to template locator
// This 'twig.loader' functions only when @Bundle/template (name-spaced) name-scheme is used
$controller = $event->getController()[0];
if ($controller instanceof AbstractController) {
$bundleName = $controller->getName(); // my AbstractController adds a getName method returning the BundleName
if ($this->customBundle) {
$overridePath = $this->customBundle->getPath() . '/Resources/' . $bundleName . '/views';
if (is_readable($overridePath)) {
$paths = $this->loader->getPaths($bundleName);
// inject overridePath before the original path in the array
array_splice($paths, count($paths) - 1, 0, array($overridePath));
$this->loader->setPaths($paths, $bundleName);
}
}
}
}
public static function getSubscribedEvents()
{
return array(
KernelEvents::CONTROLLER => array(array('setUpOverrides')),
);
}
}
I hope this helps anyone coming along looking for a similar solution.
来源:https://stackoverflow.com/questions/32075460/symfony-add-default-template-search-path