Skip to content

Instantly share code, notes, and snippets.

@tractorcow
Last active May 31, 2017 02:49
Show Gist options
  • Save tractorcow/8be54d6b019412c037049c8caddf39eb to your computer and use it in GitHub Desktop.
Save tractorcow/8be54d6b019412c037049c8caddf39eb to your computer and use it in GitHub Desktop.
app-rfc-v3
<?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