Skip to content

Instantly share code, notes, and snippets.

@jmather
Created September 29, 2012 22:48
Show Gist options
  • Select an option

  • Save jmather/3805361 to your computer and use it in GitHub Desktop.

Select an option

Save jmather/3805361 to your computer and use it in GitHub Desktop.
RESTful Versioned API with Silex using Accept header
<?php
use Silex\WebTestCase;
use Symfony\Component\HttpKernel\HttpKernel;
use Silex\Controller;
use Silex\ControllerCollection;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\KernelEvents;
class AcceptHeaderRoutingTest extends WebTestCase
{
/**
* Creates the application.
*
* @return HttpKernel
*/
public function createApplication()
{
$app = new \Silex\Application();
$app['dispatcher']->addSubscriber(new \VersionedRestKernelListener());
$app['versioned_rest_controllers_factory'] = $app->protect(function ($version) use ($app) {
return new \VersionedRestControllerCollection($app['route_factory'], $version);
});
$app['url_matcher'] = $app->share(function () use ($app) {
return new \VersionedRestUrlMatcher($app['routes'], $app['request_context'], $app['request']);
});
/** @var $controllers1 VersionedRestControllerCollection */
$controllers1 = $app['versioned_rest_controllers_factory']('application/ven.test.v1');
$controllers1->get('/test', function(Request $request) use ($app) {
$_format = $request->request->get('_format');
$_api_version = $request->request->get('_api_version');
if ($_format == 'json')
$cont = json_encode(array('content' => 'hello'));
else
$cont = '<content>hello</content>';
return new Response($cont, 200, array('Content-Type' => $_api_version.'+'.$_format));
}, array('json', 'xml'));
/** @var $controllers1 VersionedRestControllerCollection */
$controllers2 = $app['versioned_rest_controllers_factory']('application/ven.test.v2');
$controllers2->get('/test', function(Request $request) use ($app) {
$_format = $request->request->get('_format');
$_api_version = $request->request->get('_api_version');
if ($_format == 'json')
$cont = json_encode(array('content' => 'hiya'));
elseif ($_format == 'xml')
$cont = '<content>hiya</content>';
return new Response($cont, 200, array('Content-Type' => $_api_version.'+'.$_format));
}, array('xml', 'json'));
$app->mount('/', $controllers1);
$app->mount('/', $controllers2);
$app['debug'] = true;
unset($app['exception_handler']);
return $app;
}
public function testValidV1Call()
{
$client = $this->createClient();
$crawler = $client->request('GET', '/test', array(), array(), array('HTTP_ACCEPT' => 'application/ven.test.v1+xml'));
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$result = $client->getResponse()->getContent();
$this->assertEquals('<content>hello</content>', $result, 'response is correct');
}
public function testValidV2Call()
{
$client = $this->createClient();
$crawler = $client->request('GET', '/test', array(), array(), array('HTTP_ACCEPT' => 'application/ven.test.v2+xml'));
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$result = $client->getResponse()->getContent();
$this->assertEquals('<content>hiya</content>', $result, 'response is correct');
}
/**
* @expectedException Symfony\Component\HttpKernel\Exception\NotFoundHttpException
*/
public function testInvalidV3Call()
{
$client = $this->createClient();
$crawler = $client->request('GET', '/test', array(), array(), array('HTTP_ACCEPT' => 'application/ven.test.v3+xml'));
$this->assertEquals(404, $client->getResponse()->getStatusCode());
}
}
<?php
use Silex\Controller;
use Silex\Route;
class VersionedRestControllerCollection extends \Silex\ControllerCollection
{
private $accept_header;
/**
* Constructor.
*/
public function __construct(Route $defaultRoute, $accept_header)
{
$this->defaultRoute = $defaultRoute;
$this->accept_header = $accept_header;
}
/**
* Maps a pattern to a callable.
*
* You can optionally specify HTTP methods that should be matched.
*
* @param string $pattern Matched route pattern
* @param mixed $to Callback that returns the response when matched
* @param array $formats
*
* @return Controller
*/
public function match($pattern, $to, $formats = array())
{
if (!is_array($formats))
$formats = array($formats);
$route = clone $this->defaultRoute;
$route->setPattern($pattern);
$route->setDefault('_controller', $to);
$route->addRequirements(array('_format' => '('.implode('|', $formats).')'));
$route->addRequirements(array('_accept' => preg_quote($this->accept_header, '/')));
$this->controllers[] = $controller = new Controller($route);
$controller->bind(md5($this->accept_header.'_'.$pattern));
return $controller;
}
/**
* Maps a GET request to a callable.
*
* @param string $pattern Matched route pattern
* @param mixed $to Callback that returns the response when matched
* @param array $formats
*
* @return Controller
*/
public function get($pattern, $to, $formats = array())
{
return $this->match($pattern, $to, $formats)->method('GET');
}
/**
* Maps a POST request to a callable.
*
* @param string $pattern Matched route pattern
* @param mixed $to Callback that returns the response when matched
* @param array $formats
*
* @return Controller
*/
public function post($pattern, $to, $formats = array())
{
return $this->match($pattern, $to, $formats)->method('POST');
}
/**
* Maps a PUT request to a callable.
*
* @param string $pattern Matched route pattern
* @param mixed $to Callback that returns the response when matched
* @param array $formats
*
* @return Controller
*/
public function put($pattern, $to, $formats = array())
{
return $this->match($pattern, $to, $formats)->method('PUT');
}
/**
* Maps a DELETE request to a callable.
*
* @param string $pattern Matched route pattern
* @param mixed $to Callback that returns the response when matched
* @param array $formats
*
* @return Controller
*/
public function delete($pattern, $to, $formats = array())
{
return $this->match($pattern, $to, $formats)->method('DELETE');
}
}
<?php
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class VersionedRestKernelListener implements EventSubscriberInterface
{
public static function onKernelRequest(GetResponseEvent $event)
{
$pattern = '|^(.*)(?:\+([a-z]+))?$|U';
if (preg_match($pattern, $event->getRequest()->headers->get('Accept'), $matches))
{
if (isset($matches[2]) && $matches[2] != '')
$event->getRequest()->request->set('_format', $matches[2]);
$event->getRequest()->request->set('_accept', $matches[1]);
}
}
/**
* Returns an array of event names this subscriber wants to listen to.
*
* The array keys are event names and the value can be:
*
* * The method name to call (priority defaults to 0)
* * An array composed of the method name to call and the priority
* * An array of arrays composed of the method names to call and respective
* priorities, or 0 if unset
*
* For instance:
*
* * array('eventName' => 'methodName')
* * array('eventName' => array('methodName', $priority))
* * array('eventName' => array(array('methodName1', $priority), array('methodName2'))
*
* @return array The event names to listen to
*
* @api
*/
public static function getSubscribedEvents()
{
return array(KernelEvents::REQUEST => array('onKernelRequest', 100));
}
}
<?php
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\HttpFoundation\Request;
class VersionedRestUrlMatcher extends Silex\RedirectableUrlMatcher
{
private $request;
public function __construct(RouteCollection $routes, RequestContext $context, Request $request)
{
parent::__construct($routes, $context);
$this->request = $request;
}
protected function handleRouteRequirements($pathinfo, $name, Route $route)
{
$ret = parent::handleRouteRequirements($pathinfo, $name, $route);
if ($ret[0] == self::REQUIREMENT_MISMATCH)
return $ret;
foreach($route->getRequirements() as $name => $value)
{
if ($name == '_method')
continue;
if (false == preg_match('/^'.$value.'$/', $this->request->request->get($name)))
return array(self::REQUIREMENT_MISMATCH, null);
}
return array(self::REQUIREMENT_MATCH, null);
}
}
@igorw
Copy link

igorw commented Oct 2, 2012

You can replace:

$accepts = array_map(function($line) { return trim($line); }, $raw_accepts);

With:

$accepts = array_map('trim', $raw_accepts);

@jmather
Copy link
Author

jmather commented Oct 2, 2012

Hah! Thanks. So caught up in the process, I missed something silly-simple.

@cordoval
Copy link

cordoval commented Oct 2, 2012

you should also return something always

                return;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment