Skip to content

Instantly share code, notes, and snippets.

@jmather
Created September 29, 2012 22:48
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save jmather/3805361 to your computer and use it in GitHub Desktop.
Save jmather/3805361 to your computer and use it in GitHub Desktop.
RESTful Versioned API with Silex using Accept header
<?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 AcceptHeaderKernelListener implements EventSubscriberInterface
{
public static function onKernelRequest(GetResponseEvent $event)
{
if ($event->getRequest()->headers->get('Accept') != '')
{
$raw_accepts = explode(',', $event->getRequest()->headers->get('Accept'));
$accepts = array_map('trim', $raw_accepts);
$event->getRequest()->request->set('_accept', $accepts);
$event->getDispatcher()->addSubscriber(new \AcceptHeaderMatchEventListener($event->getRequest()));
}
}
public static function getSubscribedEvents()
{
return array(KernelEvents::REQUEST => array('onKernelRequest', 100));
}
}
<?php
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Routing\Matcher\UrlMatcher;
use Symfony\Component\Routing\Matcher\UrlMatcherEvent;
use Symfony\Component\HttpFoundation\Request;
class AcceptHeaderMatchEventListener implements EventSubscriberInterface
{
private $request;
public function __construct(Request $request)
{
$this->request = $request;
}
public function onMatch(UrlMatcherEvent $event)
{
foreach($this->request->request->get('_accept') as $accept)
{
if (preg_match('/^('.$event->getRoute()->getRequirement('_accept').')$/', $accept))
{
$event->getRoute()->setDefault('accept_header', $accept);
$event->setStatus(UrlMatcherEvent::REQUIREMENT_MATCH);
return;
}
}
$event->setStatus(UrlMatcherEvent::REQUIREMENT_MISMATCH);
}
public static function getSubscribedEvents()
{
return array(UrlMatcher::EVENT_HANDLE_REQUIREMENTS => array('onMatch', 100));
}
}
<?php
use Silex\Route;
class AcceptHeaderRoute extends Route
{
public function accept($content_types)
{
$content_regexps = array_map(function($item) { return preg_quote($item, '/'); }, $content_types);
$this->setRequirement('_accept', implode('|', $content_regexps));
return $this;
}
}
<?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;
use Silex\RedirectableUrlMatcher;
class AcceptHeaderRoutingTest extends WebTestCase
{
/**
* Creates the application.
*
* @return HttpKernel
*/
public function createApplication()
{
$app = new \Silex\Application();
$app['route_class'] = '\\AcceptHeaderRoute';
$app['dispatcher']->addSubscriber(new \AcceptHeaderKernelListener());
$app['url_matcher'] = $app->share(function () use ($app) {
$matcher = new RedirectableUrlMatcher($app['routes'], $app['request_context']);
$matcher->setEventDispatcher($app['dispatcher']);
return $matcher;
});
/** @var $controllers1 VersionedRestControllerCollection */
$controllers1 = $app['controllers_factory'];
$controllers1->get('/test', function($accept_header) use ($app) {
if ($accept_header == 'application/ven.test.v1+json')
$cont = json_encode(array('content' => 'hello'));
else
$cont = '<content>hello</content>';
return new Response($cont, 200, array('Content-Type' => $accept_header));
})->accept(array('application/ven.test.v1+json', 'application/ven.test.v1+xml'));
/** @var $controllers1 VersionedRestControllerCollection */
$controllers2 = $app['controllers_factory'];
$controllers1->get('/test', function($accept_header) use ($app) {
if ($accept_header == 'application/ven.test.v2+json')
$cont = json_encode(array('content' => 'hiya'));
else
$cont = '<content>hiya</content>';
return new Response($cont, 200, array('Content-Type' => $accept_header));
})->accept(array('application/ven.test.v2+json', 'application/ven.test.v2+xml'));
$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());
}
}
@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