diff --git a/src/Control/Director.php b/src/Control/Director.php index 05ca57f2f..ba61cfbd3 100644 --- a/src/Control/Director.php +++ b/src/Control/Director.php @@ -188,7 +188,6 @@ class Director implements TemplateGlobalProvider $cookies = array(), &$request = null ) { - Config::nest(); Injector::nest(); diff --git a/src/Core/AppKernel.php b/src/Core/AppKernel.php index 792ceafd6..0002a7b7f 100644 --- a/src/Core/AppKernel.php +++ b/src/Core/AppKernel.php @@ -13,6 +13,7 @@ use SilverStripe\Core\Cache\ManifestCacheFactory; use SilverStripe\Core\Config\ConfigLoader; use SilverStripe\Core\Config\CoreConfigFactory; use SilverStripe\Core\Injector\Injector; +use SilverStripe\Core\Injector\InjectorLoader; use SilverStripe\Core\Injector\SilverStripeServiceConfigurationLocator; use SilverStripe\Core\Manifest\ClassLoader; use SilverStripe\Core\Manifest\ClassManifest; @@ -38,9 +39,10 @@ class AppKernel extends CoreKernel // Initialise the dependency injector as soon as possible, as it is // subsequently used by some of the following code + $injectorLoader = InjectorLoader::inst(); $injector = new Injector(array('locator' => SilverStripeServiceConfigurationLocator::class)); - $this->setContainer($injector); - Injector::set_inst($injector); + $injectorLoader->pushManifest($injector); + $this->setInjectorLoader($injectorLoader); // Manifest cache factory $manifestCacheFactory = $this->buildManifestCacheFactory(); diff --git a/src/Core/Config/Config.php b/src/Core/Config/Config.php index 28a530446..15f1c127d 100644 --- a/src/Core/Config/Config.php +++ b/src/Core/Config/Config.php @@ -87,7 +87,7 @@ abstract class Config { // Unnest unless we would be left at 0 manifests $loader = ConfigLoader::inst(); - if ($loader->countManifests() < 2) { + if ($loader->countManifests() <= 1) { user_error( "Unable to unnest root Config, please make sure you don't have mis-matched nest/unnest", E_USER_WARNING diff --git a/src/Core/Config/ConfigLoader.php b/src/Core/Config/ConfigLoader.php index ec39262d2..b5ca19d12 100644 --- a/src/Core/Config/ConfigLoader.php +++ b/src/Core/Config/ConfigLoader.php @@ -82,14 +82,31 @@ class ConfigLoader } /** - * Nest the current manifest + * Nest the config loader and activates it * - * @return ConfigCollectionInterface + * @return static */ public function nest() { - $manifest = $this->getManifest()->nest(); - $this->pushManifest($manifest); - return $manifest; + // Nest config + $manifest = clone $this->getManifest(); + + // Create new blank loader with new stack (top level nesting) + $newLoader = new self; + $newLoader->pushManifest($manifest); + + // Activate new loader + return $newLoader->activate(); + } + + /** + * Mark this instance as the current instance + * + * @return $this + */ + public function activate() + { + static::$instance = $this; + return $this; } } diff --git a/src/Core/CoreKernel.php b/src/Core/CoreKernel.php index 4509c863a..7e81e436c 100644 --- a/src/Core/CoreKernel.php +++ b/src/Core/CoreKernel.php @@ -5,6 +5,7 @@ namespace SilverStripe\Core; use InvalidArgumentException; use SilverStripe\Core\Config\ConfigLoader; use SilverStripe\Core\Injector\Injector; +use SilverStripe\Core\Injector\InjectorLoader; use SilverStripe\Core\Manifest\ClassLoader; use SilverStripe\Core\Manifest\ModuleLoader; use SilverStripe\View\ThemeResourceLoader; @@ -14,6 +15,11 @@ use SilverStripe\View\ThemeResourceLoader; */ class CoreKernel implements Kernel { + /** + * @var Kernel + */ + protected $nestedFrom = null; + /** * @var Injector */ @@ -39,6 +45,11 @@ class CoreKernel implements Kernel */ protected $configLoader = null; + /** + * @var InjectorLoader + */ + protected $injectorLoader = null; + /** * @var ThemeResourceLoader */ @@ -54,21 +65,44 @@ class CoreKernel implements Kernel public function nest() { - // TODO: Implement nest() method. + // Clone this kernel, nesting config / injector manifest containers + $kernel = clone $this; + $kernel->setConfigLoader($this->configLoader->nest()); + $kernel->setInjectorLoader($this->injectorLoader->nest()); + return $kernel; + } + + public function activate() + { + $this->configLoader->activate(); + $this->injectorLoader->activate(); + return $this; + } + + public function getNestedFrom() + { + return $this->nestedFrom; } public function getContainer() { - return $this->container; + return $this->getInjectorLoader()->getManifest(); } - public function setContainer(Injector $container) + public function setInjectorLoader(InjectorLoader $injectorLoader) { - $this->container = $container; - $container->registerService($this, Kernel::class); + $this->injectorLoader = $injectorLoader; + $injectorLoader + ->getManifest() + ->registerService($this, Kernel::class); return $this; } + public function getInjectorLoader() + { + return $this->injectorLoader; + } + public function getClassLoader() { return $this->classLoader; diff --git a/src/Core/HTTPApplication.php b/src/Core/HTTPApplication.php index abd24d5ad..ff1bcbac7 100644 --- a/src/Core/HTTPApplication.php +++ b/src/Core/HTTPApplication.php @@ -106,10 +106,14 @@ class HTTPApplication implements Application */ public function execute(callable $callback) { - return $this->callMiddleware(function () use ($callback) { - // Pre-request boot - $this->getKernel()->boot(); - return call_user_func($callback); - }); + try { + return $this->callMiddleware(function () use ($callback) { + // Pre-request boot + $this->getKernel()->boot(); + return call_user_func($callback); + }); + } finally { + $this->getKernel()->shutdown(); + } } } diff --git a/src/Core/Injector/Injector.php b/src/Core/Injector/Injector.php index 52b76c7c7..6b3f28abd 100644 --- a/src/Core/Injector/Injector.php +++ b/src/Core/Injector/Injector.php @@ -236,18 +236,7 @@ class Injector implements ContainerInterface */ public static function inst() { - return self::$instance; - } - - /** - * Sets the default global injector instance. - * - * @param Injector $instance - * @return Injector Reference to new active Injector instance - */ - public static function set_inst(Injector $instance) - { - return self::$instance = $instance; + return InjectorLoader::inst()->getManifest(); } /** @@ -262,11 +251,10 @@ class Injector implements ContainerInterface */ public static function nest() { - $current = self::$instance; - - $new = clone $current; - $new->nestedFrom = $current; - return self::set_inst($new); + // Clone current injector and nest + $new = clone self::inst(); + InjectorLoader::inst()->pushManifest($new); + return $new; } /** @@ -277,15 +265,17 @@ class Injector implements ContainerInterface */ public static function unnest() { - if (self::inst()->nestedFrom) { - self::set_inst(self::inst()->nestedFrom); - } else { + // Unnest unless we would be left at 0 manifests + $loader = InjectorLoader::inst(); + if ($loader->countManifests() <= 1) { user_error( "Unable to unnest root Injector, please make sure you don't have mis-matched nest/unnest", E_USER_WARNING ); + } else { + $loader->popManifest(); } - return self::inst(); + return static::inst(); } /** diff --git a/src/Core/Injector/InjectorLoader.php b/src/Core/Injector/InjectorLoader.php new file mode 100644 index 000000000..fbba4b37b --- /dev/null +++ b/src/Core/Injector/InjectorLoader.php @@ -0,0 +1,109 @@ +manifests)) { + throw new BadMethodCallException("No injector manifests available"); + } + return $this->manifests[count($this->manifests) - 1]; + } + + /** + * Returns true if this class loader has a manifest. + * + * @return bool + */ + public function hasManifest() + { + return (bool)$this->manifests; + } + + /** + * Pushes a class manifest instance onto the top of the stack. + * + * @param Injector $manifest + */ + public function pushManifest(Injector $manifest) + { + $this->manifests[] = $manifest; + } + + /** + * @return Injector + */ + public function popManifest() + { + return array_pop($this->manifests); + } + + /** + * Check number of manifests + * + * @return int + */ + public function countManifests() + { + return count($this->manifests); + } + + /** + * Nest the config loader + * + * @return static + */ + public function nest() + { + // Nest config + $manifest = $this->getManifest()->nest(); + + // Create new blank loader with new stack (top level nesting) + $newLoader = new self; + $newLoader->pushManifest($manifest); + + // Activate new loader + $newLoader->activate(); + return $newLoader; + } + + /** + * Mark this instance as the current instance + */ + public function activate() + { + static::$instance = $this; + } +} diff --git a/src/Core/Kernel.php b/src/Core/Kernel.php index 7b7d979f8..6541fc8d9 100644 --- a/src/Core/Kernel.php +++ b/src/Core/Kernel.php @@ -4,6 +4,7 @@ namespace SilverStripe\Core; use SilverStripe\Core\Config\ConfigLoader; use SilverStripe\Core\Injector\Injector; +use SilverStripe\Core\Injector\InjectorLoader; use SilverStripe\Core\Manifest\ClassLoader; use SilverStripe\Core\Manifest\ModuleLoader; use SilverStripe\View\ThemeResourceLoader; @@ -42,23 +43,22 @@ interface Kernel /** * Nests this kernel, all components, and returns the nested value. * - * @return static + * @return Kernel */ public function nest(); + /** + * Ensures this kernel is the active kernel after or during nesting + * + * @return $this + */ + public function activate(); + /** * @return Injector */ public function getContainer(); - /** - * Sets injector - * - * @param Injector $container - * @return $this - */ - public function setContainer(Injector $container); - /** * @return ClassLoader */ @@ -70,6 +70,19 @@ interface Kernel */ public function setClassLoader(ClassLoader $classLoader); + /** + * Get loader for injector instance + * + * @return InjectorLoader + */ + public function getInjectorLoader(); + + /** + * @param InjectorLoader $injectorLoader + * @return $this + */ + public function setInjectorLoader(InjectorLoader $injectorLoader); + /** * @return ModuleLoader */ diff --git a/src/Core/TestKernel.php b/src/Core/TestKernel.php index b19551463..3fa8091ed 100644 --- a/src/Core/TestKernel.php +++ b/src/Core/TestKernel.php @@ -35,4 +35,10 @@ class TestKernel extends AppKernel { return true; } + + protected function bootErrorHandling() + { + // Leave phpunit to capture errors + restore_error_handler(); + } } diff --git a/src/Dev/ExtensionTestState.php b/src/Dev/ExtensionTestState.php index acd7c5c1e..5df311a04 100644 --- a/src/Dev/ExtensionTestState.php +++ b/src/Dev/ExtensionTestState.php @@ -37,7 +37,9 @@ class ExtensionTestState implements TestState public function setUpOnce($class) { - $isAltered = false; + // May be altered by another class + $isAltered = $this->extensionsToReapply || $this->extensionsToRemove; + /** @var string|SapphireTest $class */ /** @var string|DataObject $dataClass */ // Remove any illegal extensions that are present @@ -84,18 +86,19 @@ class ExtensionTestState implements TestState } } - // If we have made changes to the extensions present, then migrate the database schema. - if ($isAltered || $this->extensionsToReapply || $this->extensionsToRemove || $class::getExtraDataObjects()) { + // clear singletons, they're caching old extension info + // which is used in DatabaseAdmin->doBuild() + Injector::inst()->unregisterObjects(DataObject::class); + + // If we have altered the schema, but SapphireTest::setUpBeforeClass() would not otherwise + // reset the schema (if there were extra objects) then force a reset + if ($isAltered && empty($class::getExtraDataObjects())) { DataObject::reset(); if (!SapphireTest::using_temp_db()) { SapphireTest::create_temp_db(); } - SapphireTest::resetDBSchema(true); + $class::resetDBSchema(true); } - - // clear singletons, they're caching old extension info - // which is used in DatabaseAdmin->doBuild() - Injector::inst()->unregisterObjects(DataObject::class); } public function tearDownOnce($class) diff --git a/src/Dev/SapphireTest.php b/src/Dev/SapphireTest.php index 910ad4cce..e72f6747a 100644 --- a/src/Dev/SapphireTest.php +++ b/src/Dev/SapphireTest.php @@ -203,11 +203,11 @@ class SapphireTest extends PHPUnit_Framework_TestCase */ protected function setUp() { + // Reset state self::$kernel->reset(); - //nest config and injector for each test so they are effectively sandboxed per test - Config::nest(); - Injector::nest(); + // Nest + self::$kernel->nest(); // Call state helpers static::$state->setUp($this); @@ -299,12 +299,20 @@ class SapphireTest extends PHPUnit_Framework_TestCase // Reset kernel static::$kernel->reset(); - //nest config and injector for each suite so they are effectively sandboxed - Config::nest(); - Injector::nest(); + // Nest kernel + static::$kernel->nest(); // Call state helpers static::$state->setUpOnce(static::class); + + // Build DB if we have objects + if (static::getExtraDataObjects()) { + DataObject::reset(); + if (!self::using_temp_db()) { + self::create_temp_db(); + } + static::resetDBSchema(true); + } } /** @@ -322,14 +330,14 @@ class SapphireTest extends PHPUnit_Framework_TestCase // Call state helpers static::$state->tearDownOnce(static::class); - //unnest injector / config now that the test suite is over - // this will reset all the extensions on the object too (see setUpBeforeClass) - Injector::unnest(); - Config::unnest(); - - static::resetDBSchema(); + // Unnest + static::$kernel->activate(); + // Reset PHP state static::$kernel->reset(); + + // Reset DB schema + static::resetDBSchema(); } /** @@ -485,11 +493,10 @@ class SapphireTest extends PHPUnit_Framework_TestCase } // Call state helpers - static::$state->setUp($this); + static::$state->tearDown($this); - //unnest injector / config now that tests are over - Injector::unnest(); - Config::unnest(); + // Unnest + self::$kernel->activate(); // Reset state self::$kernel->reset(); @@ -992,6 +999,11 @@ class SapphireTest extends PHPUnit_Framework_TestCase } } + /** + * Create temp DB without creating extra objects + * + * @return string + */ public static function create_temp_db() { // Disable PHPUnit error handling diff --git a/src/Dev/TestSession.php b/src/Dev/TestSession.php index 22d7042cc..794c3b45d 100644 --- a/src/Dev/TestSession.php +++ b/src/Dev/TestSession.php @@ -53,10 +53,10 @@ class TestSession public function __construct() { - $this->session = Injector::inst()->create('SilverStripe\\Control\\Session', array()); - $this->cookies = Injector::inst()->create('SilverStripe\\Control\\Cookie_Backend'); + $this->session = Injector::inst()->create(Session::class, array()); + $this->cookies = Injector::inst()->create(Cookie_Backend::class); $this->controller = new Controller(); - $this->controller->setSession($this->session); + // @todo - Ensure $this->session is set on all requests $this->controller->pushCurrent(); }