-
-
Save tractorcow/8be54d6b019412c037049c8caddf39eb to your computer and use it in GitHub Desktop.
app-rfc-v3
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/* | |
Suggested AC: | |
- A kernel object exists, which can be easily overridden in user code, to control the boot process of any | |
silverstripe application. | |
- An application object exists, which can be easily overridden in user code, to control the execution of any | |
request in a silverstripe application. | |
- A class can have Injectable trait, which declares a `container` property which is the Injector instance. | |
If a class doesn't have Injectable, they cannot use Injector directly (note: most classes will) | |
- All components which make up the state of the Kernel object are available via Injector service, even | |
if these components are initialised pre-config (including the Kernel itself). | |
- Kernel has no static members. | |
- The only class with a static ::inst() method is Injector, and it's deprecated | |
- The only class with a static ::nest() method is Injector, and it's deprecated | |
- Config must be injected to any class which requires it via `Configurable` trait. | |
(note: most classes will) | |
- HTTPRequest is not a part of kernel, nor are any request-specific services. | |
- Services and instances related to a single request will now be non-static members of the HTTPRequest object, | |
These include Session, Cookies, and Requirements. | |
- Development experience should not be greatly adversely affected for classes which use the main core traits. | |
Critical AC: | |
- Injector::inst() will work with a deprecation notice. No core code that isn't itself deprecated will invoke this. | |
- If a non-current instance of Injector is ever invoked, it must raise a logic error to enforce application consistency. | |
*/ | |
namespace SilverStripe\Core; | |
// Begin: main.php | |
$kernel = new AppKernel(); | |
$request = HTTPRequest::createFromEnvironment(); | |
$app = new HTTPApplication($kernel); | |
$app->direct($request); | |
// End: of main.php | |
// Begin:: cli-script.php | |
$kernel = new AppKernel(); | |
$app = new CLIApplication($kernel); | |
$app->direct(); | |
// based on https://github.com/symfony/symfony/blob/3.4/src/Symfony/Component/HttpKernel/KernelInterface.php | |
interface Kernel | |
{ | |
public function boot(); | |
public function getInjector(); | |
public function setInjector(Injector $injector); | |
public function getEnvironment(); | |
} | |
/** | |
* Basic container for Kernel. Can be used or subclassed by user-code to set a custom Kernel | |
*/ | |
use SilverStripe\Config\Collections\ConfigCollectionInterface; | |
use SilverStripe\Core\Config\Config_ForClass; | |
class CoreKernel implements Kernel | |
{ | |
// basic getters / setters | |
protected $injector = null; | |
protected $environment = 'live'; | |
public function boot() | |
{ | |
// Does heavy work, e.g. start enumerating all classes, ensure config is cached, module list is enumerated | |
} | |
public function getInjector() | |
{ | |
return $this->injector; | |
} | |
public function setInjector(Injector $injector) | |
{ | |
// Ensure injector / kernel are mutually consistent | |
$this->injector = $injector; | |
$injector->registerService($this, Kernel::class); | |
return $this; | |
} | |
public function getEnvironment() | |
{ | |
return $this->environment; | |
} | |
} | |
/** | |
* SilverStripe specific code. | |
* Essentially the factory that replaces Core.php | |
*/ | |
class AppKernel extends CoreKernel | |
{ | |
public function __construct() | |
{ | |
// Initialises all services from core, but does not trigger any of them to boot | |
// See CoreKernel::boot(); | |
// Triggers mutual registration | |
$this->setInjector(new Injector()); | |
} | |
} | |
/** | |
* Core application interface. Does nothing (intentionally doesn't declare execution entry point, | |
* as each implementation will differ). | |
*/ | |
interface Application | |
{ | |
// Empty | |
} | |
/** | |
* App for HTTP requests | |
*/ | |
class HTTPApplication implements Application | |
{ | |
protected $kernel = null; | |
public function __construct($kernel) | |
{ | |
$this->kernel = $kernel; | |
} | |
public function direct(HTTPRequest $request) | |
{ | |
// Copy errorcontrailchain handling logic out of main.php | |
// Note: Finally have a way to create an errorcontrolchain-less application :) | |
$chain = new ErrorControlChain(); | |
$chain->then(function() use ($request) { | |
// Perform actual booting of kernel (heavy lifting) inside errorcontrolchain | |
$this->kernel->boot(); | |
$result = $this->routeRequest($this->kernel, $request); | |
$result->output(); | |
}); | |
} | |
/** | |
* Replacement of Director::test() | |
* Note: Example implementation only; We may create a new Application instead of using the current application. | |
* | |
* @param HTTPRequest $request | |
* @return HTTPResponse | |
*/ | |
public function test(HTTPRequest $request) | |
{ | |
// Deep clone | |
$kernel = clone $this->kernel; | |
try { | |
// @deprecated Update legacy Injector::inst() | |
$injector = Injector::nest(); | |
$kernel->setInjector($injector); | |
// Route | |
return $this->routeRequest($kernel, $request); | |
} finally { | |
// @deprecated revert legacy Injector::inst() | |
Injector::unnest(); | |
} | |
} | |
/** | |
* Replacement for Director::direct() | |
* | |
* @param HTTPrequest $request | |
* @return HTTPResponse | |
*/ | |
protected function routeRequest($kernel, $request) | |
{ | |
// Refactor Director::direct() into here, return response | |
} | |
} | |
/** | |
* Injector is only class left with legacy ::inst() method, but is marked deprecated (until 5.0). | |
*/ | |
class Injector { | |
/** | |
* To support legacy Injector::inst(); | |
* | |
* @deprecated | |
* @var static | |
*/ | |
protected static $inst = null; | |
/** | |
* @deprecated | |
* @return static | |
*/ | |
public static function inst() | |
{ | |
return self::$inst; | |
} | |
/** | |
* This code is important if we are mixing Injector::inst()->get() usage with $this->container->get() usage | |
* | |
* @important | |
* @deprecated | |
*/ | |
protected function checkInst() | |
{ | |
// Remove once we delete ::inst() access to container | |
if (static::$inst !== $this) { | |
throw new LogicException("Accessing incorrect container context"); | |
} | |
} | |
public function create($class) | |
{ | |
return $this->get($class); | |
} | |
public function get($class) | |
{ | |
// Should be called on all public methods | |
$this->checkInst(); | |
// Construct new class | |
$inst = new $class(); | |
// Set of pro-bono injections | |
// Quality of life enhancement; All Injectable classes get getContainer() for free | |
if (class_uses($class, Injectable::class)) { | |
$inst->setContainer($this); | |
} | |
// Quality of life enhancement; All Configurable classes get a getConfig() for free | |
if (class_uses($class, Configurable::class)) { | |
$inst->setConfig($this->get(ConfigCollectionInterface::class)); | |
} | |
return $class; | |
} | |
} | |
/** | |
* | |
* Trait Configurable | |
*/ | |
trait Configurable | |
{ | |
protected $collection; | |
/** | |
* Existing method updated to use injected config | |
* Note: No longer static!!! | |
* | |
* @return Config_ForClass | |
*/ | |
public function config() | |
{ | |
return Config::forClass($this->getConfig(), get_called_class()); | |
} | |
/** | |
* Returns global config | |
* | |
* @return ConfigCollectionInterface | |
*/ | |
public function getConfig() | |
{ | |
return $this->collection; | |
} | |
public function setConfig($collection) | |
{ | |
$this->collection = $collection; | |
} | |
} | |
/** | |
* Augment existing injectable trait with container logic | |
*/ | |
trait Injectable | |
{ | |
/** | |
* @var Injector | |
*/ | |
protected $container; | |
/** | |
* @param Injector $container | |
* @return $this | |
*/ | |
public function setContainer(Injector $container) | |
{ | |
$this->container = $container; | |
return $this; | |
} | |
/** | |
* @return Injector | |
*/ | |
public function getContainer() | |
{ | |
return $this->container; | |
} | |
/** | |
* $container added to first param | |
* | |
* @param Injector $container | |
* @param null $class | |
* @return static | |
*/ | |
public static function singleton(Injector $container, $class = null) | |
{ | |
if (!$class) { | |
$class = get_called_class(); | |
} | |
return $container->get($class); | |
} | |
/** | |
* $container added to first param | |
* | |
* @param Injector $container | |
* @param array ...$args | |
* @return mixed | |
*/ | |
public static function create(Injector $container, ...$args) | |
{ | |
$class = get_called_class(); | |
return $container->createWithArgs($class, $args); | |
} | |
} | |
/** | |
* Move HTTP specific state into request | |
* | |
* Note: HTTPRequest will eventually be immutable (psr-7), but probably not by 4.0 | |
*/ | |
class HTTPRequest | |
{ | |
public static function createFromEnvironment() | |
{ | |
// Copied logic from main.php | |
return new static(); | |
} | |
public function getSession() | |
{ | |
return $this->session; | |
} | |
public function getCookies() | |
{ | |
return $this->cookies; | |
} | |
} | |
class HTTPResponse | |
{ | |
public function __construct(HTTPRequest $request) | |
{ | |
$this->request = $request; | |
} | |
/** | |
* Response remembers the request it was generated for | |
*/ | |
public function getRequest() | |
{ | |
return $this->request; | |
} | |
} | |
/** | |
* Example injectable object | |
*/ | |
class SomeObject | |
{ | |
use Injectable; | |
use Configurable; | |
// Example for how an object must explicitly request a kernel to access it's methods | |
// Injectable / Configurable provide getContainer() and getConfig() for free, but other | |
// services can be injected here. | |
// Can also be done via yaml, but just inline here as an example | |
private static $dependencies = [ | |
'Kernel' => '%$'.Kernel::class | |
]; | |
protected $kernel; | |
public function getKernel() | |
{ | |
return $this->kernel; | |
} | |
public function setKernel(Kernel $kernel) | |
{ | |
$this->kernel = $kernel; | |
return $this; | |
} | |
/** | |
* Example user-code | |
* | |
* @param HTTPRequest $request | |
* @return HTTPResponse | |
*/ | |
public function handleRequest(HTTPRequest $request) | |
{ | |
// Example request handling | |
// Note how $request is simple passed, and not saved in state anywhere | |
// Any object which requires access to Session / Cookies will need to be passed this object now, | |
// instead of relying on global statics | |
// Replace Config::inst()->get(OtherObject::class, 'enabled')) | |
if ($this->getConfig()->get(OtherObject::class, 'enabled')) { | |
// ::singleton() / ::create() method now requires a container as first parameter | |
$subController = OtherObject::create($this->getContainer()); | |
$result = $subController->handleRequest($request); | |
} | |
// Note: $this->config() still works, but only on instances | |
elseif ($this->config()->get('enabled')) { | |
$result = new HTTPResponse($request); | |
} | |
else { | |
$result = null; | |
} | |
// Replaces Session::set() | |
$request->getSession()->set('OK', 1); | |
// Replaces Cookie::set() | |
$request->getCookiest()->set('OK', 1); | |
// Replace Director::get_environment_type() | |
if ($this->getKernel()->getEnvironment() === 'dev') { | |
Debug::message('dev message'); | |
} | |
return $result; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment