Skip to content

[TwigBundle] Fix bug where namespaced paths don't take parent bundles in account #19586

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed

Conversation

wesleylancel
Copy link
Contributor

@wesleylancel wesleylancel commented Aug 10, 2016

Q A
Branch? 2.7
Bug fix? yes
New feature? no
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #6919
License MIT
Doc PR

Currently namespaced paths for templates such as {% extends '@App/Layout/layout.html.twig' %} do not work with bundles that have overruled templates using the getParent() method in another bundle. See attached ticket. This change prepends the path of the bundle implementing getParent() to the paths of the namespace of bundle returned as a parent.

@jakzal
Copy link
Contributor

jakzal commented Aug 10, 2016

We'll need a test case for this.

@wesleylancel wesleylancel force-pushed the namespaced-paths-parent-bundles branch from b3d4a15 to 4ddc9ed Compare August 10, 2016 09:26
@wesleylancel
Copy link
Contributor Author

@jakzal I modified the TwigExtensionTest to include a test case for this situation.

@wesleylancel wesleylancel force-pushed the namespaced-paths-parent-bundles branch from 4ddc9ed to 57eceb7 Compare August 10, 2016 09:30
@wesleylancel
Copy link
Contributor Author

AppVeyor ran on an outdated commit, might have to be restarted.

@@ -92,6 +92,13 @@ public function load(array $configs, ContainerBuilder $container)
$dir = dirname($reflection->getFileName()).'/Resources/views';
if (is_dir($dir)) {
$this->addTwigPath($twigFilesystemLoaderDefinition, $dir, $bundle);

$classMock = new $class();
Copy link
Contributor

@ro0NL ro0NL Aug 10, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not really a mock :) what about $object or just $bundle? However im not sure we should couple this here, this way. But i guess for now we dont have much choice...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll rename the variable. Don't think there are any options to retrieve the parent in this situation.

Copy link
Contributor

@ro0NL ro0NL Aug 10, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you could call registerBundles once =/ Basically new $class() is somewhat fragile.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, and this won't work with a third level (ChildChildTwigBundle). How would you recommend calling registerBundles here? Is there any way to reach the kernel?

Copy link
Contributor

@ro0NL ro0NL Aug 10, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kernel service is out of scope, so i guess not. Maybe you can kick in at TwigBundle::createContainerExtension, without breaking BC.

This is somewhat related to #18563 in terms of improving the ecosystem.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about using reflection and newInstanceWithoutConstructor() here?

Copy link
Member

@chalasr chalasr Dec 29, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xabbuh some bundles may use constructor arguments in their getParent() method.
I tried to solve a similar issue in #18579

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true, but the only ways to solve that issue completely would be to either pass bundles to extension classes or to write some logic that determines the paths during cache warmup.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't a parameter returning the bundle map be envisaged?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that you still would not have access to actual instances of the bundles (so this would not solve #19586 (comment)).

@xabbuh
Copy link
Member

xabbuh commented Dec 28, 2016

@wesleylancel Do you think you can finish this one?

@wesleylancel
Copy link
Contributor Author

@xabbuh: I wish, as this is something that would be extremely useful for us. But, as pointed out by @ro0NL, bundles can extend infinitely A > B > C > D. My PR currently only checks for one level of extending. Ideally, I would be able to access the bundle map that is part of the kernel, but that's not available at this stage yet. If you could point me towards a possible solution I'd be happy to adjust my code accordingly.

@ro0NL
Copy link
Contributor

ro0NL commented Dec 28, 2016

Seeing the related ticket this goes back to 2013. And quite some issues are involved. I've not experienced it myself, but the template problem seems real.

Either way allowing kernel introspection would be extremely useful for everybody. Preferably with an immutable kernel. Practically we can move this a few lines up. Relying on a synthetic service before compilation.

We also can expose bundles thru parameters, we only need stringish values here. BundleInterface only defines name, path and namespace anyway. So passing a stringish bundle map could work, and seems to make sense as we also provide kernel info already. Any further details can be provided with Bundle::build.

But mayve @ogizanagi has more idea's? As i also liked his approach with a contextual builder. Maybe it can be simplified.

@ogizanagi
Copy link
Contributor

Unfortunately #19646 was my only attempt to provide a naive feature for sharing some contextual information while the container is building, avoiding the need to rely on pseudo tmp synthetic services. Along with the BootingKernel and BootingBundle classes, it allowed to solves issues related to this one as a demonstration of the possibilities.
But despite the implementation is simple, it somehow looks overkilled for the few needs.

As you suggested, maybe exposing a simple representation of the bundle map and bundles informations in a kernel parameter could be the way. But I don't remember all the ins and outs and if it was easily feasible at this point. :/

@ro0NL
Copy link
Contributor

ro0NL commented Dec 28, 2016

But despite the implementation is simple, it somehow looks overkilled for the few needs.

Agree 👍 I feel the same about my own attempts now. Eventually having a builder with context can be done in userland easily. But it doesnt solve it for SF.

If there arent any other insights, i'd say go for %kernel.bundle_map% 👍

@chalasr
Copy link
Member

chalasr commented Dec 29, 2016

i'd say go for %kernel.bundle_map%

Not sure it's doable. Putting Kernel::$bundleMap in a new kernel parameter would just lead to

Unable to dump a service container if a parameter is an object or a resource.

@ro0NL
Copy link
Contributor

ro0NL commented Dec 29, 2016

It's a stringish map, that's actually cached :) we're not passing bundle instances around here.

array(
  'name' => ['parent' => 'name2', 'namespace' => 'NS', 'path' => '/bundles/name' ],
  'name2' => //
)

Something like that i guess.

I mean.. this would lead to a nested <argument> collection in XML right?

@ro0NL
Copy link
Contributor

ro0NL commented Dec 29, 2016

Ie. looking at the issues, im not really sure we need true bundle instances here. Only metadata from bundle introspection. To build things properly.

I truly dont hope we're looking for ways to do this since 2013 😆 as it would definitely require the BootingKernel idea from @ogizanagi..

StijnMaenhaut referenced this pull request in FriendsOfSymfony/FOSUserBundle Dec 30, 2016
This makes the template compatible with projects disabling the Templating
component in recent Symfony versions.
@xabbuh
Copy link
Member

xabbuh commented Jan 6, 2017

👍 I checked this with the issues described in FriendsOfSymfony/FOSUserBundle#2376 and FriendsOfSymfony/FOSUserBundle#2378 and I can confirm that this patch fixes them.

Status: Reviewed

@maxime-pasquier
Copy link

@xabbuh great news! Could you tell us on which branches this patch will be applied ?

@chalasr
Copy link
Member

chalasr commented Jan 6, 2017

@Kmelia Once merged, this will be available in the next patch releases of each maintained branch (i.e. from 2.7 to 3.2) and the current development version (3.3).

@@ -97,6 +97,10 @@ public function load(array $configs, ContainerBuilder $container)
if (is_dir($dir = $bundle['path'].'/Resources/views')) {
$this->addTwigPath($twigFilesystemLoaderDefinition, $dir, $name);
}

if (null !== $bundle['parent']) {
$this->prependParentPaths($twigFilesystemLoaderDefinition, $container, $name, $bundle['parent']);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this handle a chain of parents ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stof: Yes, this handles that. Should be covered with the changed test.

Copy link
Contributor

@ro0NL ro0NL Jan 6, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prependParentPaths was confusing at first. Maybe keep actual path registration explicit in load()

while ($parent) {
   // add parent
}

// add self

Basically we dont need any path prepending or recursion, only adding paths in the right calling order.

Maybe utilize with addBundlePaths or so.

@@ -97,6 +97,10 @@ public function load(array $configs, ContainerBuilder $container)
if (is_dir($dir = $bundle['path'].'/Resources/views')) {
$this->addTwigPath($twigFilesystemLoaderDefinition, $dir, $name);
}

if (null !== $bundle['parent']) {
$this->prependParentPaths($twigFilesystemLoaderDefinition, $container, $name, $bundle['parent']);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prepending paths looks wrong to me here, as it would overwrite user-configured paths for the same namespace too.
What you should do is collecting the child bundles for each bundle, and then adding paths for child bundles before adding the path of the bundle itself

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stof: Did not now about that. That'll require some change to adding the normal paths too, I'll see how to best fix this. Is there an easy way to cover this in the test?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a path in the user-configured paths for the same namespace, and ensure that it ends up as the first path in the loader config for this namespace

@maxime-pasquier
Copy link

@chalasr thanks!

array(__DIR__.'/Fixtures/Bundle/ChildTwigBundle/Resources/views', 'Twig'),
array(__DIR__.'/Fixtures/Bundle/ChildChildTwigBundle/Resources/views', 'ChildTwig'),
array(__DIR__.'/Fixtures/Bundle/ChildChildTwigBundle/Resources/views', 'Twig'),
), $prependPaths);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way these tests are written does not ensure that paths are added in the right order for each namespace. there is nothing guaranteeing that the combinations of appended paths and prepended paths is actually giving us the right priority.

What we should do to have reliable tests is rebuilding the internal structure of Twig_Loader_Filesystem::$path (i.e. the result of the configuration) and ensuring that it looks good for each namespace.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stof: I think whatever gets prepended last will have the most priority. If the array above is in this order, the correct path will end up on top. However, this doesn't keep those user-configured paths in mind. I'm not sure where these get added to the loader, but if they get prepended after the bundle paths get prepended, it should work all right? Not sure if that's testable in the existing test though.

If the Twig internals of that loader do need to be changed, I don't think this can be merged before that is resolved?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stof: What if we move these lines https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php#L84-L90 to here https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php#L109 and change the method call to prependPath instead of addPath? This should make sure that user-defined paths from config take precedence over anything else.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the issue is that the way the test is written does not allow to quickly validate what the final order will be when looking at it. This is why I think the test should not be written this way. We don't want to assert 2 separate lists, as we care about a single list (the one being used at runtime).

thus, the test is even confusing when looking at it, because the list of prependPath here is in reverse order compared to the order of paths in the loader (as prepending adds at the beginning while you append in $prependPaths), making it even harder to validate/understand the assertion.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stof I'll see if I can collect all the paths before actually adding them to the loader so they can all be tested together in order.

@wesleylancel
Copy link
Contributor Author

@stof I've added a separate commit so it's easier to see what I've changed (can be squashed later). A hierarchy of bundles is now built first, after that the paths are added to the loader. I'm not sure if I could simplify the code, especially the building of the hierarchy.

I could improve the test by asserting the array per namespace if that helps for readability?

@xabbuh
Copy link
Member

xabbuh commented Jan 7, 2017

I think an easier implementation could be to have two loops. The first would collect path information for bundles we override without actually registering them. The second would then first register the additional paths from child bundles followed by paths the bundle provides itself.

maryo added a commit to vaniocz/vanio-user-bundle that referenced this pull request Jan 9, 2017
@wesleylancel
Copy link
Contributor Author

@xabbuh: Isn't that what I'm doing now? First the hierarchy is built and then the paths are registered.

);
}

if (is_dir($dir = $container->getParameter('kernel.root_dir').'/Resources/'.$name.'/views')) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to maintainers: when merging to newer branches, we need to take care of the FileExistenceResource objects here (git conflicts should remind us about this btw)

@stof
Copy link
Member

stof commented Jan 11, 2017

👍

1 similar comment
@dunglas
Copy link
Member

dunglas commented Jan 11, 2017

👍

@fabpot
Copy link
Member

fabpot commented Jan 11, 2017

Thank you @wesleylancel.

fabpot added a commit that referenced this pull request Jan 11, 2017
…ent bundles in account (wesleylancel)

This PR was squashed before being merged into the 2.7 branch (closes #19586).

Discussion
----------

[TwigBundle] Fix bug where namespaced paths don't take parent bundles in account

| Q | A |
| --- | --- |
| Branch? | 2.7 |
| Bug fix? | yes |
| New feature? | no |
| BC breaks? | no |
| Deprecations? | no |
| Tests pass? | yes |
| Fixed tickets | #6919 |
| License | MIT |
| Doc PR |  |

Currently namespaced paths for templates such as `{% extends '@App/Layout/layout.html.twig' %}` do not work with bundles that have overruled templates using the `getParent()` method in another bundle. See attached ticket. This change prepends the path of the bundle implementing `getParent()` to the paths of the namespace of bundle returned as a parent.

Commits
-------

0c77ce2 [TwigBundle] Fix bug where namespaced paths don't take parent bundles in account
@fabpot fabpot closed this Jan 11, 2017
@fabpot fabpot mentioned this pull request Jan 12, 2017
@homersimpsons
Copy link
Contributor

@fabpot Do you have any hint about the new release (containing this fix) date?

@chalasr
Copy link
Member

chalasr commented Jan 12, 2017

@homersimpsons today :) see #21258

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.