merged branch lsmith77/pre_process_app_config (PR #5566)

This PR was merged into the master branch.

Commits
-------

d7a1154 make it possible for bundles extensions to prepend settings into the application configuration of any Bundle

Discussion
----------

[2.2] add possibility for bundles extensions to prepend the app configs

Bug fix: #4652
Feature addition: yes
Backwards compatibility break: no
Symfony2 tests pass: yes
License of the code: MIT

As can be seen in the patch the extensions that should prepend the configuration are enabled automatically if they implement ``PrependExtensionInterface``.

Just as an example, here an extension, which checks if SonataAdminBundle is available and if not disables integration with it in several Bundles. It also sets some default settings for ``document_class`` and ``default_document_manager_name``:
```
diff --git a/DependencyInjection/SymfonyCmfCoreExtension.php b/DependencyInjection/SymfonyCmfCoreExtension.php
index 9f92410..c0a8dbb 100644
--- a/DependencyInjection/SymfonyCmfCoreExtension.php
+++ b/DependencyInjection/SymfonyCmfCoreExtension.php
@@ -3,11 +3,12 @@
 namespace Symfony\Cmf\Bundle\CoreBundle\DependencyInjection;

 use Symfony\Component\HttpKernel\DependencyInjection\Extension;
+use Symfony\Component\\DependencyInjection\PrependExtensionInterface;
 use Symfony\Component\DependencyInjection\ContainerBuilder;
 use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
 use Symfony\Component\Config\FileLocator;

-class SymfonyCmfCoreExtension extends Extension
+class SymfonyCmfCoreExtension extends Extension implements PrependExtensionInterface
 {
     public function load(array $configs, ContainerBuilder $container)
     {
@@ -15,4 +16,45 @@ class SymfonyCmfCoreExtension extends Extension
         $loader->load('config.xml');
         $loader->load('services.xml');
     }
+
+    public function prepend(ContainerBuilder $container)
+    {
+        $bundles = $container->getParameter('kernel.bundles');
+        if (!isset($bundles['SonataDoctrinePHPCRAdminBundle'])) {
+            // disable SonataDoctrinePHPCRAdminBundle admin support in Bundles
+            $config = array('use_sonata_admin' => false);
+            foreach ($container->getExtensions() as $name => $extension) {
+                switch ($name) {
+                    case 'symfony_cmf_menu':
+                    case 'symfony_cmf_routing_extra':
+                    case 'symfony_cmf_simple_cms':
+                        $container->prependExtensionConfig($name, $config);
+                        break;
+                }
+            }
+        }
+
+        // process the configuration of SymfonyCmfCoreExtension
+        $configs = $container->getExtensionConfig($this->getAlias());
+        $config = $this->processConfiguration(new Configuration(), $configs);
+        // add the default configs to various Bundles
+        foreach ($container->getExtensions() as $name => $extension) {
+            switch ($name) {
+                case 'symfony_cmf_content':
+                case 'symfony_cmf_simple_cms':
+                    $container->prependExtensionConfig($name, $config);
+                    break;
+                }
+        }
+    }
 }
```

---------------------------------------------------------------------------

by stof at 2012-09-21T21:10:00Z

I think you are giving too much power to bundles here: a bundle becomes able to modify all the config defined explicitly by the user if it wants to do it.

I think it would be safer to give them the possibility to load an additional config file which would be prepended (so that user-defined config would still win). Giving the ability to load files means passing the loader used by the kernel, and it should then be called before calling the load method on the kernel itself (to respect the order of loaded files)

---------------------------------------------------------------------------

by lsmith77 at 2012-09-22T05:50:08Z

Not sure how a config file helps solve anything. I mean they can load as many config files as they want already. The key is being able to automatically apply configuration to multiple Bundles as well as enabling/disabling features based on if certain Bundles are registered.

BTW the end result in my examples is also prepended, so that user config wins. However yes this would be up to the person implementing the Bundle. We could however provide a dedicated method for prepending in addition to or instead of ``setExtensionConfig``.

---------------------------------------------------------------------------

by stof at 2012-09-22T11:40:29Z

@lsmith77 If you can load a file with the main loader, this file can provide some app-level configuration (be it for your own bundle or others).
And your code example is indeed prepending. But imagine what would occur when someone uses this feature without knowing well how the component works: he will likely call ``setExtensionConfig`` in a first implementation, thus dropping all userland config for the bundle. Your setup does not only allow to make a file win over the userland config but makes it even easier to remove the userland config.

---------------------------------------------------------------------------

by lsmith77 at 2012-09-22T18:11:29Z

but i dont get how that would help. the point is to be able for one bundle to configure other bundles before the load as this is obviously alot cleaner than trying to do the same via a compiler pass. so imho this is what is needed to encourage decoupled bundles. otherwise for example CMS or other reuseable and extensible apps will be forced to always put everything in one bundle.

---------------------------------------------------------------------------

by stof at 2012-09-22T19:23:45Z

@lsmith77 I agree about the feature, not about the way to implement it. If you allow bundles to load a file as it it were some app-level config, they would become able to provide some config for other bundles (and you could load several files depending of which bundles are enabled), but without allowing bundles to remove the userland config.

---------------------------------------------------------------------------

by lsmith77 at 2012-09-22T19:50:19Z

sorry but i dont understand what you suggest. more over i dont see the problem. its already possible to seriously break stuff with compiler passes which cannot be easily enabled/disabled. this is just convenience. if it doesnt work because of some obscure combo then simply dont use it for the app since it needs to be explicitly enabled in the kernel.

---------------------------------------------------------------------------

by lsmith77 at 2012-09-24T09:25:10Z

@stof thought about your comments, are you suggesting for a Bundle to be able to generate a config file that is prepended? in that case the current behavior would already be that if we change ``setExtensionConfig`` to just be a ``prependExtensionConfig`` .. however i am not sure if we really need this limitation since as i point out this would still by far be less dangerous than compiler passes and also i expect this to be used mainly by open source applications on top of Symfony2 rather than standard bundles.

---------------------------------------------------------------------------

by lsmith77 at 2012-10-13T13:28:29Z

@lolautruche i also think this is relevant for you guys. this way you could start preconfiguring 3rd party bundles as part of your main ezPublish bundle.

---------------------------------------------------------------------------

by lolautruche at 2012-10-13T13:57:09Z

While I suspect a nice feature, the implementation looks obscure to me...

---------------------------------------------------------------------------

by lsmith77 at 2012-10-13T17:43:02Z

The implementation of the example extension or the implementation of the actual changes proposed in this PR?

---------------------------------------------------------------------------

by lolautruche at 2012-10-13T17:46:57Z

The example, sorry 😃

---------------------------------------------------------------------------

by lsmith77 at 2012-10-13T17:50:38Z

The example was fairly quickly hacked together. The basic thing you need to do is fetch the config for the bundle you want to change, manipulate the config (usually by appending an array to the array of configs so that you dont affect explicit configuration) and then set it again.

As I explained to @stof it would alternative/additionally be possible to support a method that pushes a config array to the top of the array of config stack. Such a method might make the necessary code simpler.

---------------------------------------------------------------------------

by stof at 2012-10-13T21:39:07Z

@fabpot what do you think about it ?

---------------------------------------------------------------------------

by jrobeson at 2012-10-20T15:45:18Z

I've been porting much of an existing framework over to use more symfony components and bundles. I think that this might help some of the problems i've been having. I would really appreciate some better examples as how to one would use it  (same for the cmf router, but that's another story).

---------------------------------------------------------------------------

by lsmith77 at 2012-10-21T07:28:52Z

not really sure what other examples i could give. the process is quite simple:
1) determine what configuration options to add to other Bundles

for example with the following code I determine that SonataAdmin for PHPCR is not installed (which means i should disable using it in other Bundles):
```
$bundles = $container->getParameter('kernel.bundles');
if (!isset($bundles['SonataDoctrinePHPCRAdminBundle'])) {
```

alternatively I could simply already process the configuration and then pick all or some of these configuration options:
```
$configs = $container->getExtensionConfig($this->getAlias());
$config = $this->processConfiguration(new Configuration(), $configs);
```

2) then add these configuration to what other Bundles I feel should get these options

usually I will add these to the top of the config array stack. this means that if the user would manually set the same setting in most cases the user setting will override what the pre-processor set.
```
$container->unshiftExtensionConfig($name, array('use_sonata_admin' => false));
```

---------------------------------------------------------------------------

by lsmith77 at 2012-10-24T12:52:38Z

added ``ContainerBuilder::unshiftExtensionConfig`` since this is the usual use case. with this method added it could be discussed if ``ContainerBuilder::setExtensionConfig`` is still needed or not.

---------------------------------------------------------------------------

by lsmith77 at 2012-11-24T14:48:44Z

I spoke to @fabpot today and he said that since this patch just allows you to set defaults and not really "process" the actual configuration I shouldn't call it "preProcess" so I renamed it to "prepend".

Furthermore as its just prepending @fabpot said there isnt really a need to require manually enabling it, so here is a patch to auto-enable the prepending logic:
```
diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php
index b890fbf..7374b87 100644
--- a/src/Symfony/Component/HttpKernel/Kernel.php
+++ b/src/Symfony/Component/HttpKernel/Kernel.php
@@ -701,8 +701,8 @@ abstract class Kernel implements KernelInterface, TerminableInterface
      */
     protected function prependExtensionConfigs(ContainerBuilder $container)
     {
-        foreach ($this->getPrependingExtensions() as $name) {
-            $extension = $container->getExtension($name);
+        foreach ($this->bundles as $bundle) {
+            $extension = $bundle->getContainerExtension();
             if ($extension instanceof PrependExtensionInterface) {
                 $extension->prepend($container);
             }
@@ -710,16 +710,6 @@ abstract class Kernel implements KernelInterface, TerminableInterface
     }

     /**
-     * Returns the ordered list of extensions that may prepend extension configurations.
-     *
-     * @return array
-     */
-    protected function getPrependingExtensions()
-    {
-        return array();
-    }
-
-    /**
      * Gets a new ContainerBuilder instance used to build the service container.
      *
      * @return ContainerBuilder
```

---------------------------------------------------------------------------

by lsmith77 at 2012-11-25T19:31:01Z

ok .. i pondered the code some more and now i have enabled registering of the prepending extensions by default, since its now quite easy to just override the ``prependExtensionConfigs()`` method since there is almost no logic in there anymore.

@fabpot i am not 100% sure with the naming yet ..

---------------------------------------------------------------------------

by lsmith77 at 2012-12-05T14:03:43Z

@fabpot if you are ok with the PR as it is now, i can do the rebase so you can merge this?

---------------------------------------------------------------------------

by lsmith77 at 2012-12-05T18:30:29Z

@fabpot all good now? then i will squash the commits ..

---------------------------------------------------------------------------

by lsmith77 at 2012-12-05T18:34:50Z

actually looking at the full change set again i am no longer sure if it makes sense to have ``PrependExtensionInterface`` in the DI rather than the HttpKernel.

---------------------------------------------------------------------------

by lsmith77 at 2012-12-07T09:21:14Z

@fabpot all good now?

---------------------------------------------------------------------------

by fabpot at 2012-12-07T09:37:52Z

The code looks good to me now. There are two remaining task before merging:

* Is it something we need to add somewhere in the documentation?
* Can you add a note in the DI component CHANGELOG?

Thanks.

---------------------------------------------------------------------------

by lsmith77 at 2012-12-07T09:49:17Z

i have added a changelog entry and squashed the commits.
i will also work on a documentation entry, i guess i will make it a cookbook entry. not sure if it should be included in http://symfony.com/doc/2.0/cookbook/bundles/extension.html .. but imho it would better be a separate entry.
This commit is contained in:
Fabien Potencier 2012-12-07 11:04:59 +01:00
commit 79b4ca686e
6 changed files with 72 additions and 3 deletions

View File

@ -5,7 +5,9 @@ CHANGELOG
-----
* added an Extension base class with sensible defaults to be used in conjunction
with the Config component.
with the Config component.
* added PrependExtensionInterface (to be able to allow extensions to prepend
application configuration settings for any Bundle)
2.1.0
-----
@ -18,4 +20,3 @@ CHANGELOG
(this includes dumped containers)
* [BC BREAK] fixed unescaping of class arguments, method
ParameterBag::unescapeValue() was made public

View File

@ -29,6 +29,12 @@ class MergeExtensionConfigurationPass implements CompilerPassInterface
$definitions = $container->getDefinitions();
$aliases = $container->getAliases();
foreach ($container->getExtensions() as $extension) {
if ($extension instanceof PrependExtensionInterface) {
$extension->prepend($container);
}
}
foreach ($container->getExtensions() as $name => $extension) {
if (!$config = $container->getExtensionConfig($name)) {
// this extension was not called

View File

@ -0,0 +1,24 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
interface PrependExtensionInterface
{
/**
* Allow an extension to prepend the extension configurations.
*
* @param ContainerBuilder $container
*/
public function prepend(ContainerBuilder $container);
}

View File

@ -465,6 +465,21 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
return $this->extensionConfigs[$name];
}
/**
* Prepends a config array to the configs of the given extension.
*
* @param string $name The name of the extension
* @param array $config The config to set
*/
public function prependExtensionConfig($name, array $config)
{
if (!isset($this->extensionConfigs[$name])) {
$this->extensionConfigs[$name] = array();
}
array_unshift($this->extensionConfigs[$name], $config);
}
/**
* Compiles the container.
*

View File

@ -548,6 +548,28 @@ class ContainerBuilderTest extends \PHPUnit_Framework_TestCase
$container->compile();
$container->setDefinition('a', new Definition());
}
/**
* @covers Symfony\Component\DependencyInjection\ContainerBuilder::getExtensionConfig
* @covers Symfony\Component\DependencyInjection\ContainerBuilder::prependExtensionConfig
*/
public function testExtensionConfig()
{
$container = new ContainerBuilder();
$configs = $container->getExtensionConfig('foo');
$this->assertEmpty($configs);
$first = array('foo' => 'bar');
$container->prependExtensionConfig('foo', $first);
$configs = $container->getExtensionConfig('foo');
$this->assertEquals(array($first), $configs);
$second = array('ding' => 'dong');
$container->prependExtensionConfig('foo', $second);
$configs = $container->getExtensionConfig('foo');
$this->assertEquals(array($second, $first), $configs);
}
}
class FooClass {}

View File

@ -1,4 +1,5 @@
vendor/
composer.lock
phpunit.xml
Tests/ProjectContainer.php
Tests/classes.map