Skip to content

[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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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

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?

Copy link
Contributor Author

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?

Copy link
Contributor

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 IMO

Copy link
Contributor

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 For DependentBundleInterface

{
/**
* @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;
Copy link
Contributor

Choose a reason for hiding this comment

The 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

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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
);
}
}
98 changes: 97 additions & 1 deletion src/Symfony/Component/HttpKernel/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

Punctilious, but shouldn't it be FQCN instead of FQN ? 😺

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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();
Copy link
Contributor

Choose a reason for hiding this comment

The 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.
*
Expand Down
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);
}
}
Loading