-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[2.8][HttpKernel] Added support for registering Bundle dependencies #15011
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,91 @@ | ||
<?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\HttpKernel\Bundle; | ||
|
||
/** | ||
* Class BundleDependenciesInterface. | ||
* | ||
* Adds capability to let Bundles specify other bundles they need to load (before this one), both required and optional | ||
* dependencies. | ||
* | ||
* This allows you to define only the bundle you want to use in registerBundles(), but don't need to take care about | ||
* registering the dependencies it uses, and you won't need to make any changes in your kernel if those dependencies | ||
* change. | ||
* | ||
* NOTE: In this interface bundle dependencies are returned as FQN strings because several bundles (and root) might | ||
* register the same bundle. This means dependencies can not have arguments in its constructor, reflecting best practice | ||
* in Symfony of not having any logic in Bundle constructors given it is executed on every Kernel boot. | ||
* | ||
* NOTE2: This functionality is not a bundle plugin system, but rather a way to set your upstream dependencies, | ||
* or in the case of a distribution bundle, all bundles they are to be used with. | ||
* | ||
* @author André Roemcke <andre.romcke@ez.no> | ||
* | ||
* @since 2.8 | ||
* | ||
* @api | ||
*/ | ||
interface BundleDependenciesInterface | ||
{ | ||
/** | ||
* @const Flag a Bundle Dependency as required, if missing throw exception | ||
* | ||
* @link \Symfony\Component\HttpKernel\Exception\DependencyMismatchException | ||
*/ | ||
const DEP_REQUIRED = true; | ||
|
||
/** | ||
* @const Flag a Bundle Dependency as optional, if missing silently ignore it | ||
*/ | ||
const DEP_OPTIONAL = false; | ||
|
||
/** | ||
* Returns an array of bundle dependencies Kernel should register on boot. | ||
* | ||
* Dependencies will be registered before current bundle, implying current bundle *MUST* be loaded after as it | ||
* for instance extends it. | ||
* | ||
* Example of use: | ||
* ```php | ||
* class AcmeBundle extends Bundle implements BundleDependenciesInterface | ||
* { | ||
* public function getBundleDependencies($environment, $debug) | ||
* { | ||
* $dependencies = array(); | ||
* | ||
* // If you need to load some bundles only in dev using $environment (or in $debug) | ||
* if ($environment === 'dev') { | ||
* $dependencies['Egulias\SecurityDebugCommandBundle\EguliasSecurityDebugCommandBundle'] = self::DEP_REQUIRED; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You should put brackets above and below this line to fit PSR-1 & PSR-2 coding style, even in the example There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed as of c2f5851 |
||
* } | ||
* | ||
* return $dependencies + array( | ||
* // Keys must be Fully Qualified Name (FQN) for the class to avoid bundles being loaded several times | ||
* // Note: Currently, use of DEP_OPTIONAL causes a *uncached* file system call on boot (every request) | ||
* // if dependency is missing, in a future versions this information might be cached. | ||
* 'FOS\HttpCacheBundle\FOSHttpCacheBundle' => self::DEP_OPTIONAL, | ||
* | ||
* // If you require PHP 5.5+ it is possible to use `::class` constant for required dependencies: | ||
* Oneup\FlysystemBundle\OneupFlysystemBundle::class => self::DEP_REQUIRED, | ||
* ); | ||
* } | ||
* } | ||
* ``` | ||
* | ||
* @param string $environment The current environment | ||
* @param bool $debug Whether to debugging is enabled or not | ||
* | ||
* @return mixed[string] An array where key is bundle class (FQN) names as strings, and value DEP_* constants | ||
* | ||
* @api | ||
*/ | ||
public function getBundleDependencies($environment, $debug); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
<?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\HttpKernel\Exception; | ||
|
||
/** | ||
* DependencyMismatchException. | ||
* | ||
* For exceptions related to dependency issues, like missing required dependency or recursive dependencies. | ||
* | ||
* @author André Roemcke <andre.romcke@ez.no> | ||
* | ||
* @since 2.8 | ||
*/ | ||
class DependencyMismatchException extends \RuntimeException | ||
{ | ||
/** | ||
* Constructor. | ||
* | ||
* @param string $msg The message; for recursion issues, missing required dependency, .. | ||
* @param array $stack The Bundle dependency stack trace up until the mismatch using bundle name or FQN | ||
* @param \Exception $previous The previous exception if there was one | ||
*/ | ||
public function __construct($msg, array $stack, \Exception $previous = null) | ||
{ | ||
parent::__construct( | ||
sprintf("%s, bundle dependencies stack trace: '%s'", $msg, var_export($stack, true)), | ||
0, | ||
$previous | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,10 +27,12 @@ | |
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpFoundation\Response; | ||
use Symfony\Component\HttpKernel\Bundle\BundleInterface; | ||
use Symfony\Component\HttpKernel\Bundle\BundleDependenciesInterface; | ||
use Symfony\Component\HttpKernel\Config\EnvParametersResource; | ||
use Symfony\Component\HttpKernel\Config\FileLocator; | ||
use Symfony\Component\HttpKernel\DependencyInjection\MergeExtensionConfigurationPass; | ||
use Symfony\Component\HttpKernel\DependencyInjection\AddClassesToCachePass; | ||
use Symfony\Component\HttpKernel\Exception\DependencyMismatchException; | ||
use Symfony\Component\Config\Loader\LoaderResolver; | ||
use Symfony\Component\Config\Loader\DelegatingLoader; | ||
use Symfony\Component\Config\ConfigCache; | ||
|
@@ -468,7 +470,7 @@ protected function initializeBundles() | |
$topMostBundles = array(); | ||
$directChildren = array(); | ||
|
||
foreach ($this->registerBundles() as $bundle) { | ||
foreach ($this->registeredDependencies() as $bundle) { | ||
$name = $bundle->getName(); | ||
if (isset($this->bundles[$name])) { | ||
throw new \LogicException(sprintf('Trying to register two bundles with the same name "%s"', $name)); | ||
|
@@ -514,6 +516,100 @@ protected function initializeBundles() | |
} | ||
} | ||
|
||
/** | ||
* Returns an array of bundles to register, with dependencies, ordered. | ||
* | ||
* @uses appendDependenciesRecursively | ||
* | ||
* @return BundleInterface[] An array of bundle instances. | ||
*/ | ||
protected function registeredDependencies() | ||
{ | ||
$bundles = $this->registerBundles(); | ||
$children = $rootBundles = array(); | ||
$hasDependencies = false; | ||
|
||
// Build up bundles as a hash with FQN for basis to rebuild again in correct order given dependencies | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Punctilious, but shouldn't it be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FQN as an expression already exists in PHP, other php libs, and in general: https://en.wikipedia.org/wiki/Fully_qualified_name |
||
foreach ($bundles as $bundle) { | ||
$bundleFQN = get_class($bundle); | ||
$children[$bundleFQN] = BundleDependenciesInterface::DEP_REQUIRED; | ||
$rootBundles[$bundleFQN] = $bundle; | ||
|
||
if ($bundle instanceof BundleDependenciesInterface) { | ||
$hasDependencies = true; | ||
} | ||
} | ||
|
||
if (!$hasDependencies) { | ||
return $bundles; | ||
} | ||
|
||
// Rebuild bundles order with dependencies recursively | ||
$bundles = array(); | ||
$stack = array(get_class($this) => true); | ||
$this->appendDependenciesRecursively( | ||
$children, | ||
$bundles, | ||
$rootBundles, | ||
$stack | ||
); | ||
|
||
return $bundles; | ||
} | ||
|
||
/** | ||
* Append dependencies of bundles recursively. | ||
* | ||
* Accepted arguments are as documented below where `string` implies a string with class FQN(Fully Qualified Name), | ||
* this string must be in exactly same format as returned by get_class() and PHP 5.5's CLASS constant. | ||
* Example of FQN string: "Symfony\Component\HttpKernel\Bundle\BundleInterface" | ||
* | ||
* @link BundleDependenciesInterface For further details on dependencies and DEP_* constants. | ||
* | ||
* @param mixed[string] $children Dependencies to apply, key FQN, value {@see BundleDependenciesInterface} constants | ||
* @param BundleInterface[string] $bundles Ordered bundles to append dependencies to | ||
* @param BundleInterface[string] $rootBundles Loaded Bundles from registerRootBundles() | ||
* @param bool[string] $stack For recursion protection, and for debug use on exceptions | ||
* | ||
* @throws \Symfony\Component\HttpKernel\Exception\DependencyMismatchException On missing dependencies | ||
*/ | ||
protected function appendDependenciesRecursively(array $children, array &$bundles, array $rootBundles, array $stack) | ||
{ | ||
foreach ($children as $dependencyFQN => $requiredFlag) { | ||
if (isset($bundles[$dependencyFQN])) { | ||
continue; | ||
} elseif (isset($stack[$dependencyFQN])) { | ||
throw new DependencyMismatchException(sprintf('Recursive dependency for "%s"', $dependencyFQN), array_keys($stack)); | ||
} | ||
|
||
// If already loaded root bundle, use that to not re instantiate | ||
if (isset($rootBundles[$dependencyFQN])) { | ||
$dependency = $rootBundles[$dependencyFQN]; | ||
} else { | ||
if (!class_exists($dependencyFQN)) { | ||
if ($requiredFlag === BundleDependenciesInterface::DEP_OPTIONAL) { | ||
continue; | ||
} | ||
throw new DependencyMismatchException(sprintf('Could not find "%s"', $dependencyFQN), array_keys($stack)); | ||
} | ||
$dependency = new $dependencyFQN(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Some bundles might have constructor parameters. For example: SonataUserBundle('FOSUserBundle') |
||
} | ||
|
||
// Append dependencies of the dependency before we append dependency itself | ||
if ($dependency instanceof BundleDependenciesInterface) { | ||
$stack[$dependencyFQN] = true; | ||
$this->appendDependenciesRecursively( | ||
$dependency->getBundleDependencies($this->environment, $this->debug), | ||
$bundles, | ||
$rootBundles, | ||
$stack | ||
); | ||
} | ||
|
||
$bundles[$dependencyFQN] = $dependency; | ||
} | ||
} | ||
|
||
/** | ||
* Gets the container class. | ||
* | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<?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\HttpKernel\Tests\Fixtures\BundleDependencies; | ||
|
||
use Symfony\Component\HttpKernel\Bundle\BundleDependenciesInterface; | ||
|
||
class BundleADependenciesNon implements BundleDependenciesInterface | ||
{ | ||
public function getBundleDependencies($environment, $debug) | ||
{ | ||
return array(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<?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\HttpKernel\Tests\Fixtures\BundleDependencies; | ||
|
||
use Symfony\Component\HttpKernel\Bundle\BundleDependenciesInterface; | ||
|
||
class BundleBDependenciesA implements BundleDependenciesInterface | ||
{ | ||
public function getBundleDependencies($environment, $debug) | ||
{ | ||
return array('Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies\BundleADependenciesNon' => self::DEP_REQUIRED); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
<?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\HttpKernel\Tests\Fixtures\BundleDependencies; | ||
|
||
use Symfony\Component\HttpKernel\Bundle\BundleDependenciesInterface; | ||
|
||
class BundleCDependenciesBA implements BundleDependenciesInterface | ||
{ | ||
public function getBundleDependencies($environment, $debug) | ||
{ | ||
if ('prod_test' === $environment) { | ||
return array(); | ||
} elseif ($debug) { | ||
return array('Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies\BundleADependenciesNon' => self::DEP_REQUIRED); | ||
} | ||
|
||
return array( | ||
'Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies\BundleBDependenciesA' => self::DEP_REQUIRED, | ||
'Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies\BundleADependenciesNon' => self::DEP_OPTIONAL, | ||
); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<?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\HttpKernel\Tests\Fixtures\BundleDependencies; | ||
|
||
use Symfony\Component\HttpKernel\Bundle\BundleDependenciesInterface; | ||
|
||
class BundleDDependenciesE implements BundleDependenciesInterface | ||
{ | ||
public function getBundleDependencies($environment, $debug) | ||
{ | ||
return array('Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies\BundleEDependenciesD' => self::DEP_REQUIRED); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
<?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\HttpKernel\Tests\Fixtures\BundleDependencies; | ||
|
||
use Symfony\Component\HttpKernel\Bundle\BundleDependenciesInterface; | ||
|
||
class BundleEDependenciesD implements BundleDependenciesInterface | ||
{ | ||
public function getBundleDependencies($environment, $debug) | ||
{ | ||
return array('Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies\BundleDDependenciesE' => self::DEP_REQUIRED); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't
DependentBundleInterface
a more convenient name?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe yes, naming.. Anyone else pro or against this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍 for
DependentBundleInterface
. It fits perfectly IMOThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Anything but what it currently is. ;-)
DependentBundleInterface
is better.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1 For
DependentBundleInterface