Skip to content

Commit c2f5851

Browse files
committed
[HttpKernel] Added support for registering Bundle dependencies
For further info on the new function and why it returns class names see BundleDependenciesInterface.
1 parent 7f745d7 commit c2f5851

12 files changed

+547
-14
lines changed
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\Bundle;
13+
14+
/**
15+
* Class BundleDependenciesInterface.
16+
*
17+
* Adds capability to let Bundles specify other bundles they need to load (before this one), both required and optional
18+
* dependencies.
19+
*
20+
* This allows you to define only the bundle you want to use in registerBundles(), but don't need to take care about
21+
* registering the dependencies it uses, and you won't need to make any changes in your kernel if those dependencies
22+
* change.
23+
*
24+
* NOTE: In this interface bundle dependencies are returned as FQN strings because several bundles (and root) might
25+
* register the same bundle. This means dependencies can not have arguments in its constructor, reflecting best practice
26+
* in Symfony of not having any logic in Bundle constructors given it is executed on every Kernel boot.
27+
*
28+
* NOTE2: This functionality is not a bundle plugin system, but rather a way to set your upstream dependencies,
29+
* or in the case of a distribution bundle, all bundles they are to be used with.
30+
*
31+
* @author André Roemcke <andre.romcke@ez.no>
32+
*
33+
* @since 2.8
34+
*
35+
* @api
36+
*/
37+
interface BundleDependenciesInterface
38+
{
39+
/**
40+
* @const Flag a Bundle Dependency as required, if missing throw exception
41+
*
42+
* @link \Symfony\Component\HttpKernel\Exception\DependencyMismatchException
43+
*/
44+
const DEP_REQUIRED = true;
45+
46+
/**
47+
* @const Flag a Bundle Dependency as optional, if missing silently ignore it
48+
*/
49+
const DEP_OPTIONAL = false;
50+
51+
/**
52+
* Returns an array of bundle dependencies Kernel should register on boot.
53+
*
54+
* Dependencies will be registered before current bundle, implying current bundle *MUST* be loaded after as it
55+
* for instance extends it.
56+
*
57+
* Example of use:
58+
* ```php
59+
* class AcmeBundle extends Bundle implements BundleDependenciesInterface
60+
* {
61+
* public function getBundleDependencies($environment, $debug)
62+
* {
63+
* $dependencies = array();
64+
*
65+
* // If you need to load some bundles only in dev using $environment (or in $debug)
66+
* if ($environment === 'dev') {
67+
* $dependencies['Egulias\SecurityDebugCommandBundle\EguliasSecurityDebugCommandBundle'] = self::DEP_REQUIRED;
68+
* }
69+
*
70+
* return $dependencies + array(
71+
* // Keys must be Fully Qualified Name (FQN) for the class to avoid bundles being loaded several times
72+
* // Note: Currently, use of DEP_OPTIONAL causes a *uncached* file system call on boot (every request)
73+
* // if dependency is missing, in a future versions this information might be cached.
74+
* 'FOS\HttpCacheBundle\FOSHttpCacheBundle' => self::DEP_OPTIONAL,
75+
*
76+
* // If you require PHP 5.5+ it is possible to use `::class` constant for required dependencies:
77+
* Oneup\FlysystemBundle\OneupFlysystemBundle::class => self::DEP_REQUIRED,
78+
* );
79+
* }
80+
* }
81+
* ```
82+
*
83+
* @param string $environment The current environment
84+
* @param bool $debug Whether to debugging is enabled or not
85+
*
86+
* @return mixed[string] An array where key is bundle class (FQN) names as strings, and value DEP_* constants
87+
*
88+
* @api
89+
*/
90+
public function getBundleDependencies($environment, $debug);
91+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\Exception;
13+
14+
/**
15+
* DependencyMismatchException.
16+
*
17+
* For exceptions related to dependency issues, like missing required dependency or recursive dependencies.
18+
*
19+
* @author André Roemcke <andre.romcke@ez.no>
20+
*
21+
* @since 2.8
22+
*/
23+
class DependencyMismatchException extends \RuntimeException
24+
{
25+
/**
26+
* Constructor.
27+
*
28+
* @param string $msg The message; for recursion issues, missing required dependency, ..
29+
* @param array $stack The Bundle dependency stack trace up until the mismatch using bundle name or FQN
30+
* @param \Exception $previous The previous exception if there was one
31+
*/
32+
public function __construct($msg, array $stack, \Exception $previous = null)
33+
{
34+
parent::__construct(
35+
sprintf("%s, bundle dependencies stack trace: '%s'", $msg, var_export($stack, true)),
36+
0,
37+
$previous
38+
);
39+
}
40+
}

src/Symfony/Component/HttpKernel/Kernel.php

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@
2727
use Symfony\Component\HttpFoundation\Request;
2828
use Symfony\Component\HttpFoundation\Response;
2929
use Symfony\Component\HttpKernel\Bundle\BundleInterface;
30+
use Symfony\Component\HttpKernel\Bundle\BundleDependenciesInterface;
3031
use Symfony\Component\HttpKernel\Config\EnvParametersResource;
3132
use Symfony\Component\HttpKernel\Config\FileLocator;
3233
use Symfony\Component\HttpKernel\DependencyInjection\MergeExtensionConfigurationPass;
3334
use Symfony\Component\HttpKernel\DependencyInjection\AddClassesToCachePass;
35+
use Symfony\Component\HttpKernel\Exception\DependencyMismatchException;
3436
use Symfony\Component\Config\Loader\LoaderResolver;
3537
use Symfony\Component\Config\Loader\DelegatingLoader;
3638
use Symfony\Component\Config\ConfigCache;
@@ -468,7 +470,7 @@ protected function initializeBundles()
468470
$topMostBundles = array();
469471
$directChildren = array();
470472

471-
foreach ($this->registerBundles() as $bundle) {
473+
foreach ($this->registeredDependencies() as $bundle) {
472474
$name = $bundle->getName();
473475
if (isset($this->bundles[$name])) {
474476
throw new \LogicException(sprintf('Trying to register two bundles with the same name "%s"', $name));
@@ -514,6 +516,100 @@ protected function initializeBundles()
514516
}
515517
}
516518

519+
/**
520+
* Returns an array of bundles to register, with dependencies, ordered.
521+
*
522+
* @uses appendDependenciesRecursively
523+
*
524+
* @return BundleInterface[] An array of bundle instances.
525+
*/
526+
protected function registeredDependencies()
527+
{
528+
$bundles = $this->registerBundles();
529+
$children = $rootBundles = array();
530+
$hasDependencies = false;
531+
532+
// Build up bundles as a hash with FQN for basis to rebuild again in correct order given dependencies
533+
foreach ($bundles as $bundle) {
534+
$bundleFQN = get_class($bundle);
535+
$children[$bundleFQN] = BundleDependenciesInterface::DEP_REQUIRED;
536+
$rootBundles[$bundleFQN] = $bundle;
537+
538+
if ($bundle instanceof BundleDependenciesInterface) {
539+
$hasDependencies = true;
540+
}
541+
}
542+
543+
if (!$hasDependencies) {
544+
return $bundles;
545+
}
546+
547+
// Rebuild bundles order with dependencies recursively
548+
$bundles = array();
549+
$stack = array(get_class($this) => true);
550+
$this->appendDependenciesRecursively(
551+
$children,
552+
$bundles,
553+
$rootBundles,
554+
$stack
555+
);
556+
557+
return $bundles;
558+
}
559+
560+
/**
561+
* Append dependencies of bundles recursively.
562+
*
563+
* Accepted arguments are as documented below where `string` implies a string with class FQN(Fully Qualified Name),
564+
* this string must be in exactly same format as returned by get_class() and PHP 5.5's CLASS constant.
565+
* Example of FQN string: "Symfony\Component\HttpKernel\Bundle\BundleInterface"
566+
*
567+
* @link BundleDependenciesInterface For further details on dependencies and DEP_* constants.
568+
*
569+
* @param mixed[string] $children Dependencies to apply, key FQN, value {@see BundleDependenciesInterface} constants
570+
* @param BundleInterface[string] $bundles Ordered bundles to append dependencies to
571+
* @param BundleInterface[string] $rootBundles Loaded Bundles from registerRootBundles()
572+
* @param bool[string] $stack For recursion protection, and for debug use on exceptions
573+
*
574+
* @throws \Symfony\Component\HttpKernel\Exception\DependencyMismatchException On missing dependencies
575+
*/
576+
protected function appendDependenciesRecursively(array $children, array &$bundles, array $rootBundles, array $stack)
577+
{
578+
foreach ($children as $dependencyFQN => $requiredFlag) {
579+
if (isset($bundles[$dependencyFQN])) {
580+
continue;
581+
} elseif (isset($stack[$dependencyFQN])) {
582+
throw new DependencyMismatchException(sprintf('Recursive dependency for "%s"', $dependencyFQN), array_keys($stack));
583+
}
584+
585+
// If already loaded root bundle, use that to not re instantiate
586+
if (isset($rootBundles[$dependencyFQN])) {
587+
$dependency = $rootBundles[$dependencyFQN];
588+
} else {
589+
if (!class_exists($dependencyFQN)) {
590+
if ($requiredFlag === BundleDependenciesInterface::DEP_OPTIONAL) {
591+
continue;
592+
}
593+
throw new DependencyMismatchException(sprintf('Could not find "%s"', $dependencyFQN), array_keys($stack));
594+
}
595+
$dependency = new $dependencyFQN();
596+
}
597+
598+
// Append dependencies of the dependency before we append dependency itself
599+
if ($dependency instanceof BundleDependenciesInterface) {
600+
$stack[$dependencyFQN] = true;
601+
$this->appendDependenciesRecursively(
602+
$dependency->getBundleDependencies($this->environment, $this->debug),
603+
$bundles,
604+
$rootBundles,
605+
$stack
606+
);
607+
}
608+
609+
$bundles[$dependencyFQN] = $dependency;
610+
}
611+
}
612+
517613
/**
518614
* Gets the container class.
519615
*
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies;
13+
14+
use Symfony\Component\HttpKernel\Bundle\BundleDependenciesInterface;
15+
16+
class BundleADependenciesNon implements BundleDependenciesInterface
17+
{
18+
public function getBundleDependencies($environment, $debug)
19+
{
20+
return array();
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies;
13+
14+
use Symfony\Component\HttpKernel\Bundle\BundleDependenciesInterface;
15+
16+
class BundleBDependenciesA implements BundleDependenciesInterface
17+
{
18+
public function getBundleDependencies($environment, $debug)
19+
{
20+
return array('Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies\BundleADependenciesNon' => self::DEP_REQUIRED);
21+
}
22+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies;
13+
14+
use Symfony\Component\HttpKernel\Bundle\BundleDependenciesInterface;
15+
16+
class BundleCDependenciesBA implements BundleDependenciesInterface
17+
{
18+
public function getBundleDependencies($environment, $debug)
19+
{
20+
if ('prod_test' === $environment) {
21+
return array();
22+
} elseif ($debug) {
23+
return array('Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies\BundleADependenciesNon' => self::DEP_REQUIRED);
24+
}
25+
26+
return array(
27+
'Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies\BundleBDependenciesA' => self::DEP_REQUIRED,
28+
'Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies\BundleADependenciesNon' => self::DEP_OPTIONAL,
29+
);
30+
}
31+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies;
13+
14+
use Symfony\Component\HttpKernel\Bundle\BundleDependenciesInterface;
15+
16+
class BundleDDependenciesE implements BundleDependenciesInterface
17+
{
18+
public function getBundleDependencies($environment, $debug)
19+
{
20+
return array('Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies\BundleEDependenciesD' => self::DEP_REQUIRED);
21+
}
22+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies;
13+
14+
use Symfony\Component\HttpKernel\Bundle\BundleDependenciesInterface;
15+
16+
class BundleEDependenciesD implements BundleDependenciesInterface
17+
{
18+
public function getBundleDependencies($environment, $debug)
19+
{
20+
return array('Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies\BundleDDependenciesE' => self::DEP_REQUIRED);
21+
}
22+
}

0 commit comments

Comments
 (0)