Last active
February 17, 2016 19:59
-
-
Save kentr/df78111c371194742898 to your computer and use it in GitHub Desktop.
Suggested HttpKernel::mapRequest() code to inject full server vars into the Symfony request.
This file contains hidden or 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 | |
namespace PHPPM\Bridges; | |
use PHPPM\AppBootstrapInterface; | |
use PHPPM\Bootstraps\BootstrapInterface; | |
use PHPPM\Bridges\BridgeInterface; | |
use React\Http\Request as ReactRequest; | |
use React\Http\Response as ReactResponse; | |
use Stack\Builder; | |
use Symfony\Component\HttpFoundation\Request as SymfonyRequest; | |
use Symfony\Component\HttpFoundation\Response as SymfonyResponse; | |
use Symfony\Component\HttpFoundation\StreamedResponse as SymfonyStreamedResponse; | |
use Symfony\Component\HttpKernel\TerminableInterface; | |
class HttpKernel implements BridgeInterface | |
{ | |
/** | |
* An application implementing the HttpKernelInterface | |
* | |
* @var \Symfony\Component\HttpKernel\HttpKernelInterface | |
*/ | |
protected $application; | |
/** | |
* Bootstrap an application implementing the HttpKernelInterface. | |
* | |
* In the process of bootstrapping we decorate our application with any number of | |
* *middlewares* using StackPHP's Stack\Builder. | |
* | |
* The app bootstraping itself is actually proxied off to an object implementing the | |
* PHPPM\Bridges\BridgeInterface interface which should live within your app itself and | |
* be able to be autoloaded. | |
* | |
* @param string $appBootstrap The name of the class used to bootstrap the application | |
* @param string|null $appenv The environment your application will use to bootstrap (if any) | |
* @see http://stackphp.com | |
*/ | |
public function bootstrap($appBootstrap, $appenv) | |
{ | |
// include applications autoload | |
$autoloader = dirname(realpath($_SERVER['SCRIPT_NAME'])) . '/vendor/autoload.php'; | |
if (file_exists($autoloader)) { | |
require_once $autoloader; | |
} | |
$appBootstrap = $this->normalizeAppBootstrap($appBootstrap); | |
$bootstrap = new $appBootstrap($appenv); | |
if ($bootstrap instanceof BootstrapInterface) { | |
$this->application = $bootstrap->getApplication(); | |
if ($bootstrap instanceof StackableBootstrapInterface) { | |
$stack = new Builder(); | |
$stack = $bootstrap->getStack($stack); | |
$this->application = $stack->resolve($this->application); | |
} | |
} | |
} | |
/** | |
* Handle a request using a HttpKernelInterface implementing application. | |
* | |
* @param \React\Http\Request $request | |
* @param \React\Http\Response $response | |
*/ | |
public function onRequest(ReactRequest $request, ReactResponse $response) | |
{ | |
if (null === $this->application) { | |
return; | |
} | |
$content = ''; | |
$headers = $request->getHeaders(); | |
$contentLength = isset($headers['Content-Length']) ? (int) $headers['Content-Length'] : 0; | |
$request->on('data', function($data) | |
use ($request, $response, &$content, $contentLength) | |
{ | |
// read data (may be empty for GET request) | |
$content .= $data; | |
// handle request after receive | |
if (strlen($content) >= $contentLength) { | |
$syRequest = self::mapRequest($request, $content); | |
try { | |
$syResponse = $this->application->handle($syRequest); | |
} catch (\Exception $exception) { | |
$response->writeHead(500); // internal server error | |
$response->end(); | |
return; | |
} | |
self::mapResponse($response, $syResponse); | |
if ($this->application instanceof TerminableInterface) { | |
$this->application->terminate($syRequest, $syResponse); | |
} | |
} | |
}); | |
} | |
/** | |
* Convert React\Http\Request to Symfony\Component\HttpFoundation\Request | |
* | |
* @param ReactRequest $reactRequest | |
* @return SymfonyRequest $syRequest | |
*/ | |
protected static function mapRequest(ReactRequest $reactRequest, $content) { | |
$method = strtoupper($reactRequest->getMethod()); | |
$headers = $reactRequest->getHeaders(); | |
$query = $reactRequest->getQuery(); | |
$post = array(); | |
$requestIsPostType = in_array( | |
$method, | |
array('POST', 'PUT', 'DELETE', 'PATCH') | |
); | |
// Parse body? | |
if (isset($headers['Content-Type']) && (0 === strpos($headers['Content-Type'], 'application/x-www-form-urlencoded')) | |
&& $requestIsPostType | |
) { | |
parse_str($content, $post); | |
} | |
$cookies = array(); | |
if (isset($headers['Cookie'])) { | |
$headersCookie = explode(';', $headers['Cookie']); | |
foreach ($headersCookie as $cookie) { | |
list($name, $value) = explode('=', trim($cookie)); | |
$cookies[$name] = $value; | |
} | |
} | |
// Add any query string to URI so SymfonyRequest::create() can access it. | |
$uri = $reactRequest->getPath() . | |
(empty($query) ? '' : '?' . http_build_query($query)); | |
// SymfonyRequest::create() expects $parameters to contain either | |
// $_GET or $_POST. | |
$parameters = $requestIsPostType ? $post : $query; | |
$syRequest = SymfonyRequest::create( | |
// $uri, $method, $parameters, $cookies, $files, $server, $content. | |
$uri, | |
$method, | |
$parameters, | |
$cookies, | |
array(), | |
array(), | |
$content | |
); | |
$syRequest->headers->replace($headers); | |
// Set CGI/1.1 (RFC 3875) server vars. | |
if (empty($_ENV)) { | |
// In some cases with cli, $_ENV isn't set, so get with getenv(). | |
// @see http://stackoverflow.com/questions/8798294/getenv-vs-env-in-php/21473853#21473853 | |
// @todo: Make this more efficient to eliminate running per request. | |
// Static variable? | |
$_ENV['DOCUMENT_ROOT'] = getenv('DOCUMENT_ROOT'); | |
$_ENV['SCRIPT_NAME'] = getenv('SCRIPT_NAME'); | |
} | |
$serverVars = array_merge( | |
$syRequest->server->all(), | |
array( | |
'DOCUMENT_ROOT' => $_ENV['DOCUMENT_ROOT'], | |
'GATEWAY_INTERFACE' => 'CGI/1.1', | |
'SCRIPT_NAME' => $_ENV['SCRIPT_NAME'], | |
// SCRIPT_FILENAME contains the name of the php-pm startup script. | |
// Must override here. | |
'SCRIPT_FILENAME' => $_ENV['DOCUMENT_ROOT'] . $_ENV['SCRIPT_NAME'], | |
) | |
); | |
$syRequest->server->replace($serverVars); | |
return $syRequest; | |
} | |
/** | |
* Convert Symfony\Component\HttpFoundation\Response to React\Http\Response | |
* | |
* @param ReactResponse $reactResponse | |
* @param SymfonyResponse $syResponse | |
*/ | |
protected static function mapResponse(ReactResponse $reactResponse, | |
SymfonyResponse $syResponse) | |
{ | |
$headers = $syResponse->headers->all(); | |
$reactResponse->writeHead($syResponse->getStatusCode(), $headers); | |
// @TODO convert StreamedResponse in an async manner | |
if ($syResponse instanceof SymfonyStreamedResponse) { | |
ob_start(); | |
$syResponse->sendContent(); | |
$content = ob_get_contents(); | |
ob_end_clean(); | |
} | |
else { | |
$content = $syResponse->getContent(); | |
} | |
$reactResponse->end($content); | |
} | |
/** | |
* @param $appBootstrap | |
* @return string | |
* @throws \RuntimeException | |
*/ | |
protected function normalizeAppBootstrap($appBootstrap) | |
{ | |
$appBootstrap = str_replace('\\\\', '\\', $appBootstrap); | |
if (false === class_exists($appBootstrap)) { | |
$appBootstrap = '\\' . $appBootstrap; | |
if (false === class_exists($appBootstrap)) { | |
throw new \RuntimeException('Could not find bootstrap class ' . $appBootstrap); | |
} | |
return $appBootstrap; | |
} | |
return $appBootstrap; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment