Skip to content

Commit c001a31

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 ce8d371 commit c001a31

File tree

11 files changed

+464
-12
lines changed

11 files changed

+464
-12
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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).
18+
*
19+
* This allows you to define only the bundle you want to use in registerBundles(), but don't need to take care about
20+
* registering the dependencies it uses, and you won't need to make any changes in your kernel if those dependencies
21+
* changes.
22+
*
23+
* In this interface bundle dependencies are returned as FQN strings because several bundles (and root) might register
24+
* the same bundle. This means dependencies can not have dependencies in its constructor, reflecting best practice in
25+
* Symfony of not having any logic in Bundle constructors given it is executed on every Kernel boot.
26+
*
27+
* @author André Roemcke <andre.romcke@ez.no>
28+
* @since 2.7
29+
*
30+
* @api
31+
*/
32+
interface BundleDependenciesInterface
33+
{
34+
/**
35+
* Returns an array of bundle dependencies Kernel should register on boot
36+
*
37+
* Dependencies will be registered before current bundle, implying current bundle *MUST* be loaded after as it
38+
* for instance extends it.
39+
*
40+
* Example of use:
41+
* ```php
42+
* class AcmeBundle extends Bundle implements BundleDependenciesInterface
43+
* {
44+
* public function getBundleDependencies()
45+
* {
46+
* return array(
47+
*
48+
* // All values must be FQN strings to avoid bundles being loaded several times
49+
* 'FOS\HttpCacheBundle\FOSHttpCacheBundle',
50+
*
51+
* // If you require PHP 5.5 or higher it is better to use `::CLASS` constant:
52+
* Oneup\FlysystemBundle\OneupFlysystemBundle::CLASS
53+
* );
54+
* }
55+
* }
56+
* ```
57+
*
58+
*
59+
* @return string[] An array of bundle class (FQN) names as strings.
60+
*
61+
* @api
62+
*/
63+
public function getBundleDependencies();
64+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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 or recursive dependencies.
18+
*
19+
* @author André Roemcke <andre.romcke@ez.no>
20+
* @since 2.7
21+
*/
22+
class DependencyMismatchException extends \RuntimeException
23+
{
24+
/**
25+
* Constructor.
26+
*
27+
* @param string $msg The message; for recursion issues, missing dependency, ..
28+
* @param array $stack The Bundle dependency stack trace up until the mismatch using bundle name or FQN
29+
* @param \Exception $previous The previous exception if there was one
30+
*/
31+
public function __construct($msg, array $stack, \Exception $previous = null)
32+
{
33+
parent::__construct(
34+
sprintf("%s, bundle dependencies stack trace: '%s'", $msg, var_export($stack, true)),
35+
0,
36+
$previous
37+
);
38+
}
39+
}

src/Symfony/Component/HttpKernel/Kernel.php

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@
2525
use Symfony\Component\HttpFoundation\Request;
2626
use Symfony\Component\HttpFoundation\Response;
2727
use Symfony\Component\HttpKernel\Bundle\BundleInterface;
28+
use Symfony\Component\HttpKernel\Bundle\BundleDependenciesInterface;
2829
use Symfony\Component\HttpKernel\Config\EnvParametersResource;
2930
use Symfony\Component\HttpKernel\Config\FileLocator;
3031
use Symfony\Component\HttpKernel\DependencyInjection\MergeExtensionConfigurationPass;
3132
use Symfony\Component\HttpKernel\DependencyInjection\AddClassesToCachePass;
33+
use Symfony\Component\HttpKernel\Exception\DependencyMismatchException;
3234
use Symfony\Component\Config\Loader\LoaderResolver;
3335
use Symfony\Component\Config\Loader\DelegatingLoader;
3436
use Symfony\Component\Config\ConfigCache;
@@ -252,7 +254,7 @@ public function getBundle($name, $first = true)
252254
}
253255

254256
/**
255-
* {@inheritDoc}
257+
* {@inheritdoc}
256258
*
257259
* @throws \RuntimeException if a custom resource is hidden by a resource in a derived bundle
258260
*/
@@ -459,7 +461,7 @@ protected function initializeBundles()
459461
$topMostBundles = array();
460462
$directChildren = array();
461463

462-
foreach ($this->registerBundles() as $bundle) {
464+
foreach ($this->registeredDependencies() as $bundle) {
463465
$name = $bundle->getName();
464466
if (isset($this->bundles[$name])) {
465467
throw new \LogicException(sprintf('Trying to register two bundles with the same name "%s"', $name));
@@ -505,6 +507,95 @@ protected function initializeBundles()
505507
}
506508
}
507509

510+
/**
511+
* Returns an array of bundles to register, with dependencies, ordered
512+
*
513+
* @uses appendDependenciesRecursively
514+
*
515+
* @return BundleInterface[] An array of bundle instances.
516+
*/
517+
protected function registeredDependencies()
518+
{
519+
$bundles = $this->registerBundles();
520+
$topMostBundles = $topMostBundlesMap = array();
521+
$hasDependencies = false;
522+
523+
// Build up bundles as a hash with FQN for basis to rebuild again in correct order given dependencies
524+
foreach ($bundles as $bundle) {
525+
$topMostBundles[] = $bundleFQN = get_class($bundle);
526+
$topMostBundlesMap[$bundleFQN] = $bundle;
527+
528+
if ($bundle instanceof BundleDependenciesInterface) {
529+
$hasDependencies = true;
530+
}
531+
}
532+
533+
if (!$hasDependencies) {
534+
return $bundles;
535+
}
536+
537+
// Rebuild bundles order with dependencies recursively
538+
$bundles = array();
539+
$stack = array(get_class($this) => true);
540+
$this->appendDependenciesRecursively(
541+
$topMostBundles,
542+
$bundles,
543+
$topMostBundlesMap,
544+
$stack
545+
);
546+
547+
return $bundles;
548+
}
549+
550+
/**
551+
* Append dependencies of bundles recursively
552+
*
553+
* Accepted arguments are as documented below where `string` implies a string with class FQN(Fully Qualified Name),
554+
* this string must be in exactly same format as returned by get_class() and PHP 5.5's CLASS constant.
555+
* Example of FQN string: "Symfony\Component\HttpKernel\Bundle\BundleInterface"
556+
*
557+
* @param string[] $directChildren Dependencies to apply
558+
* @param BundleInterface[string] $bundles Ordered bundles to append dependencies to
559+
* @param BundleInterface[string] $topMostBundlesMap Loaded Bundles from registerRootBundles()
560+
* @param bool[string] $stack For recursion protection, and for debug use on exceptions
561+
*
562+
* @throws \Symfony\Component\HttpKernel\Exception\DependencyMismatchException On missing dependencies
563+
*/
564+
protected function appendDependenciesRecursively(array $directChildren, array &$bundles, array $topMostBundlesMap, array $stack)
565+
{
566+
while (!empty($directChildren)) {
567+
$dependencyFQN = array_shift($directChildren);
568+
if (isset($bundles[$dependencyFQN])) {
569+
continue;
570+
} else if (isset($stack[$dependencyFQN])) {
571+
throw new DependencyMismatchException("Recursive dependency for '{$dependencyFQN}'", array_keys($stack));
572+
}
573+
574+
// If already loaded root bundle, use that to not re instantiate
575+
if (isset($topMostBundlesMap[$dependencyFQN])) {
576+
$dependency = $topMostBundlesMap[$dependencyFQN];
577+
} else {
578+
if (!class_exists($dependencyFQN)) {
579+
throw new DependencyMismatchException("Could not find '{$dependencyFQN}'", array_keys($stack));
580+
}
581+
$dependency = new $dependencyFQN();
582+
}
583+
584+
// Append dependencies of the dependency before we append dependency itself
585+
if ($dependency instanceof BundleDependenciesInterface) {
586+
$stack[$dependencyFQN] = true;
587+
$this->appendDependenciesRecursively(
588+
$dependency->getBundleDependencies(),
589+
$bundles,
590+
$topMostBundlesMap,
591+
$stack
592+
);
593+
}
594+
595+
$bundles[$dependencyFQN] = $dependency;
596+
}
597+
}
598+
508599
/**
509600
* Gets the container class.
510601
*
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()
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()
19+
{
20+
return array('Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies\BundleADependenciesNon');
21+
}
22+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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()
19+
{
20+
return array(
21+
'Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies\BundleBDependenciesA',
22+
'Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies\BundleADependenciesNon',
23+
);
24+
}
25+
}
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()
19+
{
20+
return array('Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies\BundleEDependenciesD');
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()
19+
{
20+
return array('Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies\BundleDDependenciesE');
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 BundleFDependenciesMissing implements BundleDependenciesInterface
17+
{
18+
public function getBundleDependencies()
19+
{
20+
return array('Symfony\Component\HttpKernel\Tests\Fixtures\BundleDependencies\BundleMissing');
21+
}
22+
}

src/Symfony/Component/HttpKernel/Tests/Fixtures/KernelForTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,9 @@ public function isBooted()
3434
{
3535
return $this->booted;
3636
}
37+
38+
public function registeredDependencies()
39+
{
40+
return parent::registeredDependencies();
41+
}
3742
}

0 commit comments

Comments
 (0)