Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[TwigBundle] Adds bundle view path in a delayed compiler pass #30527

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
wants to merge 14 commits into from
Closed

[TwigBundle] Adds bundle view path in a delayed compiler pass #30527

wants to merge 14 commits into from

Conversation

alterphp
Copy link

@alterphp alterphp commented Mar 11, 2019

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #30359
License MIT

Replaces #30360.

Delaies bundle view path registration into Twig filesystem loader in order to let 3rd party bundle override them if needed.

@alterphp alterphp changed the title Adds bundle view in a delayed compiler pass [TwigBundle] Adds bundle view in a delayed compiler pass Mar 11, 2019
Copy link
Member

@yceruto yceruto left a comment

Choose a reason for hiding this comment

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

It looks better than before :) Thank you!

It will be good to have a test where we can see this feature in action.

@alterphp
Copy link
Author

It will be good to have a test where we can see this feature in action.

I'm working on it

@nicolas-grekas nicolas-grekas changed the base branch from master to 4.2 March 12, 2019 19:53
@nicolas-grekas nicolas-grekas added this to the 4.2 milestone Mar 12, 2019
@alterphp
Copy link
Author

I've added a test of the feature TwigTestExtension::testThirdPartyBundlesMayOverrideBetweenThem. It looks pretty tricky. Let me know you opinion about it.

@alterphp
Copy link
Author

@yceruto @nicolas-grekas Any idea on how to fix the error on AppVeyor/Travis ?

@yceruto
Copy link
Member

yceruto commented Mar 13, 2019

The .gitignore file will ignore all vendor/ sub-dirs too, try renaming the new vendor dir under Fixtures to something else.

@yceruto
Copy link
Member

yceruto commented Mar 13, 2019

@alterphp please rebase to master again and update the PR template for "master" branch and "New feature", the current one belongs to the previous PR. I guess @nicolas-grekas has been confused because of it, and has changed the base branch to 4.2.

@alterphp
Copy link
Author

@alterphp please rebase to master again and update the PR template for "master" branch and "New feature", the current one belongs to the previous PR. I guess @nicolas-grekas has been confused because of it, and has changed the base branch to 4.2.

That was my mistake to leave master as base branch instead of 4.2

@yceruto
Copy link
Member

yceruto commented Mar 13, 2019

It will be great to have this for 4.2, but to be honest, I think this is a new feature, there is no faulty behavior in 4.2

@stof
Copy link
Member

stof commented Mar 13, 2019

I think using the PrependExtensionInterface in your bundle to inject some config for TwigBundle for the path is a better way than delaying the configuration of the bundle.

@yceruto
Copy link
Member

yceruto commented Mar 13, 2019

I think using the PrependExtensionInterface in your bundle to inject some config for TwigBundle for the path is a better way than delaying the configuration of the bundle.

Yea, I thought that too (#30360 (comment)) as workaround, but when two extra bundles are involved then it is very hard to configure the paths order.

@yceruto
Copy link
Member

yceruto commented Mar 13, 2019

To be clear, this is the workaround through PrependExtensionInterface:

twig:
    paths:
        # user-configured paths
        # ...

        # added from CompanyBundle through PrependExtensionInterface
        'templates/bundles/EasyAdminBundle/': EasyAdmin # the first one wins
        'vendor/acme/company-bundle/src/Resources/views': EasyAdmin

        # added from CommonBundle through PrependExtensionInterface
        'templates/bundles/EasyAdminBundle/': EasyAdmin # it doesn't really added
        'vendor/acme/common-bundle/src/Resources/views': EasyAdmin

        # added from TwigExtension
        'templates/bundles/EasyAdminBundle/': EasyAdmin # never reached
        'vendor/easycorp/easyadmin-bundle/src/Resources/views': EasyAdmin
        'vendor/easycorp/easyadmin-bundle/src/Resources/views': !EasyAdmin

Code:

class CompanyExtension extends Extension implements PrependExtensionInterface
{
    // ...

    public function prepend(ContainerBuilder $container)
    {
        $twigConfigs = $container->getExtensionConfig('twig');

        $paths = [];
        // keeping user-configured paths
        foreach ($twigConfigs as $twigConfig) {
            if (isset($twigConfig['paths'])) {
                $paths += $twigConfig['paths'];
            }
        }
        // the overriding dir at project level (make sure the dir exists first)
        $paths['templates/bundles/EasyAdminBundle/'] = 'EasyAdmin';
        // new overriding dir at vendor level
        $paths[\dirname(__DIR__).'/Resources/views/'] = 'EasyAdmin';

        $container->prependExtensionConfig('twig', ['paths' => $paths]);
    }
}

Same for CommonBundle.

@alterphp
Copy link
Author

alterphp commented Mar 13, 2019

I think using the PrependExtensionInterface in your bundle to inject some config for TwigBundle for the path is a better way than delaying the configuration of the bundle.

This is what I get by making my 2 bundles using PrependExtenionInterface :

"EasyAdmin" => array:4 [▼
      0 => "/var/www/symfony/vendor/alterphp/easyadmin-extension-bundle/src/Resources/views"
      1 => "/var/www/symfony/vendor/kiplin/admin-bundle/src/Resources/views"
      2 => "/var/www/symfony/templates/bundles/EasyAdminBundle"
      3 => "/var/www/symfony/vendor/easycorp/easyadmin-bundle/src/Resources/views"
    ]

What I want is :

"EasyAdmin" => array:4 [▼
      0 => "/var/www/symfony/templates/bundles/EasyAdminBundle"
      1 => "/var/www/symfony/vendor/kiplin/admin-bundle/src/Resources/views"
      2 => "/var/www/symfony/vendor/alterphp/easyadmin-extension-bundle/src/Resources/views"
      3 => "/var/www/symfony/vendor/easycorp/easyadmin-bundle/src/Resources/views"
    ]

2 issues :

  • the order is wrong between the 3rd-party bundles is wrong. Top-level bundle is kiplin/admin-bundle (loaded at last position), intermediate is alterphp/easyadmin-extension-bundle and at last, easycorp/easyadmin-bundle.
  • default overriding path at project level is not on top of all 3 bundles...

This could work by configuring at project-level, not trough bundle config.

@alterphp alterphp changed the base branch from 4.2 to master March 13, 2019 22:05
@alterphp
Copy link
Author

It will be great to have this for 4.2, but to be honest, I think this is a new feature, there is no faulty behavior in 4.2

Ok, done

@yceruto
Copy link
Member

yceruto commented Mar 13, 2019

This could work by configuring at project-level, not trough bundle config.

@alterphp Have you tried with the code #30527 (comment) that I've prepared just for you?

the order is wrong between the 3rd-party bundles is wrong. Top-level bundle is kiplin/admin-bundle (loaded at last position), intermediate is alterphp/easyadmin-extension-bundle and at last, easycorp/easyadmin-bundle.

The order in which the extensions are executed depends on the order in which the bundles are registered. Try registering KiplinAdminBundle after EasyAdminExtensionBundle.

default overriding path at project level is not on top of all 3 bundles...

You need to add the EasyAdmin path again, see #30527 (comment).

@alterphp
Copy link
Author

alterphp commented Mar 13, 2019

@yceruto Yes I used what you've written especially for me ;-)

Here is EasyAdminExtensionExtension::prepend method

    public function prepend(ContainerBuilder $container)
    {
        $twigConfigs = $container->getExtensionConfig('twig');

        $paths = [];

        // keeping user-configured paths
        foreach ($twigConfigs as $twigConfig) {
            if (isset($twigConfig['paths'])) {
                $paths += $twigConfig['paths'];
            }
        }

        // Override EasyAdmin templates
        $paths[\dirname(__DIR__).'/Resources/views/'] = 'EasyAdmin';

        // Define new BaseEasyAdmin namespace to point base EasyAdmin templates
        $baseEasyAdminBundleRefl = new \ReflectionClass(EasyAdminBundle::class);
        $paths[\dirname($baseEasyAdminBundleRefl->getFileName()).'/Resources/views/'] = 'BaseEasyAdmin';

        $container->prependExtensionConfig('twig', ['paths' => $paths]);
    }

Here is KiplinAdminExtension::prepend method

    public function prepend(ContainerBuilder $container)
    {
        $twigConfigs = $container->getExtensionConfig('twig');

        $paths = [];

        // keeping user-configured paths
        foreach ($twigConfigs as $twigConfig) {
            if (isset($twigConfig['paths'])) {
                $paths += $twigConfig['paths'];
            }
        }

        // Override EasyAdmin templates
        $paths[\dirname(__DIR__).'/Resources/views/'] = 'EasyAdmin';

        $container->prependExtensionConfig('twig', ['paths' => $paths]);
    }

KiplinAdminBundle is loaded after EasyAdminExtensionBundle, let's see config/bundles.php

return [
    // ...
    EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle::class => ['all' => true],
    AlterPHP\EasyAdminExtensionBundle\EasyAdminExtensionBundle::class => ['all' => true],
    // ...
    Kiplin\AdminBundle\KiplinAdminBundle::class => ['all' => true],
    // ...
];

You need to add the EasyAdmin path again, see #30527 (comment).

This is what I dislike the most with this workaround, you have to push again a path that should, by definition, stay on top of the namespace paths stack. This is the promise of TwigBundle configuration IMO. And as a third-party bundle provider, this is a pain to explain to users that they have to add configuration to their projects to keep the default expected behavior of Twig templates cascading paths...

WDYT ?

@alterphp
Copy link
Author

As @javiereguiluz told on Slack, I ping to include this before 4.3 feature freeze ! @nicolas-grekas

@yceruto
Copy link
Member

yceruto commented Mar 14, 2019

default overriding path at project level is not on top of all 3 bundles...

You need to add the EasyAdmin path again, see #30527 (comment).

And as a third-party bundle provider, this is a pain to explain to users that they have to add configuration to their projects to keep the default expected behavior of Twig templates cascading paths

Hi @alterphp, this is what I meant #30527 (comment):

// ...

// the overriding dir at project level (make sure the dir exists first)
$paths['templates/bundles/TwigBundle/'] = 'EasyAdmin';

// ...

The user/dev doesn't care about it, they are internal details.

@alterphp
Copy link
Author

alterphp commented Mar 14, 2019

The user/dev doesn't care about it, they are internal details.

For project developers, why not (but it's some overhead, unoptimized memory usage anyway). As a framework, Symfony goal is also to ease the development of third-party bundles, it's not only a matter of end-users/developers.
In those cases, PrependExtensionInterface is a workaround, not a solution, IMO. And there is still the problem of the order.

@alterphp alterphp changed the title [TwigBundle] Adds bundle view in a delayed compiler pass [TwigBundle] Adds bundle view path in a delayed compiler pass Mar 14, 2019
@stof
Copy link
Member

stof commented Mar 28, 2019

btw, the diff of this PR should be cleaned. There is no reason for it to change the Cache component.

@alterphp
Copy link
Author

alterphp commented Apr 2, 2019

@stof done

@alterphp
Copy link
Author

alterphp commented Apr 2, 2019

Here is the workaround required without this PR enhancement : alterphp/EasyAdminExtensionBundle#115

  • my second 3rd-party bundle :

    public function prepend(ContainerBuilder $container)
    {
        $twigConfigs = $container->getExtensionConfig('twig');

        $paths = [];
        // keeping user-configured paths
        foreach ($twigConfigs as $twigConfig) {
            if (isset($twigConfig['paths'])) {
                $paths += $twigConfig['paths'];
            }
        }

        // Re-ordering EasyAdmin paths
        $easyAdminPaths = [];
        $easyAdminExtensionBundleRefl = new \ReflectionClass(EasyAdminExtensionBundle::class);
        $easyAdminExtensionTwigPath = \dirname((string) $easyAdminExtensionBundleRefl->getFileName()).'/Resources/views/';
        foreach ($paths as $path => $namespace) {
            if ('EasyAdmin' === $namespace) {
                // Pushing KiplinAdmin bundle overrides just above alterphp EasyAdminExtension bundle
                if ($easyAdminExtensionTwigPath === $path) {
                    $easyAdminPaths[\dirname(__DIR__).'/Resources/views/'] = 'EasyAdmin';
                }
                // if ($easyAdminExtensionTwigPath === $path)
                $easyAdminPaths[$path] = 'EasyAdmin';
                unset($paths[$path]);
            }
        }
        $paths += $easyAdminPaths;

        $container->prependExtensionConfig('twig', ['paths' => $paths]);
    }

It works but it is pretty unstable and not at the quality level a 3rd party bundle should offer, IMO.

@yceruto
Copy link
Member

yceruto commented Apr 6, 2019

I'll create a small demo to see the differences between both approaches, I hope this helps to decide what to do here.

@yceruto
Copy link
Member

yceruto commented Apr 6, 2019

and here is it https://github.com/yceruto/twigpathprepend
diff for bundles: https://github.com/yceruto/twigpathprepend/commit/98a2f8f8b5a984adf3c6d398438cb55cd07512d9
and the result:
twigloader

Yes, the path templates/bundles/EasyAdminBundle/ is duplicated but I don't think it's a big deal, it'll never be reached anyway.

So I think @stof is right and probably delaying the bundle paths registration is not worth it.

@fabpot
Copy link
Member

fabpot commented Apr 8, 2019

@yceruto Does it mean that we can close this PR?

@yceruto
Copy link
Member

yceruto commented Apr 9, 2019

Yes, I think so, also others arguments here:

#28376 (comment)
Adding paths directly on the filesystem loader is quite fragile, because Symfony is injecting the paths in multiple places, and twig.loader.native_filesystem might even not be used at all (when symfony/templating is installed, we use a different loader).
Thus, compiler passes might run after the TwigBundle one, so reading method calls added on this services (with some complex logic to detect the one not added by TwigBundle itself) will not even ensure that this works (it would depend on the order of passes)

@yceruto
Copy link
Member

yceruto commented Apr 9, 2019

@alterphp Thank you so much for your work on this topic; you spent a lot of time on it. Please, don't hesitate to contact me to continue talking about this topic if you want, I'm glad to help you.

@alterphp
Copy link
Author

alterphp commented Apr 9, 2019

Hi @yceruto. Thanks for your support. I finally implemented your workaround in my bundles. It does the job ! It's a bit tricky because I have 2 overriding bundles above EasyAdmin but it works.

@alterphp alterphp closed this Apr 9, 2019
@alterphp alterphp deleted the fix/30359-2 branch April 16, 2019 19:15
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.

6 participants