API Implement kernel nesting

This commit is contained in:
Damian Mooyman 2017-06-13 17:23:21 +12:00
parent fc7188da7d
commit 2f6336a15b
13 changed files with 265 additions and 76 deletions

View File

@ -188,7 +188,6 @@ class Director implements TemplateGlobalProvider
$cookies = array(), $cookies = array(),
&$request = null &$request = null
) { ) {
Config::nest(); Config::nest();
Injector::nest(); Injector::nest();

View File

@ -13,6 +13,7 @@ use SilverStripe\Core\Cache\ManifestCacheFactory;
use SilverStripe\Core\Config\ConfigLoader; use SilverStripe\Core\Config\ConfigLoader;
use SilverStripe\Core\Config\CoreConfigFactory; use SilverStripe\Core\Config\CoreConfigFactory;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Injector\InjectorLoader;
use SilverStripe\Core\Injector\SilverStripeServiceConfigurationLocator; use SilverStripe\Core\Injector\SilverStripeServiceConfigurationLocator;
use SilverStripe\Core\Manifest\ClassLoader; use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Manifest\ClassManifest; use SilverStripe\Core\Manifest\ClassManifest;
@ -38,9 +39,10 @@ class AppKernel extends CoreKernel
// Initialise the dependency injector as soon as possible, as it is // Initialise the dependency injector as soon as possible, as it is
// subsequently used by some of the following code // subsequently used by some of the following code
$injectorLoader = InjectorLoader::inst();
$injector = new Injector(array('locator' => SilverStripeServiceConfigurationLocator::class)); $injector = new Injector(array('locator' => SilverStripeServiceConfigurationLocator::class));
$this->setContainer($injector); $injectorLoader->pushManifest($injector);
Injector::set_inst($injector); $this->setInjectorLoader($injectorLoader);
// Manifest cache factory // Manifest cache factory
$manifestCacheFactory = $this->buildManifestCacheFactory(); $manifestCacheFactory = $this->buildManifestCacheFactory();

View File

@ -87,7 +87,7 @@ abstract class Config
{ {
// Unnest unless we would be left at 0 manifests // Unnest unless we would be left at 0 manifests
$loader = ConfigLoader::inst(); $loader = ConfigLoader::inst();
if ($loader->countManifests() < 2) { if ($loader->countManifests() <= 1) {
user_error( user_error(
"Unable to unnest root Config, please make sure you don't have mis-matched nest/unnest", "Unable to unnest root Config, please make sure you don't have mis-matched nest/unnest",
E_USER_WARNING E_USER_WARNING

View File

@ -82,14 +82,31 @@ class ConfigLoader
} }
/** /**
* Nest the current manifest * Nest the config loader and activates it
* *
* @return ConfigCollectionInterface * @return static
*/ */
public function nest() public function nest()
{ {
$manifest = $this->getManifest()->nest(); // Nest config
$this->pushManifest($manifest); $manifest = clone $this->getManifest();
return $manifest;
// 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;
} }
} }

View File

@ -5,6 +5,7 @@ namespace SilverStripe\Core;
use InvalidArgumentException; use InvalidArgumentException;
use SilverStripe\Core\Config\ConfigLoader; use SilverStripe\Core\Config\ConfigLoader;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Injector\InjectorLoader;
use SilverStripe\Core\Manifest\ClassLoader; use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Manifest\ModuleLoader; use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\View\ThemeResourceLoader; use SilverStripe\View\ThemeResourceLoader;
@ -14,6 +15,11 @@ use SilverStripe\View\ThemeResourceLoader;
*/ */
class CoreKernel implements Kernel class CoreKernel implements Kernel
{ {
/**
* @var Kernel
*/
protected $nestedFrom = null;
/** /**
* @var Injector * @var Injector
*/ */
@ -39,6 +45,11 @@ class CoreKernel implements Kernel
*/ */
protected $configLoader = null; protected $configLoader = null;
/**
* @var InjectorLoader
*/
protected $injectorLoader = null;
/** /**
* @var ThemeResourceLoader * @var ThemeResourceLoader
*/ */
@ -54,21 +65,44 @@ class CoreKernel implements Kernel
public function nest() 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() public function getContainer()
{ {
return $this->container; return $this->getInjectorLoader()->getManifest();
} }
public function setContainer(Injector $container) public function setInjectorLoader(InjectorLoader $injectorLoader)
{ {
$this->container = $container; $this->injectorLoader = $injectorLoader;
$container->registerService($this, Kernel::class); $injectorLoader
->getManifest()
->registerService($this, Kernel::class);
return $this; return $this;
} }
public function getInjectorLoader()
{
return $this->injectorLoader;
}
public function getClassLoader() public function getClassLoader()
{ {
return $this->classLoader; return $this->classLoader;

View File

@ -106,10 +106,14 @@ class HTTPApplication implements Application
*/ */
public function execute(callable $callback) public function execute(callable $callback)
{ {
return $this->callMiddleware(function () use ($callback) { try {
// Pre-request boot return $this->callMiddleware(function () use ($callback) {
$this->getKernel()->boot(); // Pre-request boot
return call_user_func($callback); $this->getKernel()->boot();
}); return call_user_func($callback);
});
} finally {
$this->getKernel()->shutdown();
}
} }
} }

View File

@ -236,18 +236,7 @@ class Injector implements ContainerInterface
*/ */
public static function inst() public static function inst()
{ {
return self::$instance; return InjectorLoader::inst()->getManifest();
}
/**
* 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;
} }
/** /**
@ -262,11 +251,10 @@ class Injector implements ContainerInterface
*/ */
public static function nest() public static function nest()
{ {
$current = self::$instance; // Clone current injector and nest
$new = clone self::inst();
$new = clone $current; InjectorLoader::inst()->pushManifest($new);
$new->nestedFrom = $current; return $new;
return self::set_inst($new);
} }
/** /**
@ -277,15 +265,17 @@ class Injector implements ContainerInterface
*/ */
public static function unnest() public static function unnest()
{ {
if (self::inst()->nestedFrom) { // Unnest unless we would be left at 0 manifests
self::set_inst(self::inst()->nestedFrom); $loader = InjectorLoader::inst();
} else { if ($loader->countManifests() <= 1) {
user_error( user_error(
"Unable to unnest root Injector, please make sure you don't have mis-matched nest/unnest", "Unable to unnest root Injector, please make sure you don't have mis-matched nest/unnest",
E_USER_WARNING E_USER_WARNING
); );
} else {
$loader->popManifest();
} }
return self::inst(); return static::inst();
} }
/** /**

View File

@ -0,0 +1,109 @@
<?php
namespace SilverStripe\Core\Injector;
use BadMethodCallException;
/**
* Registers chained injectors
*/
class InjectorLoader
{
/**
* @internal
* @var self
*/
private static $instance;
/**
* @var Injector[] map of injector instances
*/
protected $manifests = array();
/**
* @return self
*/
public static function inst()
{
return self::$instance ? self::$instance : self::$instance = new self();
}
/**
* Returns the currently active class manifest instance that is used for
* loading classes.
*
* @return Injector
*/
public function getManifest()
{
if (empty($this->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;
}
}

View File

@ -4,6 +4,7 @@ namespace SilverStripe\Core;
use SilverStripe\Core\Config\ConfigLoader; use SilverStripe\Core\Config\ConfigLoader;
use SilverStripe\Core\Injector\Injector; use SilverStripe\Core\Injector\Injector;
use SilverStripe\Core\Injector\InjectorLoader;
use SilverStripe\Core\Manifest\ClassLoader; use SilverStripe\Core\Manifest\ClassLoader;
use SilverStripe\Core\Manifest\ModuleLoader; use SilverStripe\Core\Manifest\ModuleLoader;
use SilverStripe\View\ThemeResourceLoader; use SilverStripe\View\ThemeResourceLoader;
@ -42,23 +43,22 @@ interface Kernel
/** /**
* Nests this kernel, all components, and returns the nested value. * Nests this kernel, all components, and returns the nested value.
* *
* @return static * @return Kernel
*/ */
public function nest(); public function nest();
/**
* Ensures this kernel is the active kernel after or during nesting
*
* @return $this
*/
public function activate();
/** /**
* @return Injector * @return Injector
*/ */
public function getContainer(); public function getContainer();
/**
* Sets injector
*
* @param Injector $container
* @return $this
*/
public function setContainer(Injector $container);
/** /**
* @return ClassLoader * @return ClassLoader
*/ */
@ -70,6 +70,19 @@ interface Kernel
*/ */
public function setClassLoader(ClassLoader $classLoader); 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 * @return ModuleLoader
*/ */

View File

@ -35,4 +35,10 @@ class TestKernel extends AppKernel
{ {
return true; return true;
} }
protected function bootErrorHandling()
{
// Leave phpunit to capture errors
restore_error_handler();
}
} }

View File

@ -37,7 +37,9 @@ class ExtensionTestState implements TestState
public function setUpOnce($class) public function setUpOnce($class)
{ {
$isAltered = false; // May be altered by another class
$isAltered = $this->extensionsToReapply || $this->extensionsToRemove;
/** @var string|SapphireTest $class */ /** @var string|SapphireTest $class */
/** @var string|DataObject $dataClass */ /** @var string|DataObject $dataClass */
// Remove any illegal extensions that are present // 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. // clear singletons, they're caching old extension info
if ($isAltered || $this->extensionsToReapply || $this->extensionsToRemove || $class::getExtraDataObjects()) { // 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(); DataObject::reset();
if (!SapphireTest::using_temp_db()) { if (!SapphireTest::using_temp_db()) {
SapphireTest::create_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) public function tearDownOnce($class)

View File

@ -203,11 +203,11 @@ class SapphireTest extends PHPUnit_Framework_TestCase
*/ */
protected function setUp() protected function setUp()
{ {
// Reset state
self::$kernel->reset(); self::$kernel->reset();
//nest config and injector for each test so they are effectively sandboxed per test // Nest
Config::nest(); self::$kernel->nest();
Injector::nest();
// Call state helpers // Call state helpers
static::$state->setUp($this); static::$state->setUp($this);
@ -299,12 +299,20 @@ class SapphireTest extends PHPUnit_Framework_TestCase
// Reset kernel // Reset kernel
static::$kernel->reset(); static::$kernel->reset();
//nest config and injector for each suite so they are effectively sandboxed // Nest kernel
Config::nest(); static::$kernel->nest();
Injector::nest();
// Call state helpers // Call state helpers
static::$state->setUpOnce(static::class); 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 // Call state helpers
static::$state->tearDownOnce(static::class); static::$state->tearDownOnce(static::class);
//unnest injector / config now that the test suite is over // Unnest
// this will reset all the extensions on the object too (see setUpBeforeClass) static::$kernel->activate();
Injector::unnest();
Config::unnest();
static::resetDBSchema();
// Reset PHP state
static::$kernel->reset(); static::$kernel->reset();
// Reset DB schema
static::resetDBSchema();
} }
/** /**
@ -485,11 +493,10 @@ class SapphireTest extends PHPUnit_Framework_TestCase
} }
// Call state helpers // Call state helpers
static::$state->setUp($this); static::$state->tearDown($this);
//unnest injector / config now that tests are over // Unnest
Injector::unnest(); self::$kernel->activate();
Config::unnest();
// Reset state // Reset state
self::$kernel->reset(); 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() public static function create_temp_db()
{ {
// Disable PHPUnit error handling // Disable PHPUnit error handling

View File

@ -53,10 +53,10 @@ class TestSession
public function __construct() public function __construct()
{ {
$this->session = Injector::inst()->create('SilverStripe\\Control\\Session', array()); $this->session = Injector::inst()->create(Session::class, array());
$this->cookies = Injector::inst()->create('SilverStripe\\Control\\Cookie_Backend'); $this->cookies = Injector::inst()->create(Cookie_Backend::class);
$this->controller = new Controller(); $this->controller = new Controller();
$this->controller->setSession($this->session); // @todo - Ensure $this->session is set on all requests
$this->controller->pushCurrent(); $this->controller->pushCurrent();
} }