Skip to content

Instantly share code, notes, and snippets.

@tractorcow
Last active June 1, 2017 22:18
Show Gist options
  • Save tractorcow/2bb76d12074b885b9b2923dafa07ae88 to your computer and use it in GitHub Desktop.
Save tractorcow/2bb76d12074b885b9b2923dafa07ae88 to your computer and use it in GitHub Desktop.
app-rfc-v4
<?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.
- 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 Kernel object can be nested (and un-nested), which internally nests all core services, as well as injector / config.
However, this nesting is performed per-instance and not via static accessors as with config / injector.
- Config::inst() and Injector::inst() functionality remains unaltered.
- HTTPRequest is not a part of kernel, nor are any request-specific services.
- Session::inst() is removed and replaced with of the HTTPRequest::getSession()
- Development experience should not be greatly adversely affected for classes which use the main core traits.
Critical AC:
- If a non-current instance of Injector / Config 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->handle($request);
// End: of main.php
// Begin:: cli-script.php
$kernel = new AppKernel();
$app = new CLIApplication($kernel);
$app->handle();
// based on https://github.com/symfony/symfony/blob/3.4/src/Symfony/Component/HttpKernel/KernelInterface.php
interface Kernel
{
public function boot();
/**
* @return Injector
*/
public function getContainer();
public function setContainer(Injector $injector);
/**
* @return string
*/
public function getEnvironment();
/**
* @return static
*/
public function nest();
}
/**
* Basic container for Kernel. Can be used or subclassed by user-code to set a custom Kernel
*/
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 getContainer()
{
return $this->injector;
}
public function setContainer(Injector $injector)
{
// Ensure injector / kernel are mutually consistent
$this->injector = $injector;
$injector->registerService($this, Kernel::class);
return $this;
}
public function getEnvironment()
{
return $this->environment;
}
public function nest()
{
$new = clone $this;
// Note: nest config / injector here automatically.
// Further nesting of config / injector can be done within this appstate level
// Implementation here omitted for brevity
// TODO: Nest config / injector
$new->nestedFrom = $this;
return $new;
}
public function unnest()
{
// TODO: Unnest config / injector
return $this->nestedFrom;
}
}
/**
* 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->setContainer(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 handle(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)
{
// @See Kernel::nest()
$kernel = $this->kernel->nest();
try {
return $this->routeRequest($kernel, $request);
} finally {
// Safe unnest kernel
$kernel->unnest();
}
}
/**
* Replacement for Director::direct()
*
* @param HTTPrequest $request
* @return HTTPResponse
*/
protected function routeRequest($kernel, $request)
{
// Refactor Director::direct() into here, return response
}
}
class Injector {
/**
* Ensure only active instance is accessed.
* Consistency enforced by both Injector::nest() / Kernel::nest()
*
* Same logic applied to Config (omitted for brevity)
*/
protected function checkInst()
{
if (static::$inst !== $this) {
throw new LogicException("Accessing incorrect container context");
}
}
public function get($class)
{
// Add consistency check when invoking injector
$this->checkInst();
// ...
}
}
/**
* 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();
}
/**
* @return Session
*/
public function getSession()
{
return $this->session;
}
}
/**
* Example injectable object
*/
class SomeObject
{
// 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 will need to be passed this object now,
// instead of relying on global statics
$subController = OtherObject::create();
$result = $subController->handleRequest($request);
// Replaces Session::set()
$request->getSession()->set('OK', 1);
// Cookies is unchanged
Cookies::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