[Http] ROUTING BABY

Decoupled routing from HTTP
Added Router implement HttpServerInterface
Fully functional Symfony routes in application!
As a result, this drastically decreased backwards compatibility breaks while introducing new functionality
This commit is contained in:
Chris Boden 2013-04-26 23:01:28 -04:00
parent c24cdf379e
commit 4df71c3a35
6 changed files with 95 additions and 67 deletions

View File

@ -10,8 +10,8 @@ CHANGELOG
* 0.3.0 (2013-xx-xx) * 0.3.0 (2013-xx-xx)
* BC: Requre hostname and do Origin HTTP header check against it, disabling CORS by default for security reasons * BC: Require hostname and do Origin HTTP header check against it by default, helping prevent CSRF attacks
* BC: Added Routing to HTTP allowing for a single Ratchet server to handle multiple apps * Added HTTP Router component to allowing for a single Ratchet server to handle multiple apps
* BC: Decoupled HTTP from WebSocket component * BC: Decoupled HTTP from WebSocket component
* 0.2.5 (2013-04-01) * 0.2.5 (2013-04-01)

View File

@ -38,9 +38,15 @@ Need help? Have a question? Want to provide feedback? Write a message on the
<?php <?php
use Ratchet\MessageComponentInterface; use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface; use Ratchet\ConnectionInterface;
use Ratchet\Server\IoServer; use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer; use Ratchet\Http\HttpServer;
use Ratchet\Http\Router;
use Ratchet\WebSocket\WsServer; use Ratchet\WebSocket\WsServer;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\RequestContext;
require __DIR__ . '/vendor/autoload.php'; require __DIR__ . '/vendor/autoload.php';
@ -77,7 +83,12 @@ class Chat implements MessageComponentInterface {
} }
// Run the server application through the WebSocket protocol on port 8080 // Run the server application through the WebSocket protocol on port 8080
$server = IoServer::factory(new HttpServer(new WsServer(new Chat)), 8080); // This will be made easier soon, routing currently in development
$routes->add('chatRoute', new Route('/chat', array(
'_controller' => new WsServer(new Chat)
)));
$server = IoServer::factory(new HttpServer(new Router(new UrlMatcher($routes, new RequestContext))), 8080);
$server->run(); $server->run();
``` ```

View File

@ -19,61 +19,30 @@ class HttpServer implements MessageComponentInterface {
protected $_reqParser; protected $_reqParser;
/** /**
* @var \Symfony\Component\Routing\RouteCollection A collection with \Ratchet\MessageComponentInterface controllers * @var \Ratchet\Http\HttpServerInterface
*/ */
protected $_routes; protected $_httpServer;
/** /**
* @param string $host * @param HttpServerInterface
* @param RouteCollection $collection
* @throws \UnexpectedValueException If a Route Controller does not map to a \Ratchet\MessageComponentInterface
*/ */
public function __construct($host, RouteCollection $collection = null) { public function __construct(HttpServerInterface $server) {
if (null === $collection) { $this->_httpServer = $server;
$collection = new RouteCollection; $this->_reqParser = new HttpRequestParser;
} else {
foreach ($collection as $routeName => $route) {
if (is_string($route['_controller']) && class_exists($route['_controller'])) {
$route['_controller'] = new $route['_controller'];
}
if (!($route['_controller'] instanceof HttpServerInterface)) {
throw new \UnexpectedValueException('All routes must implement Ratchet\MessageComponentInterface');
}
}
}
$collection->setHost($host);
$this->_routes = $collection;
$this->_reqParser = new HttpRequestParser;
}
/**
* @param string
* @param string
* @param Ratchet\Http\HttpServerInterface
* @param array
*/
public function addRoute($name, $path, HttpServerInterface $controller) {
$this->_routes->add($name, new Route($path, array(
'_controller' => $controller
)));
} }
/** /**
* @{inheritdoc} * @{inheritdoc}
*/ */
public function onOpen(ConnectionInterface $conn) { public function onOpen(ConnectionInterface $conn) {
$conn->Http = new \StdClass; $conn->httpHeadersReceived = false;
$conn->Http->headers = false;
} }
/** /**
* @{inheritdoc} * @{inheritdoc}
*/ */
public function onMessage(ConnectionInterface $from, $msg) { public function onMessage(ConnectionInterface $from, $msg) {
if (true !== $from->Http->headers) { if (true !== $from->httpHeadersReceived) {
try { try {
if (null === ($request = $this->_reqParser->onMessage($from, $msg))) { if (null === ($request = $this->_reqParser->onMessage($from, $msg))) {
return; return;
@ -82,32 +51,20 @@ class HttpServer implements MessageComponentInterface {
return $this->close($from, 413); return $this->close($from, 413);
} }
$context = new RequestContext($request->getUrl(), $request->getMethod(), $request->getHost(), $request->getScheme(), $request->getPort()); $from->httpHeadersReceived = true;
$matcher = new UrlMatcher($this->_routes, $context);
try { return $this->_httpServer->onOpen($from, $request);
$route = $matcher->match($request->getPath());
} catch (MethodNotAllowedException $nae) {
return $this->close($from, 403);
} catch (ResourceNotFoundException $nfe) {
return $this->close($from, 404);
}
$from->Http->headers = true;
$from->Http->controller = $route['_controller'];
return $from->Http->controller->onOpen($from, $request);
} }
$from->Http->controller->onMessage($from, $msg); $this->_httpServer->onMessage($from, $msg);
} }
/** /**
* @{inheritdoc} * @{inheritdoc}
*/ */
public function onClose(ConnectionInterface $conn) { public function onClose(ConnectionInterface $conn) {
if ($conn->Http->headers) { if ($conn->httpHeadersReceived) {
$conn->Http->controller->onClose($conn); $this->_httpServer->onClose($conn);
} }
} }
@ -115,10 +72,10 @@ class HttpServer implements MessageComponentInterface {
* @{inheritdoc} * @{inheritdoc}
*/ */
public function onError(ConnectionInterface $conn, \Exception $e) { public function onError(ConnectionInterface $conn, \Exception $e) {
if ($conn->Http->headers) { if ($conn->httpHeadersReceived) {
$conn->Http->controller->onError($conn, $e); $this->_httpServer->onError($conn, $e);
} else { } else {
$conn->close(); $this->close($conn, 500);
} }
} }

View File

@ -7,8 +7,7 @@ use Guzzle\Http\Message\RequestInterface;
interface HttpServerInterface extends MessageComponentInterface { interface HttpServerInterface extends MessageComponentInterface {
/** /**
* @param \Ratchet\ConnectionInterface $conn * @param \Ratchet\ConnectionInterface $conn
* @param \Guzzle\Http\Message\RequestInterface $headers null is default because PHP won't let me overload; don't pass null!!! * @param \Guzzle\Http\Message\RequestInterface $request null is default because PHP won't let me overload; don't pass null!!!
* @return mixed
*/ */
public function onOpen(ConnectionInterface $conn, RequestInterface $headers = null); public function onOpen(ConnectionInterface $conn, RequestInterface $request = null);
} }

View File

@ -0,0 +1,61 @@
<?php
namespace Ratchet\Http;
use Ratchet\ConnectionInterface;
use Guzzle\Http\Message\RequestInterface;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
class Router implements HttpServerInterface {
/**
* @var \Symfony\Component\Routing\Matcher\UrlMatcherInterface
*/
protected $_matcher;
public function __construct(UrlMatcherInterface $matcher) {
$this->_matcher = $matcher;
}
public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) {
try {
$route = $this->_matcher->match($request->getPath());
} catch (MethodNotAllowedException $nae) {
return $this->close($from, 403);
} catch (ResourceNotFoundException $nfe) {
return $this->close($from, 404);
}
if (is_string($route['_controller']) && class_exists($route['_controller'])) {
$route['_controller'] = new $route['_controller'];
}
if (!($route['_controller'] instanceof HttpServerInterface)) {
throw new \UnexpectedValueException('All routes must implement Ratchet\HttpServerInterface');
}
$conn->controller = $route['_controller'];
$conn->controller->onOpen($conn, $request);
}
/**
* @{inheritdoc}
*/
function onMessage(ConnectionInterface $from, $msg) {
$from->controller->onMessage($from, $msg);
}
/**
* @{inheritdoc}
*/
function onClose(ConnectionInterface $conn) {
$conn->controller->onClose($conn);
}
/**
* @{inheritdoc}
*/
function onError(ConnectionInterface $conn, \Exception $e) {
$conn->controller->onError($conn, $e);
}
}

View File

@ -73,9 +73,9 @@ class WsServer implements HttpServerInterface {
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */
public function onOpen(ConnectionInterface $conn, RequestInterface $headers = null) { public function onOpen(ConnectionInterface $conn, RequestInterface $request = null) {
$conn->WebSocket = new \StdClass; $conn->WebSocket = new \StdClass;
$conn->WebSocket->request = $headers; $conn->WebSocket->request = $request;
$conn->WebSocket->established = false; $conn->WebSocket->established = false;
$this->attemptUpgrade($conn); $this->attemptUpgrade($conn);