The API is a standalone application written with the Symfony 2 components. We chose to use the components rather than the full framework to keep things light and focussed. While we're actually reasonably happy with that choice, it can make finding relevant documentation less straightforward than it could be - since most of the documentation assumes you're using the full framework.
The (simplified) architecture looks something like this:

One of the tasks we had to deal with was making sure that our API handled CORS preflight requests, if you've ever used Javascript to do any API-based interaction, you'll be familiar with CORS, and how it usually places a very annoying red error message in your console, stubbornly refusing to retrieve data from anywhere that isn't on the same domain as your application.
Ideally we wanted to hook in before requests were processed, and as responses were sent to add our headers. There is an article on implementing before and after filters in Symfony which would have been perfect - except it assumes you're using the full framework.
We did take quite a lot from that article though, and the end result was a nice small class which just deals with CORS. There are 4 key steps in our solution:
- Adjust our routing so that all endpoints will accept OPTIONS requests
- Register our listener functions
- Respond to CORS preflight requests before the route controller kicks in
- Filter responses to add CORS headers
Adjusting routing
Our routing is being handled by Symfony's UrlMatcherclass, with routes being defined in a YAML file. During early development, each route was restricted to the specific methods it was expected to serve, e.g.
getUser:
path: /user
defaults: { _controller: 'OurProject\UserEndpoint::get' }
methods: [GET]
To ensure that preflight requests could be handled, we added OPTIONS to the allowable methods for each endpoint:
getUser:
path: /user
defaults: { _controller: 'OurProject\UserEndpoint::get' }
methods: [GET,OPTIONS]
Register our listener functions
Then we created a CorsListener class that would handle all of the CORS functionality. This implements Symfony'sEventSubscriberInterface meaning we can add it as a subscriber. The basic shape of the class is as follows:
<?php
class CorsListener implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => array('onKernelRequest', 9999),
KernelEvents::RESPONSE => array('onKernelResponse', 9999),
);
}
public function onKernelRequest(GetResponseEvent $event) {}
public function onKernelResponse(FilterResponseEvent $event) {}
}
?>
The getSubscribedEvents() function will tell the event dispatcher that our class has methods to deal with theKernelEvents::REQUEST and KernelEvents::RESPONSE events. Once we had this basic class, we added it into our kernel - here's the relevant parts of our main kernel code - notice that we've added the CorsListener as a subscriber:
<?php
$request = Request::createFromGlobals();
$context = new RequestContext();
$context->fromRequest($request);
$matcher = new UrlMatcher($routes, $context);
$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new RouterListener($matcher));
$dispatcher->addSubscriber(new CorsListener());
$resolver = new ControllerResolver();
$kernel = new HttpKernel($dispatcher, $resolver);
$response = $kernel->handle($request)->send();
?>
At this stage, our onKernelRequest() method will be called before controllers are invoked to handle an inbound request. TheonKernelResponse() will be called once a response as been generated, but not sent.
Handling CORS preflight requests
To ensure that we handle preflight requests (which are just an OPTIONS request to the chosen URL), we check for the request method in ouronKernelRequest() method, and if that's the active method, we simply return an empty response - there's no response data required. Returning a response from this event causes the main controller not to be invoked.
<?php
public function onKernelRequest(GetResponseEvent $event)
{
// Don't do anything if it's not the master request.
if (!$event->isMasterRequest()) {
return;
}
$request = $event->getRequest();
$method = $request->getRealMethod();
if ('OPTIONS' == $method) {
$response = new Response();
$event->setResponse($response);
}
}
?>
Notice - we're creating a response, but setting no content. We're also not actually setting any CORS related headers here. The response will go through the response filtering just as any other response would, so we add the headers in there - see below.
Filter responses to add CORS headers
The onKernelResponse() method is called once a response has been produced, but not sent. It gives us the option to add, or modify parts of the response. In our case, we're just looking to add additional headers to the responses.
<?php
public function onKernelResponse(FilterResponseEvent $event)
{
// Don't do anything if it's not the master request.
if (!$event->isMasterRequest()) {
return;
}
$response = $event->getResponse();
$response->headers->set('Access-Control-Allow-Origin', '*');
$response->headers->set('Access-Control-Allow-Methods', 'GET,POST,PUT');
$response->headers->set('Access-Control-Allow-Headers', 'X-Header-One,X-Header-Two');
}
?>
This filter means that we add three headers to all responses, both the preflight responses, and the actual responses. We're using some custom headers so need to specifically declare those in Access-Control-Allow-Headers - depending on your exact use case you may well not need to go that far.
The results
We can now send OPTIONS preflight requests, and get the correct headers out without processing a full request cycle, and when we do make real requests, the responses include the correct headers. All of that has been done with event listeners, not tied up in the main endpoint code so it's easily amendable independently of the API handling logic, and can easily be disabled.