Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

[WIP] added support for a API versioning #136

Closed
wants to merge 11 commits into from
@lsmith77
Owner

This PR will eventually fix #12 and #30

For now we will only support media type based versioning (aka no URL based versioning)
See also the following links for details:
http://www.informit.com/articles/article.aspx?p=1566460
http://barelyenough.org/blog/2008/05/versioning-rest-web-services/

basically it looks like the best approach will be to replace the default RouterListener by simply registering a request listener that loads before the default one. In this listener we will need to examine the accept headers to determine the format and based on this decide which controller to use.

Now the tricky bit is that essentially for a single pattern, we will need to have a list of potential controllers we should examine. I have not yet found a good way to do this. Atm I would be inclined to say the best solution would be to support multiple resources per route:
symfony/symfony#2145

There are also some things still missing (on all inclusive list):

Also note that through out all of this we must be aware that some browsers seem to prefer XHTML or worse XML over HTML:
http://www.gethifi.com/blog/browser-rest-http-accept-headers

@stof
Owner

@lsmith77 any news here ?

@lsmith77
Owner

no .. basically the notes in the PR description are still valid, but its a fair bit of work and i haven't been able to work on it further ..

@everzet
Owner

Needs to be updated to support new Routing architecture. But it should be fairly easy as new arch is freaking clean :-D

@cystbear

@lsmith77 will you add object versioning from JMSSerializerBundle doc link ?

@lsmith77
Owner
@adrienbrault

Hi, any news on this ? (maybe after yesterday's REST talk at symfony live ?)
What needs to be done ?

@lsmith77
Owner

no .. there is no progress here aside from some general improvements to better integrate JMSSerializerBundle inside the View class. that BadFaith lib is however slowly, but surely, maturing.

@breerly

Any way I can help scoot this along? What's the block exactly?

@lsmith77
Owner
@breerly

Any chance we could look at URI versioning for the time being since that will likely be desired by many users?

@lsmith77
Owner
@lsmith77
Owner

here is what the flow3 guys did for routing with content type negotiation:
https://review.typo3.org/#/c/11683/1

@gimler

any news

@lsmith77
Owner

nope. symfony/symfony#5711 is stalled which seemed like the most promising initiative for a while.

@blaugueux

The version need to be also configured in the SerializationContext object.

@Codepadawan

API versioning still out in the cold?

@lsmith77
Owner

yes .. there is some progress in terms of a better content negotiation lib with https://github.com/willdurand/Negotiation but now the big task is left of integrating it within the Routing system

/cc @willdurand

@willdurand
Owner

Yup. Don't really know how to that though.

I did API versioning once using different routes /v1/..., /v2/... but it is not "cool". What we want is to leverage the Accept header but then what? How do we organize controllers and actions?

  • A sub-namespace?
  • A specific controller name mapped to a version somewhere?
  • Something else?

I thought about using a proxy controller that would forward the request to the right controller.

Example: the routing uses Controller\UserController. In this controller, if the version is:

  • v1 then we use Controller\V1\UserController
  • v2 then we use Controller\V2\UserController
@lsmith77
Owner

we could maybe do a simple solution that will likely work in more than 80% of the cases. we could have a very early listener that looks at the media type and if it finds there is only one then we attempt to extract a version via a convention (optionally via some callback). this version is set as a request attribute and can therefore be checked via a route requirement.

@willdurand
Owner

Ah I am stupid. Yes, using the routing layer makes sense.

@denderello

Actually @meandmymonkey and I thought about a similar approach lately using a listener which does negotiation before the routing using https://github.com/willdurand/Negotiation and setting request attributes.

@lsmith77
Owner

anyone want to try and implement it? :)

@willdurand
Owner

It does not work. UrlMatcher don't use attributes.. I think we need a RequestMatcher, but don't know who to inject it into the RouterListener...

@lsmith77
Owner

what about using the default then?

@willdurand
Owner

See this: willdurand/BazingaRestExtraBundle@5880f3e

Which default?

So, the UrlMatcher is not able to check attributes other than _method and _scheme. It is not able to check "custom" attributes. That is why we need a RequestMatcher but it is not injected by default in the RouterListener, and I am not sure to know how to do that the right way.

@lsmith77
Owner

i was thinking the route defaults .. but they obviously don't exist yet .. so nevermind.
btw .. do you really think it makes sense to add this feature to that Bundle rather than adding it here?

@tobion .. maybe you can help here?

@willdurand
Owner

do you really think it makes sense to add this feature to that Bundle rather than adding it here?

Not at all! I just hacked into this bundle as it does not pollute that one, and it made things easier in my test project. This feature MUST be part of the FOSRestBundle.

So now that we know that this solution does not work, we need to find a new idea :p

@kbond

Should 2 different versions go to 2 different controllers?

Shouldn't they both go to the same controller that has something like this:

$serializer->serialize($object, 'json', SerializationContext::create()->setVersion($versionFromAcceptHeader));

?

@willdurand
Owner

@kbond you could do more stuff than changing the "view" of your data between two given versions.

@Codepadawan
@Claymm

Is there anything stable already released about a dynamic versioning support with FosRestBundle and JMSSerializer (for Until and Since annotation support)?

@lsmith77 lsmith77 closed this
@lsmith77 lsmith77 referenced this pull request
Open

[META] API versioning #890

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
11 Util/FormatNegotiatorInterface.php → Controller/Annotations/FormatPriorities.php
@@ -9,11 +9,14 @@
* file that was distributed with this source code.
*/
-namespace FOS\RestBundle\Util;
+namespace FOS\RestBundle\Controller\Annotations;
-use Symfony\Component\HttpFoundation\Request;
+use Doctrine\Common\Annotations\Annotation;
-interface FormatNegotiatorInterface
+/**
+ * Format Priorities Route annotation class.
+ * @Annotation
+ */
+class FormatPriorities extends Annotation
{
- function getBestFormat(Request $request, array $availableTypes);
}
View
6 Controller/ExceptionController.php
@@ -122,11 +122,9 @@ protected function getStatusCode($exception)
protected function getFormat(Request $request, $format)
{
$request->attributes->set('_format', $format);
- $priorities = $this->container->getParameter('fos_rest.default_priorities');
- $preferExtension = $this->container->getParameter('fos_rest.prefer_extension');
- $formatNegotiator = $this->container->get('fos_rest.format_negotiator');
+ $acceptHeaderNegotiator = $this->container->get('fos_rest.accept_header_negotiator');
- return $formatNegotiator->getBestFormat($request, $priorities, $preferExtension) ?: $format;
+ return $acceptHeaderNegotiator->getBestFormat($request) ?: $format;
}
/**
View
6 DependencyInjection/Configuration.php
@@ -60,7 +60,7 @@ public function getConfigTreeBuilder()
$this->addViewSection($rootNode);
$this->addExceptionSection($rootNode);
$this->addBodyListenerSection($rootNode);
- $this->addFormatListenerSection($rootNode);
+ $this->addRouterListenerSection($rootNode);
return $treeBuilder;
}
@@ -123,11 +123,11 @@ private function addBodyListenerSection(ArrayNodeDefinition $rootNode)
->end();
}
- private function addFormatListenerSection(ArrayNodeDefinition $rootNode)
+ private function addRouterListenerSection(ArrayNodeDefinition $rootNode)
{
$rootNode
->children()
- ->arrayNode('format_listener')
+ ->arrayNode('router_listener')
->fixXmlConfig('default_priority', 'default_priorities')
->addDefaultsIfNotSet()
->canBeUnset()
View
10 DependencyInjection/FOSRestExtension.php
@@ -88,12 +88,12 @@ public function load(array $configs, ContainerBuilder $container)
$container->setParameter($this->getAlias().'.decoders', $config['body_listener']['decoders']);
}
- if (!empty($config['format_listener'])) {
- $loader->load('format_listener.xml');
+ if (!empty($config['router_listener'])) {
+ $loader->load('router_listener.xml');
- $container->setParameter($this->getAlias().'.default_priorities', $config['format_listener']['default_priorities']);
- $container->setParameter($this->getAlias().'.prefer_extension', $config['format_listener']['prefer_extension']);
- $container->setParameter($this->getAlias().'.fallback_format', $config['format_listener']['fallback_format']);
+ $container->setParameter($this->getAlias().'.default_priorities', $config['router_listener']['default_priorities']);
+ $container->setParameter($this->getAlias().'.prefer_extension', $config['router_listener']['prefer_extension']);
+ $container->setParameter($this->getAlias().'.fallback_format', $config['router_listener']['fallback_format']);
} else {
$container->setParameter($this->getAlias().'.default_priorities', array());
}
View
104 EventListener/FormatListener.php
@@ -1,104 +0,0 @@
-<?php
-
-/*
- * This file is part of the FOSRestBundle package.
- *
- * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace FOS\RestBundle\EventListener;
-
-use Symfony\Component\HttpKernel\Event\FilterControllerEvent,
- Symfony\Component\Serializer\SerializerInterface,
- Symfony\Component\HttpKernel\Exception\HttpException,
- Symfony\Component\HttpKernel\HttpKernelInterface;
-
-use FOS\RestBundle\Response\Codes,
- FOS\RestBundle\Util\FormatNegotiatorInterface;
-
-/**
- * This listener handles Accept header format negotiations.
- *
- * @author Lukas Kahwe Smith <smith@pooteeweet.org>
- */
-class FormatListener
-{
- /**
- * @var FormatNegotiatorInterface
- */
- private $formatNegotiator;
-
- /**
- * @var array Ordered array of formats (highest priority first)
- */
- private $defaultPriorities;
-
- /**
- * @var string fallback format name
- */
- private $fallbackFormat;
-
- /**
- * @var Boolean if to consider the extension last or first
- */
- private $preferExtension;
-
- /**
- * Initialize FormatListener.
- *
- * @param FormatNegotiatorInterface $formatNegotiator The content negotiator service to use
- * @param string $fallbackFormat Default fallback format
- * @param array $defaultPriorities Ordered array of formats (highest priority first)
- * @param Boolean $preferExtension If to consider the extension last or first
- */
- public function __construct(FormatNegotiatorInterface $formatNegotiator, $fallbackFormat, array $defaultPriorities = array(), $preferExtension = false)
- {
- $this->formatNegotiator = $formatNegotiator;
- $this->defaultPriorities = $defaultPriorities;
- $this->fallbackFormat = $fallbackFormat;
- $this->preferExtension = $preferExtension;
- }
-
- /**
- * Determines and sets the Request format
- *
- * @param GetResponseEvent $event The event
- */
- public function onKernelController(FilterControllerEvent $event)
- {
- $request = $event->getRequest();
-
-/*
- // TODO get priorities from the controller action
- $action = $request->attributes->get('_controller');
- $controller = $event->getController();
- $priorities =
-*/
-
- if (empty($priorities)) {
- $priorities = $this->defaultPriorities;
- }
-
- $format = null;
- if (!empty($priorities)) {
- $format = $this->formatNegotiator->getBestFormat($request, $priorities, $this->preferExtension);
- }
-
- if (null === $format) {
- $format = $this->fallbackFormat;
- }
-
- if (null === $format) {
- if ($event->getRequestType() === HttpKernelInterface::MASTER_REQUEST) {
- throw new HttpException(Codes::HTTP_NOT_ACCEPTABLE, "No matching accepted Response format could be determined");
- }
-
- return;
- }
-
- $request->setRequestFormat($format);
- }
-}
View
135 EventListener/RouterListener.php
@@ -0,0 +1,135 @@
+<?php
+
+/*
+ * This file is part of the FOSRestBundle package.
+ *
+ * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace FOS\RestBundle\EventListener;
+
+use Symfony\Component\HttpKernel\Event\GetResponseEvent,
+ Symfony\Component\HttpKernel\Exception\HttpException,
+ Symfony\Component\HttpKernel\HttpKernelInterface,
+ Symfony\Component\HttpKernel\Log\LoggerInterface,
+ Symfony\Component\Routing\RouterInterface,
+ Symfony\Component\DependencyInjection\ContainerAware;
+
+use FOS\RestBundle\Response\Codes,
+ FOS\RestBundle\Util\AcceptHeaderNegotiatorInterface;
+
+/**
+ * Initializes request attributes based on a matching route and
+ * determines the request format based on the Accept header
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ * @author Lukas Kahwe Smith <smith@pooteeweet.org>
+ */
+class RouterListener extends ContainerAware
+{
+ /**
+ * @var RouterInterface
+ */
+ private $router;
+
+ /**
+ * @var AcceptHeaderNegotiatorInterface
+ */
+ private $acceptHeaderNegotiator;
+
+ /**
+ * @var null|LoggerInterface
+ */
+ private $logger;
+
+ /**
+ * Initialize RouterListener.
+ *
+ * @param RouterInterface $router Router to map requests to controllers
+ * @param AcceptHeaderNegotiatorInterface $acceptHeaderNegotiator The content negotiator service to use
+ * @param LoggerInterface $logger Logger instance
+ */
+ public function __construct(RouterInterface $router, AcceptHeaderNegotiatorInterface $acceptHeaderNegotiator, LoggerInterface $logger = null)
+ {
+ $this->router = $router;
+ $this->acceptHeaderNegotiator = $acceptHeaderNegotiator;
+ $this->logger = $logger;
+ }
+
+ /**
+ * Determines and sets the Request format
+ *
+ * @param GetResponseEvent $event The event
+ */
+ public function onKernelRequest(GetResponseEvent $event)
+ {
+ $request = $event->getRequest();
+
+ if ($request->attributes->has('_controller')) {
+ // routing is already done
+ return;
+ }
+
+ // add attributes based on the path info (routing)
+ try {
+ $parameters = $this->router->match($request->getPathInfo());
+ if (null !== $this->logger) {
+ $this->logger->info(sprintf('Matched route "%s" (parameters: %s)', $parameters['_route'], $this->parametersToString($parameters)));
+ }
+
+ if (HttpKernelInterface::MASTER_REQUEST === $event->getRequestType()) {
+ $extension = isset($parameters['_format']) ? $parameters['_format'] : null;
+ $formatPriorities = isset($parameters['_format_priorities']) ? $parameters['_format_priorities'] : array();
+ $format = $this->acceptHeaderNegotiator->getBestFormat($request, $formatPriorities, $extension);
+
+ // TODO determine the right controller based on $format
+
+ if (null === $format) {
+ throw new HttpException(Codes::HTTP_NOT_ACCEPTABLE, "No matching accepted Response format could be determined");
+ }
+
+ if (false !== strpos($format, '/')) {
+ $format = $request->getFormat($format);
+ }
+
+ $request->setRequestFormat($format);
+ }
+
+ $request->attributes->add($parameters);
+ } catch (ResourceNotFoundException $e) {
+ $message = sprintf('No route found for "%s %s"', $request->getMethod(), $request->getPathInfo());
+
+ throw new NotFoundHttpException($message, $e);
+ } catch (MethodNotAllowedException $e) {
+ $message = sprintf('No route found for "%s %s": Method Not Allowed (Allow: %s)', $request->getMethod(), $request->getPathInfo(), strtoupper(implode(', ', $e->getAllowedMethods())));
+
+ throw new MethodNotAllowedHttpException($e->getAllowedMethods(), $message, $e);
+ }
+
+ if (HttpKernelInterface::MASTER_REQUEST === $event->getRequestType()) {
+ $context = $this->router->getContext();
+ $session = $request->getSession();
+ if ($locale = $request->attributes->get('_locale')) {
+ if ($session) {
+ $session->setLocale($locale);
+ }
+ $context->setParameter('_locale', $locale);
+ } elseif ($session) {
+ $context->setParameter('_locale', $session->getLocale());
+ }
+ }
+ }
+
+ private function parametersToString(array $parameters)
+ {
+ $pieces = array();
+ foreach ($parameters as $key => $val) {
+ $pieces[] = sprintf('"%s": "%s"', $key, (is_string($val) ? $val : json_encode($val)));
+ }
+
+ return implode(', ', $pieces);
+ }
+}
View
10 README.md
@@ -249,7 +249,7 @@ For example, below you can see how to disable all listeners:
# app/config/config.yml
fos_rest:
body_listener: false
- format_listener: false
+ router_listener: false
view:
view_response_listener: false
```
@@ -353,9 +353,9 @@ fos_rest:
Your custom decoder service must use a class that implements the
``FOS\RestBundle\Decoder\DecoderInterface``.
-### Format listener
+### Router Listener
-The Request format listener attempts to determine the best format for the request based on
+The Request router listener attempts to determine the best format for the request based on
the Request's Accept-Header and the format priority configuration. This way it becomes
possible to leverage Accept-Headers to determine the request format, rather than a file
extension (like foo.json).
@@ -378,7 +378,7 @@ while adding '*/*' to the priorities will effectively cause any priority to matc
```yaml
# app/config/config.yml
fos_rest:
- format_listener:
+ router_listener:
default_priorities: ['json', html, '*/*']
fallback_format: json
prefer_extension: true
@@ -772,7 +772,7 @@ fos_rest:
decoders:
json: fos_rest.decoder.json
xml: fos_rest.decoder.xml
- format_listener:
+ router_listener:
default_priorities: [html, '*/*']
fallback_format: html
prefer_extension: true
View
18 Resources/config/format_listener.xml
@@ -1,18 +0,0 @@
-<?xml version="1.0" ?>
-
-<container xmlns="http://symfony.com/schema/dic/services"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
-
- <services>
-
- <service id="fos_rest.format_listener" class="FOS\RestBundle\EventListener\FormatListener">
- <tag name="kernel.event_listener" event="kernel.controller" method="onKernelController" />
- <argument type="service" id="fos_rest.format_negotiator" />
- <argument>%fos_rest.fallback_format%</argument>
- <argument>%fos_rest.default_priorities%</argument>
- <argument>%fos_rest.prefer_extension%</argument>
- </service>
-
- </services>
-</container>
View
21 Resources/config/router_listener.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" ?>
+
+<container xmlns="http://symfony.com/schema/dic/services"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
+
+ <services>
+
+ <service id="fos_rest.router_listener" class="FOS\RestBundle\EventListener\RouterListener">
+ <tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="1" />
+ <tag name="monolog.logger" channel="request" />
+ <argument type="service" id="router" />
+ <argument type="service" id="fos_rest.accept_header_negotiator" />
+ <argument type="service" id="logger" on-invalid="ignore" />
+ <call method="setContainer">
+ <argument type="service" id="service_container" />
+ </call>
+ </service>
+
+ </services>
+</container>
View
8 Resources/config/util.xml
@@ -3,10 +3,14 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
- <parameter key="fos_rest.format_negotiator.class">FOS\RestBundle\Util\FormatNegotiator</parameter>
+ <parameter key="fos_rest.accept_header_negotiator.class">FOS\RestBundle\Util\AcceptHeaderNegotiator</parameter>
</parameters>
<services>
- <service id="fos_rest.format_negotiator" class="%fos_rest.format_negotiator.class%" public="true" />
+ <service id="fos_rest.accept_header_negotiator" class="%fos_rest.accept_header_negotiator.class%" public="true">
+ <argument>%fos_rest.fallback_format%</argument>
+ <argument>%fos_rest.default_priorities%</argument>
+ <argument>%fos_rest.prefer_extension%</argument>
+ </service>
</services>
</container>
View
24 Routing/Loader/RestRouteLoader.php
@@ -40,6 +40,7 @@ class RestRouteLoader implements LoaderInterface
protected $parents = array();
protected $prefix;
protected $namePrefix;
+ protected $formatPriorities;
protected $defaultFormat;
/**
@@ -107,6 +108,16 @@ public function setRouteNamesPrefix($namePrefix)
}
/**
+ * Set route format priorities.
+ *
+ * @param array $formatPriorities Route format priorities
+ */
+ public function setRouteFormatPriorities(array $formatPriorities)
+ {
+ $this->formatPriorities = $formatPriorities;
+ }
+
+ /**
* Loads a Routes collection by parsing Controller method names.
*
* @param string $controller Some identifier for the controller
@@ -166,6 +177,13 @@ public function load($controller, $type = null)
$this->namePrefix = $namePrefix->value;
}
+ $formatPrioritiesAnnotationClass = 'FOS\RestBundle\Controller\Annotations\FormatPriorities';
+ $formatPriorities = $this->reader->getClassAnnotation($class, $formatPrioritiesAnnotationClass);
+ if ($formatPriorities) {
+ $this->formatPriorities = explode(',', $formatPriorities->value);
+ array_walk($this->formatPriorities, function(&$val){$val = trim($val);});
+ }
+
// Trim "/" at the start
if (null !== $this->prefix && isset($this->prefix[0]) && '/' === $this->prefix[0]) {
$this->prefix = substr($this->prefix, 1);
@@ -292,6 +310,11 @@ public function load($controller, $type = null)
break;
}
}
+
+ if (!empty($this->formatPriorities)) {
+ $defaults['_format_priorities'] = $this->formatPriorities;
+ }
+
//Adding in the optional _format param for serialization
$pattern .= ".{_format}";
@@ -314,6 +337,7 @@ public function load($controller, $type = null)
$this->prefix = null;
$this->namePrefix = null;
+ $this->formatPriorities = null;
return $collection;
}
View
10 Tests/DependencyInjection/FOSRestExtensionTest.php
@@ -61,21 +61,21 @@ public function testLoadBodyListenerWithDefaults()
$this->assertParameter($decoders, 'fos_rest.decoders');
}
- public function testDisableFormatListener()
+ public function testDisableRouterListener()
{
$config = array(
- 'fos_rest' => array('format_listener' => false)
+ 'fos_rest' => array('router_listener' => false)
);
$this->extension->load($config, $this->container);
- $this->assertFalse($this->container->hasDefinition('fos_rest.format_listener'));
+ $this->assertFalse($this->container->hasDefinition('fos_rest.router_listener'));
}
- public function testLoadFormatListenerWithDefaults()
+ public function testLoadRouterListenerWithDefaults()
{
$this->extension->load(array(), $this->container);
- $this->assertTrue($this->container->hasDefinition('fos_rest.format_listener'));
+ $this->assertTrue($this->container->hasDefinition('fos_rest.router_listener'));
$this->assertParameter(array('html', '*/*'), 'fos_rest.default_priorities');
$this->assertParameter('html', 'fos_rest.fallback_format');
}
View
46 Tests/EventListener/FormatListenerTest.php → Tests/EventListener/RouterListenerTest.php
@@ -14,18 +14,18 @@
use Symfony\Component\HttpKernel\HttpKernelInterface,
Symfony\Component\HttpFoundation\Request;
-use FOS\RestBundle\EventListener\FormatListener;
+use FOS\RestBundle\EventListener\RouterListener;
/**
* Request listener test
*
* @author Lukas Kahwe Smith <smith@pooteeweet.org>
*/
-class FormatListenerTest extends \PHPUnit_Framework_TestCase
+class RouterListenerTest extends \PHPUnit_Framework_TestCase
{
- public function testOnKernelControllerNegotiation()
+ public function testOnKernelRequestNegotiation()
{
- $event = $this->getMockBuilder('\Symfony\Component\HttpKernel\Event\FilterControllerEvent')->disableOriginalConstructor()->getMock();
+ $event = $this->getMockBuilder('\Symfony\Component\HttpKernel\Event\GetResponseEvent')->disableOriginalConstructor()->getMock();
$request = new Request();
@@ -33,18 +33,18 @@ public function testOnKernelControllerNegotiation()
->method('getRequest')
->will($this->returnValue($request));
- $formatNegotiator = $this->getMockBuilder('FOS\RestBundle\Util\FormatNegotiator')->disableOriginalConstructor()->getMock();
+ $acceptHeaderNegotiator = $this->getMockBuilder('FOS\RestBundle\Util\AcceptHeaderNegotiator')->disableOriginalConstructor()->getMock();
- $listener = new FormatListener($formatNegotiator, 'xml', array());
+ $listener = new RouterListener($acceptHeaderNegotiator, 'xml', array());
- $listener->onKernelController($event);
+ $listener->onKernelRequest($event);
$this->assertEquals($request->getRequestFormat(), 'xml');
}
- public function testOnKernelControllerDefault()
+ public function testOnKernelRequestDefault()
{
- $event = $this->getMockBuilder('\Symfony\Component\HttpKernel\Event\FilterControllerEvent')->disableOriginalConstructor()->getMock();
+ $event = $this->getMockBuilder('\Symfony\Component\HttpKernel\Event\GetResponseEvent')->disableOriginalConstructor()->getMock();
$request = new Request();
@@ -52,21 +52,21 @@ public function testOnKernelControllerDefault()
->method('getRequest')
->will($this->returnValue($request));
- $formatNegotiator = $this->getMockBuilder('FOS\RestBundle\Util\FormatNegotiator')->disableOriginalConstructor()->getMock();
- $formatNegotiator->expects($this->once())
+ $acceptHeaderNegotiator = $this->getMockBuilder('FOS\RestBundle\Util\AcceptHeaderNegotiator')->disableOriginalConstructor()->getMock();
+ $acceptHeaderNegotiator->expects($this->once())
->method('getBestFormat')
->will($this->returnValue('xml'));
- $listener = new FormatListener($formatNegotiator, null, array('json'));
+ $listener = new RouterListener($acceptHeaderNegotiator, null, array('json'));
- $listener->onKernelController($event);
+ $listener->onKernelRequest($event);
$this->assertEquals($request->getRequestFormat(), 'xml');
}
- public function testOnKernelControllerNoFormat()
+ public function testOnKernelRequestNoFormat()
{
- $event = $this->getMockBuilder('\Symfony\Component\HttpKernel\Event\FilterControllerEvent')->disableOriginalConstructor()->getMock();
+ $event = $this->getMockBuilder('\Symfony\Component\HttpKernel\Event\GetResponseEvent')->disableOriginalConstructor()->getMock();
$request = new Request();
@@ -78,11 +78,11 @@ public function testOnKernelControllerNoFormat()
->method('getRequestType')
->will($this->returnValue(HttpKernelInterface::SUB_REQUEST));
- $formatNegotiator = $this->getMockBuilder('FOS\RestBundle\Util\FormatNegotiator')->disableOriginalConstructor()->getMock();
+ $acceptHeaderNegotiator = $this->getMockBuilder('FOS\RestBundle\Util\AcceptHeaderNegotiator')->disableOriginalConstructor()->getMock();
- $listener = new FormatListener($formatNegotiator, null, array());
+ $listener = new RouterListener($acceptHeaderNegotiator, null, array());
- $listener->onKernelController($event);
+ $listener->onKernelRequest($event);
$this->assertEquals('html', $request->getRequestFormat());
}
@@ -90,9 +90,9 @@ public function testOnKernelControllerNoFormat()
/**
* @expectedException \Symfony\Component\HttpKernel\Exception\HttpException
*/
- public function testOnKernelControllerException()
+ public function testOnKernelRequestException()
{
- $event = $this->getMockBuilder('\Symfony\Component\HttpKernel\Event\FilterControllerEvent')->disableOriginalConstructor()->getMock();
+ $event = $this->getMockBuilder('\Symfony\Component\HttpKernel\Event\GetResponseEvent')->disableOriginalConstructor()->getMock();
$request = new Request();
@@ -104,10 +104,10 @@ public function testOnKernelControllerException()
->method('getRequestType')
->will($this->returnValue(HttpKernelInterface::MASTER_REQUEST));
- $formatNegotiator = $this->getMockBuilder('FOS\RestBundle\Util\FormatNegotiator')->disableOriginalConstructor()->getMock();
+ $acceptHeaderNegotiator = $this->getMockBuilder('FOS\RestBundle\Util\AcceptHeaderNegotiator')->disableOriginalConstructor()->getMock();
- $listener = new FormatListener($formatNegotiator, null, array());
+ $listener = new RouterListener($acceptHeaderNegotiator, null, array());
- $listener->onKernelController($event);
+ $listener->onKernelRequest($event);
}
}
View
12 Tests/Util/FormatNegotiatorTest.php → Tests/Util/AcceptHeaderNegotiatorTest.php
@@ -11,11 +11,11 @@
namespace FOS\RestBundle\Tests\Util;
-use FOS\RestBundle\Util\FormatNegotiator;
+use FOS\RestBundle\Util\AcceptHeaderNegotiator;
use Symfony\Component\HttpFoundation\Request;
-class FormatNegotiatorTest extends \PHPUnit_Framework_TestCase
+class AcceptHeaderNegotiatorTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider getData
@@ -24,11 +24,10 @@ public function testGetBestFormat($acceptHeader, $format, $priorities, $preferEx
{
$request = new Request();
$request->headers->set('Accept', $acceptHeader);
- $request->attributes->set('_format', $format);
- $formatNegotiator = new FormatNegotiator();
+ $acceptHeaderNegotiator = new AcceptHeaderNegotiator(null, array(), $preferExtension);
- $this->assertEquals($expected, $formatNegotiator->getBestFormat($request, $priorities, $preferExtension));
+ $this->assertEquals($expected, $acceptHeaderNegotiator->getBestFormat($request, $priorities, $format));
}
public function getData()
@@ -46,4 +45,5 @@ public function getData()
array('text/html,application/xhtml+xml,application/xml;q=0.9,*/*', null, array('json'), false, 'json'),
array('text/html,application/xhtml+xml,application/xml', null, array('json'), false, null),
);
- }}
+ }
+}
View
136 Util/AcceptHeaderNegotiator.php
@@ -0,0 +1,136 @@
+<?php
+
+/*
+ * This file is part of the FOSRestBundle package.
+ *
+ * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace FOS\RestBundle\Util;
+
+use Symfony\Component\HttpFoundation\Request;
+
+class AcceptHeaderNegotiator implements AcceptHeaderNegotiatorInterface
+{
+ /**
+ * @var array Ordered array of formats (highest priority first)
+ */
+ private $defaultPriorities;
+
+ /**
+ * @var string fallback format name
+ */
+ private $fallbackFormat;
+
+ /**
+ * @var Boolean if to consider the extension last or first
+ */
+ private $preferExtension;
+
+ /**
+ * Initialize AcceptHeaderNegotiator.
+ *
+ * @param string $fallbackFormat Default fallback format
+ * @param array $defaultPriorities Ordered array of formats (highest priority first)
+ * @param Boolean $preferExtension If to consider the extension last or first
+ */
+ public function __construct($fallbackFormat, array $defaultPriorities = array(), $preferExtension = false)
+ {
+ $this->defaultPriorities = $defaultPriorities;
+ $this->fallbackFormat = $fallbackFormat;
+ $this->preferExtension = $preferExtension;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getBestFormat(Request $request, array $priorities = null, $extension = null)
+ {
+ if (empty($priorities)) {
+ $priorities = $this->defaultPriorities;
+ }
+
+ $mimeTypes = $request->splitHttpAcceptHeader($request->headers->get('Accept'), true);
+
+ if (null !== $extension && $request->getMimeType($extension)) {
+ if ($this->preferExtension) {
+ $q = reset($mimeTypes);
+ $mimeTypes = array($request->getMimeType($extension) => $q+1) + $mimeTypes;
+ } else {
+ $q = end($mimeTypes);
+ $mimeTypes[$request->getMimeType($extension)] = $q - 1;
+ }
+ }
+
+ if (empty($mimeTypes)) {
+ return null;
+ }
+
+ // TODO also handle foo/*
+ $catchAllEnabled = in_array('*/*', $priorities);
+ $format = $this->getFormatByPriorities($request, $mimeTypes, $priorities, $catchAllEnabled);
+
+ if (null === $format) {
+ $format = $this->fallbackFormat;
+ }
+
+ return $format;
+ }
+
+ /**
+ * Get the format applying the supplied priorities to the mime types
+ *
+ * @param Request $request The request
+ * @param array $mimeTypes Ordered array of mimetypes as keys with priroties s values
+ * @param array $priorities Ordered array of formats (highest priority first)
+ * @param Boolean $catchAllEnabled If there is a catch all priority
+ *
+ * @return null|string The format string
+ */
+ protected function getFormatByPriorities($request, $mimeTypes, $priorities, $catchAllEnabled = false)
+ {
+ $max = reset($mimeTypes);
+ $keys = array_keys($mimeTypes, $max);
+
+ $formats = array();
+ foreach ($keys as $mimeType) {
+ unset($mimeTypes[$mimeType]);
+ if ($mimeType === '*/*') {
+ return reset($priorities);
+ }
+
+ $priority = array_search($mimeType, $priorities);
+ if (false !== $priority) {
+ $formats[$mimeType] = $priority;
+ continue;
+ }
+
+ $format = $request->getFormat($mimeType);
+ if (null !== $format) {
+ $priority = array_search($format, $priorities);
+ if (false !== $priority) {
+ $formats[$format] = $priority;
+ continue;
+ }
+ }
+
+ if ($catchAllEnabled) {
+ $formats[$mimeType] = count($priorities);
+ }
+ }
+
+ if (!empty($formats)) {
+ asort($formats);
+ return key($formats);
+ }
+
+ if (!empty($mimeTypes)) {
+ return $this->getFormatByPriorities($request, $mimeTypes, $priorities);
+ }
+
+ return null;
+ }
+}
View
28 Util/AcceptHeaderNegotiatorInterface.php
@@ -0,0 +1,28 @@
+<?php
+
+/*
+ * This file is part of the FOSRestBundle package.
+ *
+ * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace FOS\RestBundle\Util;
+
+use Symfony\Component\HttpFoundation\Request;
+
+interface AcceptHeaderNegotiatorInterface
+{
+ /**
+ * Detect the request format based on the priorities and the Accept header
+ *
+ * @param Request $request The request
+ * @param array $priorities Ordered array of formats (highest priority first)
+ * @param string $extension The request "file" extension
+ *
+ * @return null|string The format string
+ */
+ function getBestFormat(Request $request, array $priorities = null, $extension = null);
+}
View
89 Util/FormatNegotiator.php
@@ -1,89 +0,0 @@
-<?php
-
-/*
- * This file is part of the FOSRestBundle package.
- *
- * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace FOS\RestBundle\Util;
-
-use Symfony\Component\HttpFoundation\Request;
-
-class FormatNegotiator implements FormatNegotiatorInterface
-{
- /**
- * Detect the request format based on the priorities and the Accept header
- *
- * Note: Request "_format" parameter is considered the preferred Accept header
- *
- * @param Request $request The request
- * @param array $priorities Ordered array of formats (highest priority first)
- * @param Boolean $preferExtension If to consider the extension last or first
- *
- * @return void|string The format string
- */
- public function getBestFormat(Request $request, array $priorities, $preferExtension = false)
- {
- $mimetypes = $request->splitHttpAcceptHeader($request->headers->get('Accept'));
-
- $extension = $request->get('_format');
- if (null !== $extension && $request->getMimeType($extension)) {
- $mimetypes[$request->getMimeType($extension)] = $preferExtension
- ? reset($mimetypes)+1
- : end($mimetypes)-1;
- arsort($mimetypes);
- }
-
- if (empty($mimetypes)) {
- return null;
- }
-
- $catchAllEnabled = in_array('*/*', $priorities);
- return $this->getFormatByPriorities($request, $mimetypes, $priorities, $catchAllEnabled);
- }
-
- /**
- * Get the format applying the supplied priorities to the mime types
- *
- * @param Request $request The request
- * @param array $mimetypes Ordered array of mimetypes as keys with priroties s values
- * @param array $priorities Ordered array of formats (highest priority first)
- * @param Boolean $catchAllEnabled If there is a catch all priority
- *
- * @return void|string The format string
- */
- protected function getFormatByPriorities($request, $mimetypes, $priorities, $catchAllEnabled = false)
- {
- $max = reset($mimetypes);
- $keys = array_keys($mimetypes, $max);
-
- $formats = array();
- foreach ($keys as $mimetype) {
- unset($mimetypes[$mimetype]);
- if ($mimetype === '*/*') {
- return reset($priorities);
- }
- $format = $request->getFormat($mimetype);
- if ($format) {
- $priority = array_search($format, $priorities);
- if (false !== $priority) {
- $formats[$format] = $priority;
- } elseif ($catchAllEnabled) {
- $formats[$format] = count($priorities);
- }
- }
- }
-
- if (empty($formats) && !empty($mimetypes)) {
- return $this->getFormatByPriorities($request, $mimetypes, $priorities);
- }
-
- asort($formats);
-
- return key($formats);
- }
-}
Something went wrong with that request. Please try again.