.gitignore 0000666 00000000075 13052362131 0006536 0 ustar 00 phpunit.xml
vendor
composer.lock
composer.phar
.php_cs.cache
.php_cs 0000666 00000001115 13052362131 0006017 0 ustar 00 in(__DIR__)
->exclude('features/fixtures/TestApp/cache')
;
$header = <<
For the full copyright and license information, please view the LICENSE
file that was distributed with this source code.
EOF;
HeaderCommentFixer::setHeader($header);
return Symfony\CS\Config\Config::create()
->setUsingCache(true)
->fixers(['-psr0'])
->finder($finder)
;
.styleci.yml 0000666 00000000045 13052362131 0007020 0 ustar 00 preset: symfony
disabled:
- braces
.travis.yml 0000666 00000002075 13052362131 0006661 0 ustar 00 language: php
php:
- 5.5
- 5.6
- 7.0
- hhvm
sudo: false
cache:
directories:
- $HOME/.composer/cache/files
branches:
only:
- master
- /^\d+\.\d+$/
matrix:
fast_finish: true
include:
- php: 5.5
env: COMPOSER_FLAGS="--prefer-lowest" SYMFONY_DEPRECATIONS_HELPER=weak
- php: 7.0
env: SYMFONY_VERSION='2.8.*'
- php: 7.0
env: SYMFONY_VERSION='3.0.*'
- php: 7.0
env: SYMFONY_VERSION='3.1.*'
before_install:
- if [[ "$TRAVIS_PHP_VERSION" != "5.6" && "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then phpenv config-rm xdebug.ini; fi
- composer self-update
- if [ "$SYMFONY_VERSION" != "" ]; then composer require --dev --no-update symfony/symfony=$SYMFONY_VERSION; fi
install: composer update $COMPOSER_FLAGS --prefer-dist
script: if [ "$TRAVIS_PHP_VERSION" == "5.6" ]; then phpunit --coverage-clover=coverage.clover; else phpunit; fi
after_script:
- if [ "$TRAVIS_PHP_VERSION" == "5.6" ]; then wget https://scrutinizer-ci.com/ocular.phar && php ocular.phar code-coverage:upload --format=php-clover coverage.clover; fi
CHANGELOG.md 0000666 00000010447 13052362131 0006363 0 ustar 00 CHANGELOG
=========
1.8.0
-----
* added a new `InvalidParameterException` as a specialization of the `BadRequestHttpException`
* deprecated the `FOS\RestBundle\Util\ViolationFormatter` class and the
`FOS\RestBundle\Util\ViolationFormatterInterface`
* deprecated the `ViolationFormatterInterface` argument of the `ParamFetcher` class constructor
* deprecated the `RedirectView` and `RouteRedirectView` classes, use `View::createRedirect()` and
`View::createRouteRedirect()` instead
* added a `fos_rest.exception.debug` config option that defaults to the `kernel.debug` container
parameter and can be turned on to include the caught exception message in the exception controller's
response
* introduced the concept of REST zones which makes it possible to disable all REST listeners
when a request matches certain attributes
* fixed that serialization groups are always passed to the constructor as an array
* added annotations to support additional HTTP methods defined by RFC 2518 (WebDAV)
* added a new loader that allows to extract REST routes from all controller classes from a
directory
* introduced a serializer adapter layer to ease the integration of custom serialization
implementations
* deprecated the getter methods of the `ViewHandler` class
* fixed an issue that prevented decoration of the `TemplateReferenceInterface` from the Symfony
Templating component
* fixed: no longer overwrite an explicitly configured template in the view response listener
* added support for API versioning in URL parameters, the `Accept` header or using a custom header
* marked some classes and methods as internal, do no longer use them in your code as they are likely
to be removed in future releases
* deprecated the `DoctrineInflector` class and the `InflectorInterface` from the
`FOS\RestBundle\Util\Inflector`in favor of their replacements in the `FOS\RestBundle\Inflector`
namespace
* deprecated the `FormatNegotiator` class and the `FormatNegotiatorInterface` from the
`FOS\RestBundle\Util` namespace in favor of the new `FOS\RestBundle\Negotiation\FormatNegotiator`
class
* deprecated the `FOS\RestBundle\Util\MediaTypeNegotiatorInterface` which should no longer be used
1.7.9
-----
* handle `\Throwable` instances in the `ExceptionController`
* fixed that the default exclusion strategy groups for the serializer are not the empty string
* fixed a BC break that prevented the `CamelKeysNormalizer` from removing leading underscores
* fixed the `AllowedMethodsRouteLoader` to work with Symfony 3.0
1.7.8
-----
* removed uses of the reflection API in favor of faster solutions when possible
* fixed the configuration to use serialization groups and versions at the same time
1.7.7
-----
* when using Symfony 3.x, the bundle doesn't call methods anymore that have been deprecated in
Symfony 2.x and were removed in Symfony 3.0
* the `ViewResponseListener` does not overwrite explicitly configured templates anymore
* fixed the `ParamFetcher` class to properly handle sub requests
1.7.6
-----
* added a `CamelKeysNormalizerWithLeadingUnderscore` that keeps leading underscores when
converting snake case to camel case (for example, leaving `_username` unchanged)
1.7.5
-----
**CAUTION:** Accidentally, this patch release was never published.
1.7.4
-----
* removed some code from the `ViewResponseListener` class that was already present in the parent
`TemplateListener` class
1.7.3
-----
* made it possible to use the bundle with Symfony 3.x and fixed some compatibility issues with
Symfony 3.0
* fixed the exception controller to return a 406 (Not Acceptable) response when the format
negotiator throws an exception
1.7.2
-----
* fixed loading XML schema definition files in case the paths contain special characters (like
spaces)
* return the FQCN in the form type extension's `getExtendedType()` method to be compatible with
Symfony >= 2.8
* added the `extended-type` attribute to the `form.type_extension` tag to be compatible with
Symfony >= 2.8
* fixed some code examples in the documentation
* fixed exception message when using non-numeric identifiers (like UUID or GUID)
* allow version 1.x of `jms/serializer` and `jms/serializer-bundle`
* allow to use the Symfony serializer even if the JMS serializer is present
1.7.1
-----
* fix regression when handling methods in `@Route` annotations
Context/Context.php 0000666 00000012017 13052362131 0010326 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Context;
/**
* Stores the serialization or deserialization context (groups, version, ...).
*
* @author Ener-Getick
*/
final class Context
{
/**
* @var array
*/
private $attributes = array();
/**
* @var int|null
*/
private $version;
/**
* @var array|null
*/
private $groups;
/**
* @var int
*/
private $maxDepth;
/**
* @var bool
*/
private $isMaxDepthEnabled;
/**
* @var bool
*/
private $serializeNull;
/**
* Sets an attribute.
*
* @param string $key
* @param mixed $value
*
* @return self
*/
public function setAttribute($key, $value)
{
$this->attributes[$key] = $value;
return $this;
}
/**
* Checks if contains a normalization attribute.
*
* @param string $key
*
* @return bool
*/
public function hasAttribute($key)
{
return isset($this->attributes[$key]);
}
/**
* Gets an attribute.
*
* @param string $key
*
* @return mixed
*/
public function getAttribute($key)
{
if (isset($this->attributes[$key])) {
return $this->attributes[$key];
}
}
/**
* Gets the attributes.
*
* @return array
*/
public function getAttributes()
{
return $this->attributes;
}
/**
* Sets the normalization version.
*
* @param int|null $version
*
* @return self
*/
public function setVersion($version)
{
$this->version = $version;
return $this;
}
/**
* Gets the normalization version.
*
* @return int|null
*/
public function getVersion()
{
return $this->version;
}
/**
* Adds a normalization group.
*
* @param string $group
*
* @return self
*/
public function addGroup($group)
{
if (null === $this->groups) {
$this->groups = [];
}
if (!in_array($group, $this->groups)) {
$this->groups[] = $group;
}
return $this;
}
/**
* Adds normalization groups.
*
* @param string[] $groups
*
* @return self
*/
public function addGroups(array $groups)
{
foreach ($groups as $group) {
$this->addGroup($group);
}
return $this;
}
/**
* Gets the normalization groups.
*
* @return string[]|null
*/
public function getGroups()
{
return $this->groups;
}
/**
* Set the normalization groups.
*
* @param string[]|null $groups
*
* @return self
*/
public function setGroups(array $groups = null)
{
$this->groups = $groups;
return $this;
}
/**
* Sets the normalization max depth.
*
* @param int|null $maxDepth
*
* @return self
*
* @deprecated since 2.1, to be removed in 3.0. Use {@link self::enableMaxDepth()} and {@link self::disableMaxDepth()} instead
*/
public function setMaxDepth($maxDepth)
{
if (1 === func_num_args() || func_get_arg(1)) {
@trigger_error(sprintf('%s is deprecated since version 2.1 and will be removed in 3.0. Use %s::enableMaxDepth() and %s::disableMaxDepth() instead.', __METHOD__, __CLASS__, __CLASS__), E_USER_DEPRECATED);
}
$this->maxDepth = $maxDepth;
return $this;
}
/**
* Gets the normalization max depth.
*
* @return int|null
*
* @deprecated since version 2.1, to be removed in 3.0. Use {@link self::isMaxDepthEnabled()} instead
*/
public function getMaxDepth()
{
if (0 === func_num_args() || func_get_arg(0)) {
@trigger_error(sprintf('%s is deprecated since version 2.1 and will be removed in 3.0. Use %s::isMaxDepthEnabled() instead.', __METHOD__, __CLASS__), E_USER_DEPRECATED);
}
return $this->maxDepth;
}
public function enableMaxDepth()
{
$this->isMaxDepthEnabled = true;
return $this;
}
public function disableMaxDepth()
{
$this->isMaxDepthEnabled = false;
return $this;
}
/**
* @return bool|null
*/
public function isMaxDepthEnabled()
{
return $this->isMaxDepthEnabled;
}
/**
* Sets serialize null.
*
* @param bool|null $serializeNull
*
* @return self
*/
public function setSerializeNull($serializeNull)
{
$this->serializeNull = $serializeNull;
return $this;
}
/**
* Gets serialize null.
*
* @return bool|null
*/
public function getSerializeNull()
{
return $this->serializeNull;
}
}
Controller/Annotations/AbstractParam.php 0000666 00000003402 13052362131 0014420 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
use Symfony\Component\Validator\Constraints;
/**
* {@inheritdoc}
*
* @author Jordi Boggiano
* @author Boris Guéry
* @author Ener-Getick
*/
abstract class AbstractParam implements ParamInterface
{
/** @var string */
public $name;
/** @var string */
public $key;
/** @var mixed */
public $default;
/** @var string */
public $description;
/** @var bool */
public $strict = false;
/** @var bool */
public $nullable = false;
/** @var array */
public $incompatibles = array();
/** {@inheritdoc} */
public function getName()
{
return $this->name;
}
/** {@inheritdoc} */
public function getDefault()
{
return $this->default;
}
/** {@inheritdoc} */
public function getDescription()
{
return $this->description;
}
/** {@inheritdoc} */
public function getIncompatibilities()
{
return $this->incompatibles;
}
/** {@inheritdoc} */
public function getConstraints()
{
$constraints = array();
if (!$this->nullable) {
$constraints[] = new Constraints\NotNull();
}
return $constraints;
}
/** {@inheritdoc} */
public function isStrict()
{
return $this->strict;
}
/**
* @return string
*/
protected function getKey()
{
return $this->key ?: $this->name;
}
}
Controller/Annotations/AbstractScalarParam.php 0000666 00000004145 13052362131 0015553 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
use FOS\RestBundle\Validator\Constraints\Regex;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\All;
/**
* {@inheritdoc}
*
* @author Ener-Getick
*/
abstract class AbstractScalarParam extends AbstractParam
{
/** @var mixed */
public $requirements = null;
/** @var bool */
public $map = false;
/** @var bool */
public $allowBlank = true;
/** {@inheritdoc} */
public function getConstraints()
{
$constraints = parent::getConstraints();
if ($this->requirements instanceof Constraint) {
$constraints[] = $this->requirements;
} elseif (is_scalar($this->requirements)) {
$constraints[] = new Regex(array(
'pattern' => '#^(?:'.$this->requirements.')$#xsu',
'message' => sprintf(
'Parameter \'%s\' value, does not match requirements \'%s\'',
$this->getName(),
$this->requirements
),
));
} elseif (is_array($this->requirements) && isset($this->requirements['rule']) && $this->requirements['error_message']) {
$constraints[] = new Regex(array(
'pattern' => '#^(?:'.$this->requirements['rule'].')$#xsu',
'message' => $this->requirements['error_message'],
));
}
if (false === $this->allowBlank) {
$constraints[] = new NotBlank();
}
// If the user wants to map the value, apply all constraints to every
// value of the map
if ($this->map) {
$constraints = array(
new All(array('constraints' => $constraints)),
);
}
return $constraints;
}
}
Controller/Annotations/Copy.php 0000666 00000001045 13052362131 0012607 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
/**
* COPY Route annotation class.
*
* @Annotation
* @Target("METHOD")
*
* @author Maximilian Bosch
*/
class Copy extends Route
{
public function getMethod()
{
return 'COPY';
}
}
Controller/Annotations/Delete.php 0000666 00000000754 13052362131 0013105 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
/**
* DELETE Route annotation class.
*
* @Annotation
* @Target("METHOD")
*/
class Delete extends Route
{
public function getMethod()
{
return 'DELETE';
}
}
Controller/Annotations/FileParam.php 0000666 00000003374 13052362131 0013544 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints;
use Symfony\Component\Validator\Constraints\All;
/**
* Represents a file that must be present.
*
* @Annotation
* @Target("METHOD")
*
* @author Ener-Getick
*/
class FileParam extends AbstractParam
{
/** @var bool */
public $strict = true;
/** @var mixed */
public $requirements = null;
/** @var bool */
public $image = false;
/** @var bool */
public $map = false;
/**
* {@inheritdoc}
*/
public function getConstraints()
{
$constraints = parent::getConstraints();
if ($this->requirements instanceof Constraint) {
$constraints[] = $this->requirements;
}
$options = is_array($this->requirements) ? $this->requirements : array();
if ($this->image) {
$constraints[] = new Constraints\Image($options);
} else {
$constraints[] = new Constraints\File($options);
}
// If the user wants to map the value
if ($this->map) {
$constraints = array(
new All(array('constraints' => $constraints)),
);
}
return $constraints;
}
/**
* {@inheritdoc}
*/
public function getValue(Request $request, $default = null)
{
return $request->files->get($this->getKey(), $default);
}
}
Controller/Annotations/Get.php 0000666 00000000743 13052362131 0012420 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
/**
* GET Route annotation class.
*
* @Annotation
* @Target("METHOD")
*/
class Get extends Route
{
public function getMethod()
{
return 'GET';
}
}
Controller/Annotations/Head.php 0000666 00000000746 13052362131 0012545 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
/**
* HEAD Route annotation class.
*
* @Annotation
* @Target("METHOD")
*/
class Head extends Route
{
public function getMethod()
{
return 'HEAD';
}
}
Controller/Annotations/Link.php 0000666 00000000746 13052362131 0012601 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
/**
* LINK Route annotation class.
*
* @Annotation
* @Target("METHOD")
*/
class Link extends Route
{
public function getMethod()
{
return 'LINK';
}
}
Controller/Annotations/Lock.php 0000666 00000001045 13052362131 0012565 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
/**
* LOCK Route annotation class.
*
* @Annotation
* @Target("METHOD")
*
* @author Maximilian Bosch
*/
class Lock extends Route
{
public function getMethod()
{
return 'LOCK';
}
}
Controller/Annotations/Mkcol.php 0000666 00000001050 13052362131 0012736 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
/**
* MKCOL Route annotation class.
*
* @Annotation
* @Target("METHOD")
*
* @author Maximilian Bosch
*/
class Mkcol extends Route
{
public function getMethod()
{
return 'MKCOL';
}
}
Controller/Annotations/Move.php 0000666 00000001045 13052362131 0012603 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
/**
* MOVE Route annotation class.
*
* @Annotation
* @Target("METHOD")
*
* @author Maximilian Bosch
*/
class Move extends Route
{
public function getMethod()
{
return 'MOVE';
}
}
Controller/Annotations/NamePrefix.php 0000666 00000000740 13052362131 0013734 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
use Doctrine\Common\Annotations\Annotation;
/**
* NamePrefix Route annotation class.
*
* @Annotation
* @Target("CLASS")
*/
class NamePrefix extends Annotation
{
}
Controller/Annotations/NoRoute.php 0000666 00000000656 13052362131 0013277 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
/**
* No Route annotation class.
*
* @Annotation
* @Target({"METHOD","CLASS"})
*/
class NoRoute extends Route
{
}
Controller/Annotations/Options.php 0000666 00000000757 13052362131 0013341 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
/**
* OPTIONS Route annotation class.
*
* @Annotation
* @Target("METHOD")
*/
class Options extends Route
{
public function getMethod()
{
return 'OPTIONS';
}
}
Controller/Annotations/ParamInterface.php 0000666 00000002451 13052362131 0014560 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Validator\Constraint;
/**
* Represents a parameter that can be present in the request attributes.
*
* @author Ener-Getick
*/
interface ParamInterface
{
/**
* Get param name.
*
* @return string
*/
public function getName();
/**
* @return mixed
*/
public function getDefault();
/**
* @return string
*/
public function getDescription();
/**
* Get incompatibles parameters.
*
* @return array
*/
public function getIncompatibilities();
/**
* @return Constraint[]
*/
public function getConstraints();
/**
* @return bool
*/
public function isStrict();
/**
* Get param value in function of the current request.
*
* @param Request $request
* @param mixed $default value
*
* @return mixed
*/
public function getValue(Request $request, $default);
}
Controller/Annotations/Patch.php 0000666 00000000751 13052362131 0012737 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
/**
* PATCH Route annotation class.
*
* @Annotation
* @Target("METHOD")
*/
class Patch extends Route
{
public function getMethod()
{
return 'PATCH';
}
}
Controller/Annotations/Post.php 0000666 00000000746 13052362131 0012631 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
/**
* POST Route annotation class.
*
* @Annotation
* @Target("METHOD")
*/
class Post extends Route
{
public function getMethod()
{
return 'POST';
}
}
Controller/Annotations/Prefix.php 0000666 00000000730 13052362131 0013132 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
use Doctrine\Common\Annotations\Annotation;
/**
* Prefix Route annotation class.
*
* @Annotation
* @Target("CLASS")
*/
class Prefix extends Annotation
{
}
Controller/Annotations/PropFind.php 0000666 00000001061 13052362131 0013414 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
/**
* PROPFIND Route annotation class.
*
* @Annotation
* @Target("METHOD")
*
* @author Maximilian Bosch
*/
class PropFind extends Route
{
public function getMethod()
{
return 'PROPFIND';
}
}
Controller/Annotations/PropPatch.php 0000666 00000001064 13052362131 0013576 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
/**
* PROPPATCH Route annotation class.
*
* @Annotation
* @Target("METHOD")
*
* @author Maximilian Bosch
*/
class PropPatch extends Route
{
public function getMethod()
{
return 'PROPPATCH';
}
}
Controller/Annotations/Put.php 0000666 00000000743 13052362131 0012451 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
/**
* PUT Route annotation class.
*
* @Annotation
* @Target("METHOD")
*/
class Put extends Route
{
public function getMethod()
{
return 'PUT';
}
}
Controller/Annotations/QueryParam.php 0000666 00000001354 13052362131 0013766 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
use Symfony\Component\HttpFoundation\Request;
/**
* Represents a parameter that must be present in GET data.
*
* @Annotation
* @Target({"CLASS", "METHOD"})
*
* @author Alexander
*/
class QueryParam extends AbstractScalarParam
{
/**
* {@inheritdoc}
*/
public function getValue(Request $request, $default = null)
{
return $request->query->get($this->getKey(), $default);
}
}
Controller/Annotations/RequestParam.php 0000666 00000001512 13052362131 0014305 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
use Symfony\Component\HttpFoundation\Request;
/**
* Represents a parameter that must be present in POST data.
*
* @Annotation
* @Target("METHOD")
*
* @author Jordi Boggiano
* @author Boris Guéry
*/
class RequestParam extends AbstractScalarParam
{
/** @var bool */
public $strict = true;
/**
* {@inheritdoc}
*/
public function getValue(Request $request, $default = null)
{
return $request->request->get($this->getKey(), $default);
}
}
Controller/Annotations/Route.php 0000666 00000001407 13052362131 0012775 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route as BaseRoute;
/**
* Route annotation class.
*
* @Annotation
*/
class Route extends BaseRoute
{
public function __construct(array $data)
{
parent::__construct($data);
if (!$this->getMethods()) {
$this->setMethods((array) $this->getMethod());
}
}
/**
* @return string|null
*/
public function getMethod()
{
return;
}
}
Controller/Annotations/RouteResource.php 0000666 00000001041 13052362131 0014477 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
/**
* RouteResource annotation class.
*
* @Annotation
* @Target("CLASS")
*/
class RouteResource
{
/**
* @var string required
*/
public $resource;
/**
* @var bool
*/
public $pluralize = true;
}
Controller/Annotations/Unlink.php 0000666 00000000754 13052362131 0013143 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
/**
* UNLINK Route annotation class.
*
* @Annotation
* @Target("METHOD")
*/
class Unlink extends Route
{
public function getMethod()
{
return 'UNLINK';
}
}
Controller/Annotations/Unlock.php 0000666 00000001053 13052362131 0013127 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
/**
* UNLOCK Route annotation class.
*
* @Annotation
* @Target("METHOD")
*
* @author Maximilian Bosch
*/
class Unlock extends Route
{
public function getMethod()
{
return 'UNLOCK';
}
}
Controller/Annotations/Version.php 0000666 00000000732 13052362131 0013324 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
use Doctrine\Common\Annotations\Annotation;
/**
* Version Route annotation class.
*
* @Annotation
* @Target("CLASS")
*/
class Version extends Annotation
{
}
Controller/Annotations/View.php 0000666 00000005037 13052362131 0012614 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller\Annotations;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
/**
* View annotation class.
*
* @Annotation
* @Target({"METHOD","CLASS"})
*/
class View extends Template
{
/**
* @var string
*/
protected $templateVar;
/**
* @var int
*/
protected $statusCode;
/**
* @var array
*/
protected $serializerGroups;
/**
* @var bool
*/
protected $populateDefaultVars = true;
/**
* @var bool
*/
protected $serializerEnableMaxDepthChecks;
/**
* Sets the template var name to be used for templating formats.
*
* @param string $templateVar
*/
public function setTemplateVar($templateVar)
{
$this->templateVar = $templateVar;
}
/**
* Returns the template var name to be used for templating formats.
*
* @return string
*/
public function getTemplateVar()
{
return $this->templateVar;
}
/**
* @param int $statusCode
*/
public function setStatusCode($statusCode)
{
$this->statusCode = $statusCode;
}
/**
* @return int
*/
public function getStatusCode()
{
return $this->statusCode;
}
/**
* @param array $serializerGroups
*/
public function setSerializerGroups($serializerGroups)
{
$this->serializerGroups = $serializerGroups;
}
/**
* @return array
*/
public function getSerializerGroups()
{
return $this->serializerGroups;
}
/**
* @param bool $populateDefaultVars
*/
public function setPopulateDefaultVars($populateDefaultVars)
{
$this->populateDefaultVars = (bool) $populateDefaultVars;
}
/**
* @return bool
*/
public function isPopulateDefaultVars()
{
return $this->populateDefaultVars;
}
/**
* @param bool $serializerEnableMaxDepthChecks
*/
public function setSerializerEnableMaxDepthChecks($serializerEnableMaxDepthChecks)
{
$this->serializerEnableMaxDepthChecks = $serializerEnableMaxDepthChecks;
}
/**
* @return bool
*/
public function getSerializerEnableMaxDepthChecks()
{
return $this->serializerEnableMaxDepthChecks;
}
}
Controller/ControllerTrait.php 0000666 00000005725 13052362131 0012540 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller;
use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Symfony\Component\HttpFoundation\Response;
/**
* Trait for Controllers using the View functionality of FOSRestBundle.
*
* @author Benjamin Eberlei
* @author Lukas Kahwe Smith
*/
trait ControllerTrait
{
/**
* @var ViewHandlerInterface
*/
private $viewhandler;
/**
* Get the ViewHandler.
*
* @param ViewHandlerInterface $viewhandler
*/
public function setViewHandler(ViewHandlerInterface $viewhandler)
{
$this->viewhandler = $viewhandler;
}
/**
* Get the ViewHandler.
*
* @return ViewHandlerInterface
*/
protected function getViewHandler()
{
if (!$this->viewhandler instanceof ViewHandlerInterface) {
throw new \RuntimeException('A "ViewHandlerInterface" instance must be set when using the FOSRestBundle "ControllerTrait".');
}
return $this->viewhandler;
}
/**
* Creates a view.
*
* Convenience method to allow for a fluent interface.
*
* @param mixed $data
* @param int $statusCode
* @param array $headers
*
* @return View
*/
protected function view($data = null, $statusCode = null, array $headers = [])
{
return View::create($data, $statusCode, $headers);
}
/**
* Creates a Redirect view.
*
* Convenience method to allow for a fluent interface.
*
* @param string $url
* @param int $statusCode
* @param array $headers
*
* @return View
*/
protected function redirectView($url, $statusCode = Response::HTTP_FOUND, array $headers = [])
{
return View::createRedirect($url, $statusCode, $headers);
}
/**
* Creates a Route Redirect View.
*
* Convenience method to allow for a fluent interface.
*
* @param string $route
* @param mixed $parameters
* @param int $statusCode
* @param array $headers
*
* @return View
*/
protected function routeRedirectView($route, array $parameters = [], $statusCode = Response::HTTP_CREATED, array $headers = [])
{
return View::createRouteRedirect($route, $parameters, $statusCode, $headers);
}
/**
* Converts view into a response object.
*
* Not necessary to use, if you are using the "ViewResponseListener", which
* does this conversion automatically in kernel event "onKernelView".
*
* @param View $view
*
* @return Response
*/
protected function handleView(View $view)
{
return $this->getViewHandler()->handle($view);
}
}
Controller/ExceptionController.php 0000666 00000011241 13052362131 0013401 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller;
use FOS\RestBundle\Util\ExceptionValueMap;
use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
use Symfony\Component\HttpKernel\Log\DebugLoggerInterface;
/**
* Custom ExceptionController that uses the view layer and supports HTTP response status code mapping.
*/
class ExceptionController
{
/**
* @var ViewHandlerInterface
*/
private $viewHandler;
/**
* @var ExceptionValueMap
*/
private $exceptionCodes;
/**
* @var bool
*/
private $showException;
public function __construct(
ViewHandlerInterface $viewHandler,
ExceptionValueMap $exceptionCodes,
$showException
) {
$this->viewHandler = $viewHandler;
$this->exceptionCodes = $exceptionCodes;
$this->showException = $showException;
}
/**
* Converts an Exception to a Response.
*
* @param Request $request
* @param \Exception|\Throwable $exception
* @param DebugLoggerInterface|null $logger
*
* @throws \InvalidArgumentException
*
* @return Response
*/
public function showAction(Request $request, $exception, DebugLoggerInterface $logger = null)
{
$currentContent = $this->getAndCleanOutputBuffering($request->headers->get('X-Php-Ob-Level', -1));
$code = $this->getStatusCode($exception);
$templateData = $this->getTemplateData($currentContent, $code, $exception, $logger);
$view = $this->createView($exception, $code, $templateData, $request, $this->showException);
$response = $this->viewHandler->handle($view);
return $response;
}
/**
* @param \Exception $exception
* @param int $code
* @param array $templateData
* @param Request $request
* @param bool $showException
*
* @return View
*/
protected function createView(\Exception $exception, $code, array $templateData, Request $request, $showException)
{
$view = new View($exception, $code, $exception instanceof HttpExceptionInterface ? $exception->getHeaders() : []);
$view->setTemplateVar('raw_exception');
$view->setTemplateData($templateData);
return $view;
}
/**
* Determines the status code to use for the response.
*
* @param \Exception $exception
*
* @return int
*/
protected function getStatusCode(\Exception $exception)
{
// If matched
if ($statusCode = $this->exceptionCodes->resolveException($exception)) {
return $statusCode;
}
// Otherwise, default
if ($exception instanceof HttpExceptionInterface) {
return $exception->getStatusCode();
}
return 500;
}
/**
* Determines the parameters to pass to the view layer.
*
* Overwrite it in a custom ExceptionController class to add additionally parameters
* that should be passed to the view layer.
*
* @param string $currentContent
* @param int $code
* @param \Exception $exception
* @param DebugLoggerInterface $logger
*
* @return array
*/
private function getTemplateData($currentContent, $code, \Exception $exception, DebugLoggerInterface $logger = null)
{
return [
'exception' => FlattenException::create($exception),
'status' => 'error',
'status_code' => $code,
'status_text' => array_key_exists($code, Response::$statusTexts) ? Response::$statusTexts[$code] : 'error',
'currentContent' => $currentContent,
'logger' => $logger,
];
}
/**
* Gets and cleans any content that was already outputted.
*
* This code comes from Symfony and should be synchronized on a regular basis
* see src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php
*
* @return string
*/
private function getAndCleanOutputBuffering($startObLevel)
{
if (ob_get_level() <= $startObLevel) {
return '';
}
Response::closeOutputBuffers($startObLevel + 1, true);
return ob_get_clean();
}
}
Controller/FOSRestController.php 0000666 00000001660 13052362131 0012734 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller;
use FOS\RestBundle\View\ViewHandlerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
/**
* Controllers using the View functionality of FOSRestBundle.
*
* @author Lukas Kahwe Smith
*/
abstract class FOSRestController extends Controller
{
use ControllerTrait;
/**
* Get the ViewHandler.
*
* @return ViewHandlerInterface
*/
protected function getViewHandler()
{
if (!$this->viewhandler instanceof ViewHandlerInterface) {
$this->viewhandler = $this->container->get('fos_rest.view_handler');
}
return $this->viewhandler;
}
}
Controller/TemplatingExceptionController.php 0000666 00000002433 13052362131 0015431 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller;
use FOS\RestBundle\Util\ExceptionValueMap;
use FOS\RestBundle\View\ViewHandlerInterface;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Templating\TemplateReferenceInterface;
abstract class TemplatingExceptionController extends ExceptionController
{
protected $templating;
public function __construct(
ViewHandlerInterface $viewHandler,
ExceptionValueMap $exceptionCodes,
$showException,
EngineInterface $templating
) {
parent::__construct($viewHandler, $exceptionCodes, $showException);
$this->templating = $templating;
}
/**
* Finds the template for the given format and status code.
*
* @param Request $request
* @param int $statusCode
* @param bool $showException
*
* @return TemplateReferenceInterface
*/
abstract protected function findTemplate(Request $request, $statusCode, $showException);
}
Controller/TwigExceptionController.php 0000666 00000004512 13052362131 0014237 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference;
use Symfony\Component\HttpFoundation\Request;
/**
* Custom ExceptionController that uses the view layer and supports HTTP response status code mapping.
* It additionally is able to prepare the template parameters for the core EngineInterface.
*/
class TwigExceptionController extends TemplatingExceptionController
{
/**
* {@inheritdoc}
*/
protected function createView(\Exception $exception, $code, array $templateData, Request $request, $showException)
{
$view = parent::createView($exception, $code, $templateData, $request, $showException);
$view->setTemplate($this->findTemplate($request, $code, $showException));
return $view;
}
/**
* {@inheritdoc}
*
* This code is inspired by TwigBundle and should be synchronized on a regular basis
* see src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php
*/
protected function findTemplate(Request $request, $statusCode, $showException)
{
$format = $request->getRequestFormat();
$name = $showException ? 'exception' : 'error';
if ($showException && 'html' === $format) {
$name = 'exception_full';
}
// when not in debug, try to find a template for the specific HTTP status code and format
if (!$showException) {
$template = new TemplateReference('TwigBundle', 'Exception', $name.$statusCode, $format, 'twig');
if ($this->templating->exists($template)) {
return $template;
}
}
// try to find a template for the given format
$template = new TemplateReference('TwigBundle', 'Exception', $name, $format, 'twig');
if ($this->templating->exists($template)) {
return $template;
}
// default to a generic HTML exception
$request->setRequestFormat('html');
return new TemplateReference('TwigBundle', 'Exception', $showException ? 'exception_full' : $name, 'html', 'twig');
}
}
Decoder/ContainerDecoderProvider.php 0000666 00000002720 13052362131 0013546 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Decoder;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides encoders through the Symfony DIC.
*
* @author Igor Wiedler
*/
class ContainerDecoderProvider implements DecoderProviderInterface
{
private $container;
private $decoders;
/**
* Constructor.
*
* @param ContainerInterface $container The container from which the actual decoders are retrieved
* @param array $decoders List of key (format) value (service ids) of decoders
*/
public function __construct(ContainerInterface $container, array $decoders)
{
$this->container = $container;
$this->decoders = $decoders;
}
/**
* {@inheritdoc}
*/
public function supports($format)
{
return isset($this->decoders[$format]);
}
/**
* {@inheritdoc}
*/
public function getDecoder($format)
{
if (!$this->supports($format)) {
throw new \InvalidArgumentException(
sprintf("Format '%s' is not supported by ContainerDecoderProvider.", $format)
);
}
return $this->container->get($this->decoders[$format]);
}
}
Decoder/DecoderInterface.php 0000666 00000001156 13052362131 0012013 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Decoder;
/**
* Defines the interface of decoders.
*
* @author Jordi Boggiano
*/
interface DecoderInterface
{
/**
* Decodes a string into PHP data.
*
* @param string $data
*
* @return mixed False in case the content could not be decoded
*/
public function decode($data);
}
Decoder/DecoderProviderInterface.php 0000666 00000001441 13052362131 0013523 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Decoder;
/**
* Defines the interface of decoder providers.
*
* @author Igor Wiedler
*/
interface DecoderProviderInterface
{
/**
* Checks if a certain format is supported.
*
* @param string $format
*
* @return bool
*/
public function supports($format);
/**
* Provides decoders, possibly lazily.
*
* @param string $format
*
* @return \FOS\RestBundle\Decoder\DecoderInterface
*/
public function getDecoder($format);
}
Decoder/JsonDecoder.php 0000666 00000001047 13052362131 0011023 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Decoder;
/**
* Decodes JSON data.
*
* @author Jordi Boggiano
*/
class JsonDecoder implements DecoderInterface
{
/**
* {@inheritdoc}
*/
public function decode($data)
{
return @json_decode($data, true);
}
}
Decoder/JsonToFormDecoder.php 0000666 00000003257 13052362131 0012157 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Decoder;
/**
* Decodes JSON data and make it compliant with application/x-www-form-encoded style.
*
* @author Kévin Dunglas
*/
class JsonToFormDecoder implements DecoderInterface
{
/**
* Makes data decoded from JSON application/x-www-form-encoded compliant.
*
* @param array $data
*/
private function xWwwFormEncodedLike(&$data)
{
foreach ($data as $key => &$value) {
if (is_array($value)) {
// Encode recursively
$this->xWwwFormEncodedLike($value);
} elseif (false === $value) {
// Checkbox-like behavior removes false data but PATCH HTTP method with just checkboxes does not work
// To fix this issue we prefer transform false data to null
// See https://github.com/FriendsOfSymfony/FOSRestBundle/pull/883
$value = null;
} elseif (!is_string($value)) {
// Convert everything to string
// true values will be converted to '1', this is the default checkbox behavior
$value = strval($value);
}
}
}
/**
* {@inheritdoc}
*/
public function decode($data)
{
$decodedData = @json_decode($data, true);
if ($decodedData) {
$this->xWwwFormEncodedLike($decodedData);
}
return $decodedData;
}
}
Decoder/XmlDecoder.php 0000666 00000001737 13052362131 0010660 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Decoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
/**
* Decodes XML data.
*
* @author Jordi Boggiano
* @author John Wards
* @author Fabian Vogler
*/
class XmlDecoder implements DecoderInterface
{
private $encoder;
public function __construct()
{
$this->encoder = new XmlEncoder();
}
/**
* {@inheritdoc}
*/
public function decode($data)
{
try {
return $this->encoder->decode($data, 'xml');
} catch (UnexpectedValueException $e) {
return;
}
}
}
DependencyInjection/Compiler/ConfigurationCheckPass.php 0000666 00000002736 13052362131 0017354 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Checks if the SensioFrameworkExtraBundle views annotations are disabled when using the View Response listener.
*
* @author Eriksen Costa
*
* @internal
*/
final class ConfigurationCheckPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if ($container->has('fos_rest.converter.request_body') && !$container->has('sensio_framework_extra.converter.listener')) {
throw new \RuntimeException('You need to enable the parameter converter listeners in SensioFrameworkExtraBundle when using the FOSRestBundle RequestBodyParamConverter');
}
if ($container->has('fos_rest.view_response_listener') && isset($container->getParameter('kernel.bundles')['SensioFrameworkExtraBundle'])) {
if (!$container->has('sensio_framework_extra.view.listener')) {
throw new \RuntimeException('You must enable the SensioFrameworkExtraBundle view annotations to use the ViewResponseListener.');
}
}
}
}
DependencyInjection/Compiler/FormatListenerRulesPass.php 0000666 00000005703 13052362131 0017555 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Eduardo Gulias Davis
*
* @internal
*/
final class FormatListenerRulesPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('fos_rest.format_listener')) {
return;
}
if ($container->hasParameter('web_profiler.debug_toolbar.mode')) {
$path = '_profiler';
if (2 === $container->getParameter('web_profiler.debug_toolbar.mode')) {
$path .= '|_wdt';
}
$profilerRule = [
'host' => null,
'methods' => null,
'path' => "^/$path/",
'priorities' => ['html', 'json'],
'fallback_format' => 'html',
'attributes' => array(),
'prefer_extension' => true,
];
$this->addRule($profilerRule, $container);
}
$rules = $container->getParameter('fos_rest.format_listener.rules');
foreach ($rules as $rule) {
$this->addRule($rule, $container);
}
$container->setParameter('fos_rest.format_listener.rules', null);
}
private function addRule(array $rule, ContainerBuilder $container)
{
$matcher = $this->createRequestMatcher(
$container,
$rule['path'],
$rule['host'],
$rule['methods'],
$rule['attributes']
);
unset($rule['path'], $rule['host']);
if (is_bool($rule['prefer_extension']) && $rule['prefer_extension']) {
$rule['prefer_extension'] = '2.0';
}
$container->getDefinition('fos_rest.format_negotiator')
->addMethodCall('add', [$matcher, $rule]);
}
private function createRequestMatcher(ContainerBuilder $container, $path = null, $host = null, $methods = null, array $attributes = array())
{
$arguments = [$path, $host, $methods, null, $attributes];
$serialized = serialize($arguments);
$id = 'fos_rest.request_matcher.'.md5($serialized).sha1($serialized);
if (!$container->hasDefinition($id)) {
// only add arguments that are necessary
$container
->setDefinition($id, new DefinitionDecorator('fos_rest.format_request_matcher'))
->setArguments($arguments);
}
return new Reference($id);
}
}
DependencyInjection/Compiler/JMSHandlersPass.php 0000666 00000003725 13052362131 0015720 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Checks if the JMS serializer is available to be able to use handlers.
*
* @author Christian Flothmann
*
* @internal
*/
final class JMSHandlersPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if ($container->has('jms_serializer.handler_registry')) {
$this->registerHandlerRegistry($container);
return;
}
$container->removeDefinition('fos_rest.serializer.handler_registry');
$container->removeDefinition('fos_rest.serializer.exception_normalizer.jms');
$container->getParameterBag()->remove('jms_serializer.form_error_handler.class');
}
/**
* Inlined because {@link JMS\SerializerBundle\DependencyInjection\Compiler\CustomHandlersPass}
* must be executed before replacing jms_serializer.handler_registry.
*/
public function registerHandlerRegistry(ContainerBuilder $container)
{
$handlerRegistry = new Definition('FOS\RestBundle\Serializer\JMSHandlerRegistry', array(new Reference('fos_rest.serializer.jms_handler_registry')));
$oldRegistry = $container->getDefinition('jms_serializer.handler_registry');
$oldRegistry->setPublic(false);
$container->setDefinition('jms_serializer.handler_registry', $handlerRegistry);
$container->setDefinition('fos_rest.serializer.jms_handler_registry', $oldRegistry);
}
}
DependencyInjection/Compiler/SerializerConfigurationPass.php 0000666 00000006147 13052362131 0020450 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Checks if a serializer is either set or can be auto-configured.
*
* @author Christian Flothmann
* @author Florian Voutzinos
*
* @internal
*/
final class SerializerConfigurationPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if ($container->has('fos_rest.serializer')) {
$class = $container->getParameterBag()->resolveValue(
$container->findDefinition('fos_rest.serializer')->getClass()
);
if (!is_subclass_of($class, 'FOS\RestBundle\Serializer\Serializer')) {
throw new \InvalidArgumentException(sprintf('"fos_rest.serializer" must implement FOS\RestBundle\Serializer\Serializer (instance of "%s" given).', $class));
}
return;
}
if (!$container->has('serializer') && !$container->has('jms_serializer.serializer')) {
throw new \InvalidArgumentException('Neither a service called "jms_serializer.serializer" nor "serializer" is available and no serializer is explicitly configured. You must either enable the JMSSerializerBundle, enable the FrameworkBundle serializer or configure a custom serializer.');
}
if ($container->has('serializer')) {
$class = $container->getParameterBag()->resolveValue(
$container->findDefinition('serializer')->getClass()
);
if (is_subclass_of($class, 'Symfony\Component\Serializer\SerializerInterface')) {
$container->setAlias('fos_rest.serializer', 'fos_rest.serializer.symfony');
$container->removeDefinition('fos_rest.serializer.exception_wrapper_serialize_handler');
} elseif (is_subclass_of($class, 'JMS\Serializer\SerializerInterface')) {
$container->setAlias('fos_rest.serializer', 'fos_rest.serializer.jms');
} elseif (is_subclass_of($class, 'FOS\RestBundle\Serializer\Serializer')) {
$container->setAlias('fos_rest.serializer', 'serializer');
} else {
throw new \InvalidArgumentException(sprintf('"fos_rest.serializer" must implement FOS\RestBundle\Serializer\Serializer (instance of "%s" given).', $class));
}
return;
} else {
$container->removeDefinition('fos_rest.serializer.exception_wrapper_normalizer');
}
if ($container->has('jms_serializer.serializer')) {
$container->setAlias('fos_rest.serializer', 'fos_rest.serializer.jms');
} else {
$container->removeDefinition('fos_rest.serializer.exception_wrapper_serialize_handler');
}
}
}
DependencyInjection/Compiler/TwigExceptionPass.php 0000666 00000001507 13052362131 0016373 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Remove the 'fos_rest.exception.twig_controller' service if twig is enabled.
*
* @internal
*/
final class TwigExceptionPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->has('templating.engine.twig')) {
$container->removeDefinition('fos_rest.exception.twig_controller');
}
}
}
DependencyInjection/Configuration.php 0000666 00000044441 13052362131 0014014 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\HttpFoundation\Response;
/**
* This class contains the configuration information for the bundle.
*
* This information is solely responsible for how the different configuration
* sections are normalized, and merged.
*
* @author Lukas Kahwe Smith
*
* @internal
*/
final class Configuration implements ConfigurationInterface
{
/**
* Default debug mode value.
*
* @var bool
*/
private $debug;
/**
* @param bool $debug
*/
public function __construct($debug)
{
$this->debug = (bool) $debug;
}
/**
* Generates the configuration tree.
*
* @return TreeBuilder
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('fos_rest', 'array');
$rootNode
->children()
->scalarNode('disable_csrf_role')->defaultNull()->end()
->arrayNode('access_denied_listener')
->canBeEnabled()
->beforeNormalization()
->ifArray()->then(function ($v) { if (!empty($v) && empty($v['formats'])) {
unset($v['enabled']);
$v = ['enabled' => true, 'formats' => $v];
}
return $v; })
->end()
->fixXmlConfig('format', 'formats')
->children()
->scalarNode('service')->defaultNull()->end()
->arrayNode('formats')
->useAttributeAsKey('name')
->prototype('boolean')->end()
->end()
->end()
->end()
->scalarNode('unauthorized_challenge')->defaultNull()->end()
->arrayNode('param_fetcher_listener')
->beforeNormalization()
->ifString()
->then(function ($v) { return ['enabled' => in_array($v, ['force', 'true']), 'force' => 'force' === $v]; })
->end()
->canBeEnabled()
->children()
->booleanNode('enabled')->defaultFalse()->end()
->booleanNode('force')->defaultFalse()->end()
->scalarNode('service')->defaultNull()->end()
->end()
->end()
->scalarNode('cache_dir')->cannotBeEmpty()->defaultValue('%kernel.cache_dir%/fos_rest')->end()
->arrayNode('allowed_methods_listener')
->canBeEnabled()
->children()
->scalarNode('service')->defaultNull()->end()
->end()
->end()
->arrayNode('routing_loader')
->addDefaultsIfNotSet()
->children()
->scalarNode('default_format')->defaultNull()->end()
->scalarNode('include_format')->defaultTrue()->end()
->end()
->end()
->arrayNode('body_converter')
->addDefaultsIfNotSet()
->children()
->scalarNode('enabled')->defaultFalse()->end()
->scalarNode('validate')->defaultFalse()->end()
->scalarNode('validation_errors_argument')->defaultValue('validationErrors')->end()
->end()
->end()
->arrayNode('service')
->addDefaultsIfNotSet()
->children()
->scalarNode('router')->defaultValue('router')->end()
->scalarNode('templating')->defaultValue('templating')->end()
->scalarNode('serializer')->defaultNull()->end()
->scalarNode('view_handler')->defaultValue('fos_rest.view_handler.default')->end()
->scalarNode('inflector')->defaultValue('fos_rest.inflector.doctrine')->end()
->scalarNode('validator')->defaultValue('validator')->end()
->end()
->end()
->arrayNode('serializer')
->addDefaultsIfNotSet()
->children()
->scalarNode('version')->defaultNull()->end()
->arrayNode('groups')
->prototype('scalar')->end()
->end()
->booleanNode('serialize_null')->defaultFalse()->end()
->end()
->end()
->arrayNode('zone')
->cannotBeOverwritten()
->prototype('array')
->fixXmlConfig('ip')
->children()
->scalarNode('path')
->defaultNull()
->info('use the urldecoded format')
->example('^/path to resource/')
->end()
->scalarNode('host')->defaultNull()->end()
->arrayNode('methods')
->beforeNormalization()->ifString()->then(function ($v) { return preg_split('/\s*,\s*/', $v); })->end()
->prototype('scalar')->end()
->end()
->arrayNode('ips')
->beforeNormalization()->ifString()->then(function ($v) { return array($v); })->end()
->prototype('scalar')->end()
->end()
->end()
->end()
->end()
->end();
$this->addViewSection($rootNode);
$this->addExceptionSection($rootNode);
$this->addBodyListenerSection($rootNode);
$this->addFormatListenerSection($rootNode);
$this->addVersioningSection($rootNode);
return $treeBuilder;
}
private function addViewSection(ArrayNodeDefinition $rootNode)
{
$rootNode
->children()
->arrayNode('view')
->fixXmlConfig('format', 'formats')
->fixXmlConfig('mime_type', 'mime_types')
->fixXmlConfig('templating_format', 'templating_formats')
->fixXmlConfig('force_redirect', 'force_redirects')
->addDefaultsIfNotSet()
->children()
->scalarNode('default_engine')->defaultValue('twig')->end()
->arrayNode('force_redirects')
->useAttributeAsKey('name')
->defaultValue(['html' => true])
->prototype('boolean')->end()
->end()
->arrayNode('mime_types')
->canBeEnabled()
->beforeNormalization()
->ifArray()->then(function ($v) {
if (!empty($v) && empty($v['formats'])) {
unset($v['enabled']);
$v = ['enabled' => true, 'formats' => $v];
}
return $v;
})
->end()
->fixXmlConfig('format', 'formats')
->children()
->scalarNode('service')->defaultNull()->end()
->arrayNode('formats')
->useAttributeAsKey('name')
->prototype('variable')->end()
->end()
->end()
->end()
->arrayNode('formats')
->useAttributeAsKey('name')
->defaultValue(['json' => true, 'xml' => true])
->prototype('boolean')->end()
->end()
->arrayNode('templating_formats')
->useAttributeAsKey('name')
->defaultValue(['html' => true])
->prototype('boolean')->end()
->end()
->arrayNode('view_response_listener')
->beforeNormalization()
->ifString()
->then(function ($v) { return ['enabled' => in_array($v, ['force', 'true']), 'force' => 'force' === $v]; })
->end()
->canBeEnabled()
->children()
->booleanNode('enabled')->defaultFalse()->end()
->booleanNode('force')->defaultFalse()->end()
->scalarNode('service')->defaultNull()->end()
->end()
->end()
->scalarNode('failed_validation')->defaultValue(Response::HTTP_BAD_REQUEST)->end()
->scalarNode('empty_content')->defaultValue(Response::HTTP_NO_CONTENT)->end()
->booleanNode('serialize_null')->defaultFalse()->end()
->arrayNode('jsonp_handler')
->canBeUnset()
->children()
->scalarNode('callback_param')->defaultValue('callback')->end()
->scalarNode('mime_type')->defaultValue('application/javascript+jsonp')->end()
->end()
->end()
->end()
->end()
->end();
}
private function addBodyListenerSection(ArrayNodeDefinition $rootNode)
{
$rootNode
->children()
->arrayNode('body_listener')
->fixXmlConfig('decoder', 'decoders')
->addDefaultsIfNotSet()
->canBeUnset()
->canBeDisabled()
->children()
->scalarNode('service')->defaultNull()->end()
->scalarNode('default_format')->defaultNull()->end()
->booleanNode('throw_exception_on_unsupported_content_type')
->defaultFalse()
->end()
->arrayNode('decoders')
->useAttributeAsKey('name')
->defaultValue(['json' => 'fos_rest.decoder.json', 'xml' => 'fos_rest.decoder.xml'])
->prototype('scalar')->end()
->end()
->arrayNode('array_normalizer')
->addDefaultsIfNotSet()
->beforeNormalization()
->ifString()->then(function ($v) { return ['service' => $v]; })
->end()
->children()
->scalarNode('service')->defaultNull()->end()
->booleanNode('forms')->defaultFalse()->end()
->end()
->end()
->end()
->end()
->end();
}
private function addFormatListenerSection(ArrayNodeDefinition $rootNode)
{
$rootNode
->children()
->arrayNode('format_listener')
->fixXmlConfig('rule', 'rules')
->addDefaultsIfNotSet()
->canBeUnset()
->beforeNormalization()
->ifTrue(function ($v) {
// check if we got an assoc array in rules
return isset($v['rules'])
&& is_array($v['rules'])
&& array_keys($v['rules']) !== range(0, count($v['rules']) - 1);
})
->then(function ($v) {
$v['rules'] = [$v['rules']];
return $v;
})
->end()
->canBeEnabled()
->children()
->scalarNode('service')->defaultNull()->end()
->arrayNode('rules')
->cannotBeOverwritten()
->prototype('array')
->fixXmlConfig('priority', 'priorities')
->fixXmlConfig('attribute', 'attributes')
->children()
->scalarNode('path')->defaultNull()->info('URL path info')->end()
->scalarNode('host')->defaultNull()->info('URL host name')->end()
->variableNode('methods')->defaultNull()->info('Method for URL')->end()
->arrayNode('attributes')
->useAttributeAsKey('name')
->prototype('variable')->end()
->end()
->booleanNode('stop')->defaultFalse()->end()
->booleanNode('prefer_extension')->defaultTrue()->end()
->scalarNode('fallback_format')->defaultValue('html')->end()
->arrayNode('priorities')
->beforeNormalization()->ifString()->then(function ($v) { return preg_split('/\s*,\s*/', $v); })->end()
->prototype('scalar')->end()
->end()
->end()
->end()
->end()
->end()
->end()
->end();
}
private function addVersioningSection(ArrayNodeDefinition $rootNode)
{
$rootNode
->children()
->arrayNode('versioning')
->canBeEnabled()
->children()
->scalarNode('default_version')->defaultNull()->end()
->arrayNode('resolvers')
->addDefaultsIfNotSet()
->children()
->arrayNode('query')
->canBeDisabled()
->children()
->scalarNode('parameter_name')->defaultValue('version')->end()
->end()
->end()
->arrayNode('custom_header')
->canBeDisabled()
->children()
->scalarNode('header_name')->defaultValue('X-Accept-Version')->end()
->end()
->end()
->arrayNode('media_type')
->canBeDisabled()
->children()
->scalarNode('regex')->defaultValue('/(v|version)=(?P[0-9\.]+)/')->end()
->end()
->end()
->end()
->end()
->arrayNode('guessing_order')
->defaultValue(['query', 'custom_header', 'media_type'])
->validate()
->ifTrue(function ($v) {
foreach ($v as $resolver) {
if (!in_array($resolver, ['query', 'custom_header', 'media_type'])) {
return true;
}
}
})
->thenInvalid('Versioning guessing order can only contain "query", "custom_header", "media_type".')
->end()
->prototype('scalar')->end()
->end()
->end()
->end()
->end();
}
private function addExceptionSection(ArrayNodeDefinition $rootNode)
{
$rootNode
->children()
->arrayNode('exception')
->fixXmlConfig('code', 'codes')
->fixXmlConfig('message', 'messages')
->addDefaultsIfNotSet()
->canBeEnabled()
->children()
->scalarNode('exception_controller')->defaultNull()->end()
->arrayNode('codes')
->useAttributeAsKey('name')
->validate()
->ifTrue(function ($v) { return 0 !== count(array_filter($v, function ($i) { return !defined('Symfony\Component\HttpFoundation\Response::'.$i) && !is_int($i); })); })
->thenInvalid('Invalid HTTP code in fos_rest.exception.codes, see Symfony\Component\HttpFoundation\Response for all valid codes.')
->end()
->prototype('scalar')->end()
->end()
->arrayNode('messages')
->useAttributeAsKey('name')
->prototype('boolean')->end()
->end()
->booleanNode('debug')
->defaultValue($this->debug)
->end()
->end()
->end()
->end();
}
}
DependencyInjection/FOSRestExtension.php 0000666 00000046040 13052362131 0014364 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\HttpFoundation\Response;
class FOSRestExtension extends Extension
{
/**
* {@inheritdoc}
*/
public function getConfiguration(array $config, ContainerBuilder $container)
{
return new Configuration($container->getParameter('kernel.debug'));
}
/**
* Loads the services based on your application configuration.
*
* @param array $configs
* @param ContainerBuilder $container
*
* @throws \InvalidArgumentException
* @throws \LogicException
*/
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration($container->getParameter('kernel.debug'));
$config = $this->processConfiguration($configuration, $configs);
$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('view.xml');
$loader->load('routing.xml');
$loader->load('request.xml');
$loader->load('serializer.xml');
$container->getDefinition('fos_rest.routing.loader.controller')->replaceArgument(4, $config['routing_loader']['default_format']);
$container->getDefinition('fos_rest.routing.loader.yaml_collection')->replaceArgument(4, $config['routing_loader']['default_format']);
$container->getDefinition('fos_rest.routing.loader.xml_collection')->replaceArgument(4, $config['routing_loader']['default_format']);
$container->getDefinition('fos_rest.routing.loader.yaml_collection')->replaceArgument(2, $config['routing_loader']['include_format']);
$container->getDefinition('fos_rest.routing.loader.xml_collection')->replaceArgument(2, $config['routing_loader']['include_format']);
$container->getDefinition('fos_rest.routing.loader.reader.action')->replaceArgument(3, $config['routing_loader']['include_format']);
// The validator service alias is only set if validation is enabled for the request body converter
$validator = $config['service']['validator'];
unset($config['service']['validator']);
foreach ($config['service'] as $key => $service) {
if (null !== $service) {
$container->setAlias('fos_rest.'.$key, $service);
}
}
$this->loadForm($config, $loader, $container);
$this->loadException($config, $loader, $container);
$this->loadBodyConverter($config, $validator, $loader, $container);
$this->loadView($config, $loader, $container);
$this->loadBodyListener($config, $loader, $container);
$this->loadFormatListener($config, $loader, $container);
$this->loadVersioning($config, $loader, $container);
$this->loadParamFetcherListener($config, $loader, $container);
$this->loadAllowedMethodsListener($config, $loader, $container);
$this->loadAccessDeniedListener($config, $loader, $container);
$this->loadZoneMatcherListener($config, $loader, $container);
// Needs RequestBodyParamConverter and View Handler loaded.
$this->loadSerializer($config, $container);
}
private function loadForm(array $config, XmlFileLoader $loader, ContainerBuilder $container)
{
if (!empty($config['disable_csrf_role'])) {
$loader->load('forms.xml');
$container->getDefinition('fos_rest.form.extension.csrf_disable')->replaceArgument(1, $config['disable_csrf_role']);
}
}
private function loadAccessDeniedListener(array $config, XmlFileLoader $loader, ContainerBuilder $container)
{
if ($config['access_denied_listener']['enabled'] && !empty($config['access_denied_listener']['formats'])) {
$loader->load('access_denied_listener.xml');
$service = $container->getDefinition('fos_rest.access_denied_listener');
if (!empty($config['access_denied_listener']['service'])) {
$service->clearTag('kernel.event_subscriber');
}
$service->replaceArgument(0, $config['access_denied_listener']['formats']);
$service->replaceArgument(1, $config['unauthorized_challenge']);
}
}
private function loadAllowedMethodsListener(array $config, XmlFileLoader $loader, ContainerBuilder $container)
{
if ($config['allowed_methods_listener']['enabled']) {
if (!empty($config['allowed_methods_listener']['service'])) {
$service = $container->getDefinition('fos_rest.allowed_methods_listener');
$service->clearTag('kernel.event_listener');
}
$loader->load('allowed_methods_listener.xml');
$container->getDefinition('fos_rest.allowed_methods_loader')->replaceArgument(1, $config['cache_dir']);
}
}
private function loadBodyListener(array $config, XmlFileLoader $loader, ContainerBuilder $container)
{
if ($config['body_listener']['enabled']) {
$loader->load('body_listener.xml');
$service = $container->getDefinition('fos_rest.body_listener');
if (!empty($config['body_listener']['service'])) {
$service->clearTag('kernel.event_listener');
}
$service->replaceArgument(1, $config['body_listener']['throw_exception_on_unsupported_content_type']);
$service->addMethodCall('setDefaultFormat', array($config['body_listener']['default_format']));
$container->getDefinition('fos_rest.decoder_provider')->replaceArgument(1, $config['body_listener']['decoders']);
$arrayNormalizer = $config['body_listener']['array_normalizer'];
if (null !== $arrayNormalizer['service']) {
$bodyListener = $container->getDefinition('fos_rest.body_listener');
$bodyListener->addArgument(new Reference($arrayNormalizer['service']));
$bodyListener->addArgument($arrayNormalizer['forms']);
}
}
}
private function loadFormatListener(array $config, XmlFileLoader $loader, ContainerBuilder $container)
{
if ($config['format_listener']['enabled'] && !empty($config['format_listener']['rules'])) {
$loader->load('format_listener.xml');
if (!empty($config['format_listener']['service'])) {
$service = $container->getDefinition('fos_rest.format_listener');
$service->clearTag('kernel.event_listener');
}
$container->setParameter(
'fos_rest.format_listener.rules',
$config['format_listener']['rules']
);
}
}
private function loadVersioning(array $config, XmlFileLoader $loader, ContainerBuilder $container)
{
if (!empty($config['versioning']['enabled'])) {
$loader->load('versioning.xml');
$versionListener = $container->getDefinition('fos_rest.versioning.listener');
$versionListener->replaceArgument(2, $config['versioning']['default_version']);
$resolvers = [];
if ($config['versioning']['resolvers']['query']['enabled']) {
$resolvers['query'] = $container->getDefinition('fos_rest.versioning.query_parameter_resolver');
$resolvers['query']->replaceArgument(0, $config['versioning']['resolvers']['query']['parameter_name']);
}
if ($config['versioning']['resolvers']['custom_header']['enabled']) {
$resolvers['custom_header'] = $container->getDefinition('fos_rest.versioning.header_resolver');
$resolvers['custom_header']->replaceArgument(0, $config['versioning']['resolvers']['custom_header']['header_name']);
}
if ($config['versioning']['resolvers']['media_type']['enabled']) {
$resolvers['media_type'] = $container->getDefinition('fos_rest.versioning.media_type_resolver');
$resolvers['media_type']->replaceArgument(0, $config['versioning']['resolvers']['media_type']['regex']);
}
$chainResolver = $container->getDefinition('fos_rest.versioning.chain_resolver');
foreach ($config['versioning']['guessing_order'] as $resolver) {
if (isset($resolvers[$resolver])) {
$chainResolver->addMethodCall('addResolver', [$resolvers[$resolver]]);
}
}
}
}
private function loadParamFetcherListener(array $config, XmlFileLoader $loader, ContainerBuilder $container)
{
if ($config['param_fetcher_listener']['enabled']) {
$loader->load('param_fetcher_listener.xml');
if (!empty($config['param_fetcher_listener']['service'])) {
$service = $container->getDefinition('fos_rest.param_fetcher_listener');
$service->clearTag('kernel.event_listener');
}
if ($config['param_fetcher_listener']['force']) {
$container->getDefinition('fos_rest.param_fetcher_listener')->replaceArgument(1, true);
}
}
}
private function loadBodyConverter(array $config, $validator, XmlFileLoader $loader, ContainerBuilder $container)
{
if (empty($config['body_converter'])) {
return;
}
if (!empty($config['body_converter']['enabled'])) {
$loader->load('request_body_param_converter.xml');
if (!empty($config['body_converter']['validation_errors_argument'])) {
$container->getDefinition('fos_rest.converter.request_body')->replaceArgument(4, $config['body_converter']['validation_errors_argument']);
}
}
if (!empty($config['body_converter']['validate'])) {
$container->setAlias('fos_rest.validator', $validator);
}
}
private function loadView(array $config, XmlFileLoader $loader, ContainerBuilder $container)
{
if (!empty($config['view']['jsonp_handler'])) {
$handler = new DefinitionDecorator($config['service']['view_handler']);
$handler->setPublic(true);
$jsonpHandler = new Reference('fos_rest.view_handler.jsonp');
$handler->addMethodCall('registerHandler', ['jsonp', [$jsonpHandler, 'createResponse']]);
$container->setDefinition('fos_rest.view_handler', $handler);
$container->getDefinition('fos_rest.view_handler.jsonp')->replaceArgument(0, $config['view']['jsonp_handler']['callback_param']);
if (empty($config['view']['mime_types']['jsonp'])) {
$config['view']['mime_types']['jsonp'] = $config['view']['jsonp_handler']['mime_type'];
}
}
if ($config['view']['mime_types']['enabled']) {
$loader->load('mime_type_listener.xml');
if (!empty($config['mime_type_listener']['service'])) {
$service = $container->getDefinition('fos_rest.mime_type_listener');
$service->clearTag('kernel.event_listener');
}
$container->getDefinition('fos_rest.mime_type_listener')->replaceArgument(0, $config['view']['mime_types']['formats']);
}
if ($config['view']['view_response_listener']['enabled']) {
$loader->load('view_response_listener.xml');
$service = $container->getDefinition('fos_rest.view_response_listener');
if (!empty($config['view_response_listener']['service'])) {
$service->clearTag('kernel.event_listener');
}
$service->replaceArgument(1, $config['view']['view_response_listener']['force']);
}
$formats = [];
foreach ($config['view']['formats'] as $format => $enabled) {
if ($enabled) {
$formats[$format] = false;
}
}
foreach ($config['view']['templating_formats'] as $format => $enabled) {
if ($enabled) {
$formats[$format] = true;
}
}
$container->getDefinition('fos_rest.routing.loader.yaml_collection')->replaceArgument(3, $formats);
$container->getDefinition('fos_rest.routing.loader.xml_collection')->replaceArgument(3, $formats);
$container->getDefinition('fos_rest.routing.loader.reader.action')->replaceArgument(4, $formats);
foreach ($config['view']['force_redirects'] as $format => $code) {
if (true === $code) {
$config['view']['force_redirects'][$format] = Response::HTTP_FOUND;
}
}
if (!is_numeric($config['view']['failed_validation'])) {
$config['view']['failed_validation'] = constant('\Symfony\Component\HttpFoundation\Response::'.$config['view']['failed_validation']);
}
$defaultViewHandler = $container->getDefinition('fos_rest.view_handler.default');
$defaultViewHandler->replaceArgument(4, $formats);
$defaultViewHandler->replaceArgument(5, $config['view']['failed_validation']);
if (!is_numeric($config['view']['empty_content'])) {
$config['view']['empty_content'] = constant('\Symfony\Component\HttpFoundation\Response::'.$config['view']['empty_content']);
}
$defaultViewHandler->replaceArgument(6, $config['view']['empty_content']);
$defaultViewHandler->replaceArgument(7, $config['view']['serialize_null']);
$defaultViewHandler->replaceArgument(8, $config['view']['force_redirects']);
$defaultViewHandler->replaceArgument(9, $config['view']['default_engine']);
}
private function loadException(array $config, XmlFileLoader $loader, ContainerBuilder $container)
{
if ($config['exception']['enabled']) {
$loader->load('exception_listener.xml');
if (!empty($config['exception']['service'])) {
$service = $container->getDefinition('fos_rest.exception_listener');
$service->clearTag('kernel.event_subscriber');
}
if ($config['exception']['exception_controller']) {
$container->getDefinition('fos_rest.exception_listener')->replaceArgument(0, $config['exception']['exception_controller']);
} elseif (isset($container->getParameter('kernel.bundles')['TwigBundle'])) {
$container->getDefinition('fos_rest.exception_listener')->replaceArgument(0, 'fos_rest.exception.twig_controller:showAction');
}
$container->getDefinition('fos_rest.exception.codes_map')
->replaceArgument(0, $config['exception']['codes']);
$container->getDefinition('fos_rest.exception.messages_map')
->replaceArgument(0, $config['exception']['messages']);
$container->getDefinition('fos_rest.exception.controller')
->replaceArgument(2, $config['exception']['debug']);
$container->getDefinition('fos_rest.serializer.exception_normalizer.jms')
->replaceArgument(1, $config['exception']['debug']);
$container->getDefinition('fos_rest.serializer.exception_normalizer.symfony')
->replaceArgument(1, $config['exception']['debug']);
}
foreach ($config['exception']['codes'] as $exception => $code) {
if (!is_numeric($code)) {
$config['exception']['codes'][$exception] = constant("\Symfony\Component\HttpFoundation\Response::$code");
}
$this->testExceptionExists($exception);
}
foreach ($config['exception']['messages'] as $exception => $message) {
$this->testExceptionExists($exception);
}
}
private function loadSerializer(array $config, ContainerBuilder $container)
{
$bodyConverter = $container->hasDefinition('fos_rest.converter.request_body') ? $container->getDefinition('fos_rest.converter.request_body') : null;
$viewHandler = $container->getDefinition('fos_rest.view_handler.default');
if (!empty($config['serializer']['version'])) {
if ($bodyConverter) {
$bodyConverter->replaceArgument(2, $config['serializer']['version']);
}
$viewHandler->addMethodCall('setExclusionStrategyVersion', array($config['serializer']['version']));
}
if (!empty($config['serializer']['groups'])) {
if ($bodyConverter) {
$bodyConverter->replaceArgument(1, $config['serializer']['groups']);
}
$viewHandler->addMethodCall('setExclusionStrategyGroups', array($config['serializer']['groups']));
}
$viewHandler->addMethodCall('setSerializeNullStrategy', array($config['serializer']['serialize_null']));
}
private function loadZoneMatcherListener(array $config, XmlFileLoader $loader, ContainerBuilder $container)
{
if (!empty($config['zone'])) {
$loader->load('zone_matcher_listener.xml');
$zoneMatcherListener = $container->getDefinition('fos_rest.zone_matcher_listener');
foreach ($config['zone'] as $zone) {
$matcher = $this->createZoneRequestMatcher($container,
$zone['path'],
$zone['host'],
$zone['methods'],
$zone['ips']
);
$zoneMatcherListener->addMethodCall('addRequestMatcher', array($matcher));
}
}
}
private function createZoneRequestMatcher(ContainerBuilder $container, $path = null, $host = null, $methods = array(), $ip = null)
{
if ($methods) {
$methods = array_map('strtoupper', (array) $methods);
}
$serialized = serialize(array($path, $host, $methods, $ip));
$id = 'fos_rest.zone_request_matcher.'.md5($serialized).sha1($serialized);
// only add arguments that are necessary
$arguments = array($path, $host, $methods, $ip);
while (count($arguments) > 0 && !end($arguments)) {
array_pop($arguments);
}
$container
->setDefinition($id, new DefinitionDecorator('fos_rest.zone_request_matcher'))
->setArguments($arguments)
;
return new Reference($id);
}
/**
* Checks if an exception is loadable.
*
* @param string $exception Class to test
*
* @throws \InvalidArgumentException if the class was not found
*/
private function testExceptionExists($exception)
{
if (!is_subclass_of($exception, \Exception::class) && !is_a($exception, \Exception::class, true)) {
throw new \InvalidArgumentException("FOSRestBundle exception mapper: Could not load class '$exception' or the class does not extend from '\Exception'. Most probably this is a configuration problem.");
}
}
}
EventListener/AccessDeniedListener.php 0000666 00000005627 13052362131 0014076 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\EventListener;
use FOS\RestBundle\FOSRestBundle;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* This listener handles ensures that for specific formats AccessDeniedExceptions
* will return a 403 regardless of how the firewall is configured.
*
* @author Lukas Kahwe Smith
*
* @internal
*/
class AccessDeniedListener implements EventSubscriberInterface
{
private $formats;
private $challenge;
/**
* Constructor.
*
* @param array $formats An array with keys corresponding to request formats or content types
* that must be processed by this listener
* @param string $challenge
*/
public function __construct($formats, $challenge)
{
$this->formats = $formats;
$this->challenge = $challenge;
}
public function onKernelException(GetResponseForExceptionEvent $event)
{
static $handling;
if (true === $handling) {
return false;
}
$request = $event->getRequest();
if (!$request->attributes->get(FOSRestBundle::ZONE_ATTRIBUTE, true)) {
return false;
}
if (empty($this->formats[$request->getRequestFormat()]) && empty($this->formats[$request->getContentType()])) {
return false;
}
$handling = true;
$exception = $event->getException();
if ($exception instanceof AccessDeniedException) {
$exception = new AccessDeniedHttpException('You do not have the necessary permissions', $exception);
$event->setException($exception);
} elseif ($exception instanceof AuthenticationException) {
if ($this->challenge) {
$exception = new UnauthorizedHttpException($this->challenge, 'You are not authenticated', $exception);
} else {
$exception = new HttpException(401, 'You are not authenticated', $exception);
}
$event->setException($exception);
}
$handling = false;
}
public static function getSubscribedEvents()
{
return [
KernelEvents::EXCEPTION => ['onKernelException', 5],
];
}
}
EventListener/AllowedMethodsListener.php 0000666 00000002705 13052362131 0014471 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\EventListener;
use FOS\RestBundle\FOSRestBundle;
use FOS\RestBundle\Response\AllowedMethodsLoader\AllowedMethodsLoaderInterface;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
/**
* Listener to append Allow-ed methods for a given route/resource.
*
* @author Boris Guéry
*
* @internal
*/
class AllowedMethodsListener
{
private $loader;
/**
* Constructor.
*
* @param AllowedMethodsLoaderInterface $loader
*/
public function __construct(AllowedMethodsLoaderInterface $loader)
{
$this->loader = $loader;
}
/**
* @param FilterResponseEvent $event
*/
public function onKernelResponse(FilterResponseEvent $event)
{
$request = $event->getRequest();
if (!$request->attributes->get(FOSRestBundle::ZONE_ATTRIBUTE, true)) {
return;
}
$allowedMethods = $this->loader->getAllowedMethods();
if (isset($allowedMethods[$event->getRequest()->get('_route')])) {
$event->getResponse()
->headers
->set('Allow', implode(', ', $allowedMethods[$event->getRequest()->get('_route')]));
}
}
}
EventListener/BodyListener.php 0000666 00000012752 13052362131 0012456 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\EventListener;
use FOS\RestBundle\Decoder\DecoderProviderInterface;
use FOS\RestBundle\FOSRestBundle;
use FOS\RestBundle\Normalizer\ArrayNormalizerInterface;
use FOS\RestBundle\Normalizer\Exception\NormalizationException;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
/**
* This listener handles Request body decoding.
*
* @author Lukas Kahwe Smith
*
* @internal
*/
class BodyListener
{
private $decoderProvider;
private $throwExceptionOnUnsupportedContentType;
private $defaultFormat;
private $arrayNormalizer;
private $normalizeForms;
/**
* Constructor.
*
* @param DecoderProviderInterface $decoderProvider
* @param bool $throwExceptionOnUnsupportedContentType
* @param ArrayNormalizerInterface $arrayNormalizer
* @param bool $normalizeForms
*/
public function __construct(
DecoderProviderInterface $decoderProvider,
$throwExceptionOnUnsupportedContentType = false,
ArrayNormalizerInterface $arrayNormalizer = null,
$normalizeForms = false
) {
$this->decoderProvider = $decoderProvider;
$this->throwExceptionOnUnsupportedContentType = $throwExceptionOnUnsupportedContentType;
$this->arrayNormalizer = $arrayNormalizer;
$this->normalizeForms = $normalizeForms;
}
/**
* Sets the fallback format if there's no Content-Type in the request.
*
* @param string $defaultFormat
*/
public function setDefaultFormat($defaultFormat)
{
$this->defaultFormat = $defaultFormat;
}
/**
* Core request handler.
*
* @param GetResponseEvent $event
*
* @throws BadRequestHttpException
* @throws UnsupportedMediaTypeHttpException
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!$request->attributes->get(FOSRestBundle::ZONE_ATTRIBUTE, true)) {
return;
}
$method = $request->getMethod();
$contentType = $request->headers->get('Content-Type');
$normalizeRequest = $this->normalizeForms && $this->isFormRequest($request);
if ($this->isDecodeable($request)) {
$format = null === $contentType
? $request->getRequestFormat()
: $request->getFormat($contentType);
$format = $format ?: $this->defaultFormat;
$content = $request->getContent();
if (!$this->decoderProvider->supports($format)) {
if ($this->throwExceptionOnUnsupportedContentType
&& $this->isNotAnEmptyDeleteRequestWithNoSetContentType($method, $content, $contentType)
) {
throw new UnsupportedMediaTypeHttpException("Request body format '$format' not supported");
}
return;
}
if (!empty($content)) {
$decoder = $this->decoderProvider->getDecoder($format);
$data = $decoder->decode($content);
if (is_array($data)) {
$request->request = new ParameterBag($data);
$normalizeRequest = true;
} else {
throw new BadRequestHttpException('Invalid '.$format.' message received');
}
}
}
if (null !== $this->arrayNormalizer && $normalizeRequest) {
$data = $request->request->all();
try {
$data = $this->arrayNormalizer->normalize($data);
} catch (NormalizationException $e) {
throw new BadRequestHttpException($e->getMessage());
}
$request->request = new ParameterBag($data);
}
}
/**
* Check if the Request is not a DELETE with no content and no Content-Type.
*
* @param $method
* @param $content
* @param $contentType
*
* @return bool
*/
private function isNotAnEmptyDeleteRequestWithNoSetContentType($method, $content, $contentType)
{
return false === ('DELETE' === $method && empty($content) && empty($contentType));
}
/**
* Check if we should try to decode the body.
*
* @param Request $request
*
* @return bool
*/
protected function isDecodeable(Request $request)
{
if (!in_array($request->getMethod(), ['POST', 'PUT', 'PATCH', 'DELETE'])) {
return false;
}
return !$this->isFormRequest($request);
}
/**
* Check if the content type indicates a form submission.
*
* @param Request $request
*
* @return bool
*/
private function isFormRequest(Request $request)
{
$contentTypeParts = explode(';', $request->headers->get('Content-Type'));
if (isset($contentTypeParts[0])) {
return in_array($contentTypeParts[0], ['multipart/form-data', 'application/x-www-form-urlencoded']);
}
return false;
}
}
EventListener/ExceptionListener.php 0000666 00000003426 13052362131 0013515 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\EventListener;
use FOS\RestBundle\FOSRestBundle;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\EventListener\ExceptionListener as HttpKernelExceptionListener;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Log\DebugLoggerInterface;
/**
* ExceptionListener.
*
* @author Ener-Getick
*
* @internal
*/
class ExceptionListener extends HttpKernelExceptionListener
{
/**
* {@inheritdoc}
*/
public function onKernelException(GetResponseForExceptionEvent $event)
{
$request = $event->getRequest();
if (!$request->attributes->get(FOSRestBundle::ZONE_ATTRIBUTE, true)) {
return;
}
parent::onKernelException($event);
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents()
{
return array(
KernelEvents::EXCEPTION => array('onKernelException', -100),
);
}
/**
* {@inheritdoc}
*/
protected function duplicateRequest(\Exception $exception, Request $request)
{
$attributes = array(
'_controller' => $this->controller,
'exception' => $exception,
'logger' => $this->logger instanceof DebugLoggerInterface ? $this->logger : null,
);
$request = $request->duplicate(null, null, $attributes);
$request->setMethod('GET');
return $request;
}
}
EventListener/FormatListener.php 0000666 00000004533 13052362131 0013007 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\EventListener;
use FOS\RestBundle\FOSRestBundle;
use FOS\RestBundle\Util\StopFormatListenerException;
use FOS\RestBundle\Negotiation\FormatNegotiator;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* This listener handles Accept header format negotiations.
*
* @author Lukas Kahwe Smith
*
* @internal
*/
class FormatListener
{
private $formatNegotiator;
/**
* Initialize FormatListener.
*
* @param FormatNegotiatorInterface $formatNegotiator
*/
public function __construct(FormatNegotiator $formatNegotiator)
{
$this->formatNegotiator = $formatNegotiator;
}
/**
* Determines and sets the Request format.
*
* @param GetResponseEvent $event The event
*
* @throws NotAcceptableHttpException
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!$request->attributes->get(FOSRestBundle::ZONE_ATTRIBUTE, true)) {
return;
}
try {
$format = $request->getRequestFormat(null);
if (null === $format) {
$accept = $this->formatNegotiator->getBest('');
if (null !== $accept && 0.0 < $accept->getQuality()) {
$format = $request->getFormat($accept->getValue());
if (null !== $format) {
$request->attributes->set('media_type', $accept->getValue());
}
}
}
if (null === $format) {
if ($event->getRequestType() === HttpKernelInterface::MASTER_REQUEST) {
throw new NotAcceptableHttpException('No matching accepted Response format could be determined');
}
return;
}
$request->setRequestFormat($format);
} catch (StopFormatListenerException $e) {
// nothing to do
}
}
}
EventListener/MimeTypeListener.php 0000666 00000004034 13052362131 0013304 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\EventListener;
use FOS\RestBundle\FOSRestBundle;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* This listener handles registering custom mime types.
*
* @author Lukas Kahwe Smith
*
* @internal
*/
class MimeTypeListener
{
private $mimeTypes;
/**
* Constructor.
*
* @param array $mimeTypes An array with the format as key and
* the corresponding mime type as value
*/
public function __construct(array $mimeTypes)
{
$this->mimeTypes = $mimeTypes;
}
/**
* Core request handler.
*
* @param GetResponseEvent $event The event
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!$request->attributes->get(FOSRestBundle::ZONE_ATTRIBUTE, true)) {
return;
}
if (HttpKernelInterface::MASTER_REQUEST === $event->getRequestType()) {
foreach ($this->mimeTypes as $format => $mimeTypes) {
if (method_exists(Request::class, 'getMimeTypes')) {
$mimeTypes = array_merge($mimeTypes, Request::getMimeTypes($format));
} elseif (null !== $request->getMimeType($format)) {
$class = new \ReflectionClass(Request::class);
$properties = $class->getStaticProperties();
if (isset($properties['formats'][$format])) {
$mimeTypes = array_merge($mimeTypes, $properties['formats'][$format]);
}
}
$request->setFormat($format, $mimeTypes);
}
}
}
}
EventListener/ParamFetcherListener.php 0000666 00000007172 13052362131 0014122 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\EventListener;
use FOS\RestBundle\FOSRestBundle;
use FOS\RestBundle\Request\ParamFetcherInterface;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
/**
* This listener handles various setup tasks related to the query fetcher.
*
* Setting the controller callable on the query fetcher
* Setting the query fetcher as a request attribute
*
* @author Lukas Kahwe Smith
*
* @internal
*/
class ParamFetcherListener
{
private $paramFetcher;
private $setParamsAsAttributes;
/**
* Constructor.
*
* @param ParamFetcherInterface $paramFetcher
* @param bool $setParamsAsAttributes
*/
public function __construct(ParamFetcherInterface $paramFetcher, $setParamsAsAttributes = false)
{
$this->paramFetcher = $paramFetcher;
$this->setParamsAsAttributes = $setParamsAsAttributes;
}
/**
* Core controller handler.
*
* @param FilterControllerEvent $event
*
* @throws \InvalidArgumentException
*/
public function onKernelController(FilterControllerEvent $event)
{
$request = $event->getRequest();
if (!$request->attributes->get(FOSRestBundle::ZONE_ATTRIBUTE, true)) {
return;
}
$controller = $event->getController();
if (is_callable($controller) && method_exists($controller, '__invoke')) {
$controller = [$controller, '__invoke'];
}
$this->paramFetcher->setController($controller);
$attributeName = $this->getAttributeName($controller);
$request->attributes->set($attributeName, $this->paramFetcher);
if ($this->setParamsAsAttributes) {
$params = $this->paramFetcher->all();
foreach ($params as $name => $param) {
if ($request->attributes->has($name) && null !== $request->attributes->get($name)) {
$msg = sprintf("ParamFetcher parameter conflicts with a path parameter '$name' for route '%s'", $request->attributes->get('_route'));
throw new \InvalidArgumentException($msg);
}
$request->attributes->set($name, $param);
}
}
}
/**
* Determines which attribute the ParamFetcher should be injected as.
*
* @param callable $controller The controller action as an "array" callable
*
* @return string
*/
private function getAttributeName(callable $controller)
{
list($object, $name) = $controller;
$method = new \ReflectionMethod($object, $name);
foreach ($method->getParameters() as $param) {
if ($this->isParamFetcherType($param)) {
return $param->getName();
}
}
// If there is no typehint, inject the ParamFetcher using a default name.
return 'paramFetcher';
}
/**
* Returns true if the given controller parameter is type-hinted as
* an instance of ParamFetcher.
*
* @param \ReflectionParameter $controllerParam A parameter of the controller action
*
* @return bool
*/
private function isParamFetcherType(\ReflectionParameter $controllerParam)
{
$type = $controllerParam->getClass();
if (null === $type) {
return false;
}
return $type->implementsInterface(ParamFetcherInterface::class);
}
}
EventListener/VersionListener.php 0000666 00000003522 13052362131 0013201 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\EventListener;
use FOS\RestBundle\FOSRestBundle;
use FOS\RestBundle\Version\VersionResolverInterface;
use FOS\RestBundle\View\ConfigurableViewHandlerInterface;
use FOS\RestBundle\View\ViewHandlerInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
/**
* @internal
*/
class VersionListener
{
private $viewHandler;
private $versionResolver;
private $defaultVersion;
private $version = false;
public function __construct(ViewHandlerInterface $viewHandler, VersionResolverInterface $versionResolver, $defaultVersion = null)
{
$this->viewHandler = $viewHandler;
$this->versionResolver = $versionResolver;
$this->defaultVersion = $defaultVersion;
}
/**
* Gets the version.
*
* @return mixed
*/
public function getVersion()
{
return $this->version;
}
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!$request->attributes->get(FOSRestBundle::ZONE_ATTRIBUTE, true)) {
return;
}
$this->version = $this->versionResolver->resolve($request);
if (false === $this->version && null !== $this->defaultVersion) {
$this->version = $this->defaultVersion;
}
if (false !== $this->version) {
$request->attributes->set('version', $this->version);
if ($this->viewHandler instanceof ConfigurableViewHandlerInterface) {
$this->viewHandler->setExclusionStrategyVersion($this->version);
}
}
}
}
EventListener/ViewResponseListener.php 0000666 00000013013 13052362131 0014201 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\EventListener;
use FOS\RestBundle\Controller\Annotations\View as ViewAnnotation;
use FOS\RestBundle\FOSRestBundle;
use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandlerInterface;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Templating\TemplateReferenceInterface;
/**
* The ViewResponseListener class handles the View core event as well as the "@extra:Template" annotation.
*
* @author Lukas Kahwe Smith
*
* @internal
*/
class ViewResponseListener implements EventSubscriberInterface
{
private $viewHandler;
private $forceView;
/**
* Constructor.
*
* @param ViewHandlerInterface $viewHandler
* @param bool $forceView
*/
public function __construct(ViewHandlerInterface $viewHandler, $forceView)
{
$this->viewHandler = $viewHandler;
$this->forceView = $forceView;
}
/**
* Renders the parameters and template and initializes a new response object with the
* rendered content.
*
* @param GetResponseForControllerResultEvent $event
*/
public function onKernelView(GetResponseForControllerResultEvent $event)
{
$request = $event->getRequest();
if (!$request->attributes->get(FOSRestBundle::ZONE_ATTRIBUTE, true)) {
return false;
}
$configuration = $request->attributes->get('_template');
$view = $event->getControllerResult();
if (!$view instanceof View) {
if (!$configuration instanceof ViewAnnotation && !$this->forceView) {
return;
}
$view = new View($view);
}
if ($configuration instanceof ViewAnnotation) {
if ($configuration->getTemplateVar()) {
$view->setTemplateVar($configuration->getTemplateVar());
}
if (null !== $configuration->getStatusCode() && (null === $view->getStatusCode() || Response::HTTP_OK === $view->getStatusCode())) {
$view->setStatusCode($configuration->getStatusCode());
}
$context = $view->getContext();
if ($configuration->getSerializerGroups()) {
$context->addGroups($configuration->getSerializerGroups());
}
if ($configuration->getSerializerEnableMaxDepthChecks()) {
$context->setMaxDepth(0, false);
}
if (true === $configuration->getSerializerEnableMaxDepthChecks()) {
$context->enableMaxDepth();
} elseif (false === $configuration->getSerializerEnableMaxDepthChecks()) {
$context->disableMaxDepth();
}
list($controller, $action) = $configuration->getOwner();
$vars = $this->getDefaultVars($configuration, $controller, $action);
} else {
$vars = null;
}
if (null === $view->getFormat()) {
$view->setFormat($request->getRequestFormat());
}
if ($this->viewHandler->isFormatTemplating($view->getFormat())
&& !$view->getRoute()
&& !$view->getLocation()
) {
if (null !== $vars && 0 !== count($vars)) {
$parameters = (array) $this->viewHandler->prepareTemplateParameters($view);
foreach ($vars as $var) {
if (!array_key_exists($var, $parameters)) {
$parameters[$var] = $request->attributes->get($var);
}
}
$view->setData($parameters);
}
if ($configuration && ($template = $configuration->getTemplate()) && !$view->getTemplate()) {
if ($template instanceof TemplateReferenceInterface) {
$template->set('format', null);
}
$view->setTemplate($template);
}
}
$response = $this->viewHandler->handle($view, $request);
$event->setResponse($response);
}
public static function getSubscribedEvents()
{
// Must be executed before SensioFrameworkExtraBundle's listener
return array(
KernelEvents::VIEW => array('onKernelView', 30),
);
}
/**
* @param Request $request
* @param Template $template
* @param object $controller
* @param string $action
*
* @return array
*
* @see \Sensio\Bundle\FrameworkExtraBundle\EventListener\TemplateListener::resolveDefaultParameters()
*/
private function getDefaultVars(Template $template = null, $controller, $action)
{
if (0 !== count($arguments = $template->getVars())) {
return $arguments;
}
if (!$template instanceof ViewAnnotation || $template->isPopulateDefaultVars()) {
$r = new \ReflectionObject($controller);
$arguments = array();
foreach ($r->getMethod($action)->getParameters() as $param) {
$arguments[] = $param->getName();
}
return $arguments;
}
}
}
EventListener/ZoneMatcherListener.php 0000666 00000002633 13052362131 0013775 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\EventListener;
use FOS\RestBundle\FOSRestBundle;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
/**
* Matches FOSRest's zones.
*
* @author Florian Voutzinos
*
* @internal
*/
class ZoneMatcherListener
{
/**
* @var RequestMatcherInterface[]
*/
private $requestMatchers = array();
public function addRequestMatcher(RequestMatcherInterface $requestMatcher)
{
$this->requestMatchers[] = $requestMatcher;
}
/**
* Adds an optional "_fos_rest_zone" request attribute to be checked for existence by other listeners.
*
* @param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
foreach ($this->requestMatchers as $requestMatcher) {
if ($requestMatcher->matches($request)) {
$request->attributes->set(FOSRestBundle::ZONE_ATTRIBUTE, true);
return;
}
}
$request->attributes->set(FOSRestBundle::ZONE_ATTRIBUTE, false);
}
}
Exception/InvalidParameterException.php 0000666 00000004103 13052362131 0014317 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Exception;
use FOS\RestBundle\Controller\Annotations\ParamInterface;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Validator\ConstraintViolationListInterface;
class InvalidParameterException extends BadRequestHttpException
{
private $parameter;
private $violations;
public function getParameter()
{
return $this->parameter;
}
public function getViolations()
{
return $this->violations;
}
public static function withViolations(ParamInterface $parameter, ConstraintViolationListInterface $violations)
{
$message = '';
foreach ($violations as $key => $violation) {
if ($key > 0) {
$message .= "\n";
}
$invalidValue = $violation->getInvalidValue();
$message .= sprintf(
'Parameter "%s" of value "%s" violated a constraint "%s"',
$parameter->getName(),
is_scalar($invalidValue) ? $invalidValue : var_export($invalidValue, true),
$violation->getMessage()
);
}
return self::withViolationsAndMessage($parameter, $violations, $message);
}
/**
* Do not use this method. It will be removed in 2.0.
*
* @param ParamInterface $parameter
* @param ConstraintViolationListInterface $violations
* @param string $message
*
* @return self
*
* @internal
*/
public static function withViolationsAndMessage(ParamInterface $parameter, ConstraintViolationListInterface $violations, $message)
{
$exception = new self($message);
$exception->parameter = $parameter;
$exception->violations = $violations;
return $exception;
}
}
FOSRestBundle.php 0000666 00000002673 13052362131 0007704 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle;
use FOS\RestBundle\DependencyInjection\Compiler\ConfigurationCheckPass;
use FOS\RestBundle\DependencyInjection\Compiler\JMSHandlersPass;
use FOS\RestBundle\DependencyInjection\Compiler\FormatListenerRulesPass;
use FOS\RestBundle\DependencyInjection\Compiler\SerializerConfigurationPass;
use FOS\RestBundle\DependencyInjection\Compiler\TwigExceptionPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
use Symfony\Component\HttpKernel\Bundle\Bundle;
/**
* @author Lukas Kahwe Smith
* @author Eriksen Costa
*/
class FOSRestBundle extends Bundle
{
const ZONE_ATTRIBUTE = '_fos_rest_zone';
/**
* {@inheritdoc}
*/
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new SerializerConfigurationPass());
$container->addCompilerPass(new ConfigurationCheckPass());
$container->addCompilerPass(new FormatListenerRulesPass());
$container->addCompilerPass(new TwigExceptionPass());
$container->addCompilerPass(new JMSHandlersPass(), PassConfig::TYPE_REMOVE);
}
}
Form/Extension/DisableCSRFExtension.php 0000666 00000003466 13052362131 0014063 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Form\Extension;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
/**
* Class DisableCSRFExtension.
*
* @author Grégoire Pineau
*/
class DisableCSRFExtension extends AbstractTypeExtension
{
/**
* @var TokenStorageInterface
*/
private $tokenStorage;
/**
* @var string
*/
private $role;
/**
* @var AuthorizationCheckerInterface
*/
private $authorizationChecker;
public function __construct(TokenStorageInterface $tokenStorage, $role, AuthorizationCheckerInterface $authorizationChecker)
{
$this->tokenStorage = $tokenStorage;
$this->role = $role;
$this->authorizationChecker = $authorizationChecker;
}
public function configureOptions(OptionsResolver $resolver)
{
if (!$this->tokenStorage->getToken()) {
return;
}
if (!$this->authorizationChecker->isGranted($this->role)) {
return;
}
$resolver->setDefaults([
'csrf_protection' => false,
]);
}
public function getExtendedType()
{
return method_exists(AbstractType::class, 'getBlockPrefix')
? FormType::class
: 'form' // SF <2.8 BC
;
}
}
Form/Transformer/EntityToIdObjectTransformer.php 0000666 00000004252 13052362131 0016073 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Form\Transformer;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* Class EntityToIdObjectTransformer.
*
* @author Marc Juchli
*/
class EntityToIdObjectTransformer implements DataTransformerInterface
{
/**
* @var ObjectManager
*/
private $om;
/**
* @var string
*/
private $entityName;
/**
* @param ObjectManager $om
* @param string $entityName
*/
public function __construct(ObjectManager $om, $entityName)
{
$this->entityName = $entityName;
$this->om = $om;
}
/**
* Do nothing.
*
* @param object|null $object
*
* @return string
*/
public function transform($object)
{
if (null === $object) {
return '';
}
return current(array_values($this->om->getClassMetadata($this->entityName)->getIdentifierValues($object)));
}
/**
* Transforms an array including an identifier to an object.
*
* @param array $idObject
*
* @throws TransformationFailedException if object is not found
*
* @return object|null
*/
public function reverseTransform($idObject)
{
if (!is_array($idObject)) {
return;
}
$identifier = current(array_values($this->om->getClassMetadata($this->entityName)->getIdentifier()));
$id = $idObject[$identifier];
$object = $this->om
->getRepository($this->entityName)
->findOneBy([$identifier => $id]);
if (null === $object) {
throw new TransformationFailedException(sprintf(
'An object with identifier key "%s" and value "%s" does not exist!',
$identifier, $id
));
}
return $object;
}
}
Inflector/DoctrineInflector.php 0000666 00000001160 13052362131 0012615 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Inflector;
use Doctrine\Common\Inflector\Inflector;
/**
* Inflector object using the Doctrine/Inflector.
*
* @author Mark Kazemier
*/
class DoctrineInflector implements InflectorInterface
{
/**
* {@inheritdoc}
*/
public function pluralize($word)
{
return Inflector::pluralize($word);
}
}
Inflector/InflectorInterface.php 0000666 00000001036 13052362131 0012750 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Inflector;
/**
* Inflector interface.
*
* @author Mark Kazemier
*/
interface InflectorInterface
{
/**
* Pluralizes noun.
*
* @param string $word
*
* @return string
*/
public function pluralize($word);
}
Negotiation/FormatNegotiator.php 0000666 00000012746 13052362131 0013033 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Negotiation;
use FOS\RestBundle\Util\StopFormatListenerException;
use Negotiation\Accept;
use Negotiation\Negotiator as BaseNegotiator;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcherInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* @author Ener-Getick
*/
class FormatNegotiator extends BaseNegotiator
{
private $map = [];
private $requestStack;
public function __construct(RequestStack $requestStack, array $mimeTypes = array())
{
$this->requestStack = $requestStack;
$this->mimeTypes = $mimeTypes;
}
/**
* @param RequestMatcherInterface $requestMatcher
* @param array $options
*/
public function add(RequestMatcherInterface $requestMatcher, array $options = [])
{
$this->map[] = [$requestMatcher, $options];
}
/**
* {@inheritdoc}
* The best format is also determined in function of the bundle configuration.
*
* @throws StopFormatListenerException
*/
public function getBest($header, array $priorities = [])
{
$request = $this->getRequest();
$header = $header ?: $request->headers->get('Accept');
foreach ($this->map as $elements) {
// Check if the current RequestMatcherInterface matches the current request
if (!$elements[0]->matches($request)) {
continue;
}
$options = &$elements[1]; // Do not reallow memory for this variable
if (!empty($options['stop'])) {
throw new StopFormatListenerException('Stopped format listener');
}
if (empty($options['priorities']) && empty($priorities)) {
if (!empty($options['fallback_format'])) {
return new Accept($request->getMimeType($options['fallback_format']));
}
continue;
}
if (isset($options['prefer_extension']) && $options['prefer_extension'] && !isset($extensionHeader)) {
$extension = pathinfo($request->getPathInfo(), PATHINFO_EXTENSION);
if (!empty($extension)) {
// $extensionHeader will now be either a non empty string or an empty string
$extensionHeader = $request->getMimeType($extension);
if ($header && $extensionHeader) {
$header .= ',';
}
$header .= $extensionHeader.'; q='.$options['prefer_extension'];
}
}
if ($header) {
$mimeTypes = $this->normalizePriorities($request,
empty($priorities) ? $options['priorities'] : $priorities
);
$mimeType = parent::getBest($header, $mimeTypes);
if ($mimeType !== null) {
return $mimeType;
}
}
if (isset($options['fallback_format'])) {
// if false === fallback_format then we fail here instead of considering more rules
if (false === $options['fallback_format']) {
return;
}
// stop looking at rules since we have a fallback defined
return new Accept($request->getMimeType($options['fallback_format']));
}
}
}
/**
* @param array $values
*
* @return array
*/
private function sanitize(array $values)
{
return array_map(function ($value) {
return preg_replace('/\s+/', '', strtolower($value));
}, $values);
}
/**
* Transform the format (json, html, ...) to their mimeType form (application/json, text/html, ...).
*
* @param Request $request
* @param string[] $priorities
*
* @return string[] formatted priorities
*/
private function normalizePriorities(Request $request, array $priorities)
{
$priorities = $this->sanitize($priorities);
$mimeTypes = array();
foreach ($priorities as $priority) {
if (strpos($priority, '/')) {
$mimeTypes[] = $priority;
continue;
}
if (method_exists(Request::class, 'getMimeTypes')) {
$mimeTypes = array_merge($mimeTypes, Request::getMimeTypes($priority));
} elseif (null !== $request->getMimeType($priority)) {
$class = new \ReflectionClass(Request::class);
$properties = $class->getStaticProperties();
$mimeTypes = array_merge($mimeTypes, $properties['formats'][$priority]);
}
if (isset($this->mimeTypes[$priority])) {
foreach ($this->mimeTypes[$priority] as $mimeType) {
$mimeTypes[] = $mimeType;
}
}
}
return $mimeTypes;
}
/**
* @throws \RuntimeException
*
* @return Request
*/
private function getRequest()
{
$request = $this->requestStack->getCurrentRequest();
if ($request === null) {
throw new \RuntimeException('There is no current request.');
}
return $request;
}
}
Normalizer/ArrayNormalizerInterface.php 0000666 00000001241 13052362131 0014337 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Normalizer;
/**
* Normalizes arrays.
*
* @author Florian Voutzinos
*/
interface ArrayNormalizerInterface
{
/**
* Normalizes the array.
*
* @param array $data The array to normalize
*
* @throws Exception\NormalizationException
*
* @return array The normalized array
*/
public function normalize(array $data);
}
Normalizer/CamelKeysNormalizer.php 0000666 00000004057 13052362131 0013325 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Normalizer;
use FOS\RestBundle\Normalizer\Exception\NormalizationException;
/**
* Normalizes the array by changing its keys from underscore to camel case.
*
* @author Florian Voutzinos
*/
class CamelKeysNormalizer implements ArrayNormalizerInterface
{
/**
* {@inheritdoc}
*/
public function normalize(array $data)
{
$this->normalizeArray($data);
return $data;
}
/**
* Normalizes an array.
*
* @param array &$data
*
* @throws Exception\NormalizationException
*/
private function normalizeArray(array &$data)
{
$normalizedData = array();
foreach ($data as $key => $val) {
$normalizedKey = $this->normalizeString($key);
if ($normalizedKey !== $key) {
if (array_key_exists($normalizedKey, $normalizedData)) {
throw new NormalizationException(sprintf(
'The key "%s" is invalid as it will override the existing key "%s"',
$key,
$normalizedKey
));
}
}
$normalizedData[$normalizedKey] = $val;
$key = $normalizedKey;
if (is_array($val)) {
$this->normalizeArray($normalizedData[$key]);
}
}
$data = $normalizedData;
}
/**
* Normalizes a string.
*
* @param string $string
*
* @return string
*/
protected function normalizeString($string)
{
if (false === strpos($string, '_')) {
return $string;
}
return preg_replace_callback('/_([a-zA-Z0-9])/', function ($matches) {
return strtoupper($matches[1]);
}, $string);
}
}
Normalizer/CamelKeysNormalizerWithLeadingUnderscore.php 0000666 00000002216 13052362131 0017472 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Normalizer;
/**
* Normalizes the array by changing its keys from underscore to camel case, while
* leaving leading underscores unchanged.
*
* @author Lukas Kahwe Smith
*/
class CamelKeysNormalizerWithLeadingUnderscore extends CamelKeysNormalizer
{
/**
* Normalizes a string while leaving leading underscores unchanged.
*
* @param string $string
*
* @return string
*/
protected function normalizeString($string)
{
if (false === strpos($string, '_')) {
return $string;
}
$offset = strspn($string, '_');
if ($offset) {
$underscorePrefix = substr($string, 0, $offset);
$string = substr($string, $offset);
} else {
$underscorePrefix = '';
}
return $underscorePrefix.parent::normalizeString($string);
}
}
Normalizer/Exception/NormalizationException.php 0000666 00000000743 13052362131 0016046 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Normalizer\Exception;
/**
* Exception thrown when the normalization failed.
*
* @author Florian Voutzinos
*/
class NormalizationException extends \RuntimeException
{
}
README.md 0000666 00000004067 13052362131 0006032 0 ustar 00 FOSRestBundle
=============
This bundle provides various tools to rapidly develop RESTful API's &
applications with Symfony. Features include:
- A View layer to enable output and format agnostic Controllers
- A custom route loader to generate url's following REST conventions
- Accept header format negotiation including handling for custom mime types
- RESTful decoding of HTTP request body and Accept headers
- Exception controller for sending appropriate HTTP status codes
[![Build Status](https://travis-ci.org/FriendsOfSymfony/FOSRestBundle.svg?branch=master)](https://travis-ci.org/FriendsOfSymfony/FOSRestBundle)
[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/FriendsOfSymfony/FOSRestBundle/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/FriendsOfSymfony/FOSRestBundle/?branch=master)
[![Code Coverage](https://scrutinizer-ci.com/g/FriendsOfSymfony/FOSRestBundle/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/FriendsOfSymfony/FOSRestBundle/?branch=master)
[![Total Downloads](https://poser.pugx.org/FriendsOfSymfony/rest-bundle/downloads.svg)](https://packagist.org/packages/FriendsOfSymfony/rest-bundle)
[![Latest Stable Version](https://poser.pugx.org/FriendsOfSymfony/rest-bundle/v/stable.svg)](https://packagist.org/packages/FriendsOfSymfony/rest-bundle)
[![SensioLabsInsight](https://insight.sensiolabs.com/projects/0be23389-2e85-49cf-b333-caaa36d11c62/mini.png)](https://insight.sensiolabs.com/projects/0be23389-2e85-49cf-b333-caaa36d11c62)
Documentation
-------------
[Read the Documentation](http://symfony.com/doc/master/bundles/FOSRestBundle/index.html)
Please see the [UPGRADING-2.0.md](https://github.com/FriendsOfSymfony/FOSRestBundle/blob/master/UPGRADING-2.0.md) for any
relevant instructions when upgrading to a newer version.
Installation
------------
All the installation instructions are located in the [documentation](http://symfony.com/doc/master/bundles/FOSRestBundle/1-setting_up_the_bundle.html).
License
-------
This bundle is under the MIT license. See the complete license in the bundle:
Resources/meta/LICENSE
Request/ParamFetcher.php 0000666 00000016311 13052362131 0011250 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Request;
use FOS\RestBundle\Controller\Annotations\ParamInterface;
use FOS\RestBundle\Exception\InvalidParameterException;
use FOS\RestBundle\Util\ResolverTrait;
use FOS\RestBundle\Validator\Constraints\ResolvableConstraintInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Validator\Exception\ValidatorException;
use Symfony\Component\Validator\ConstraintViolation;
/**
* Helper to validate parameters of the active request.
*
* @author Alexander
* @author Lukas Kahwe Smith
* @author Jordi Boggiano
* @author Boris Guéry
*/
class ParamFetcher implements ParamFetcherInterface
{
use ResolverTrait;
private $container;
private $parameterBag;
private $requestStack;
private $validator;
/**
* Initializes fetcher.
*
* @param ContainerInterface $container
* @param ParamReaderInterface $paramReader
* @param RequestStack $requestStack
* @param ValidatorInterface $validator
*/
public function __construct(ContainerInterface $container, ParamReaderInterface $paramReader, RequestStack $requestStack, ValidatorInterface $validator = null)
{
$this->container = $container;
$this->requestStack = $requestStack;
$this->validator = $validator;
$this->parameterBag = new ParameterBag($paramReader);
}
/**
* {@inheritdoc}
*/
public function setController($controller)
{
$this->parameterBag->setController($this->getRequest(), $controller);
}
/**
* Add additional params to the ParamFetcher during runtime.
*
* Note that adding a param that has the same name as an existing param will override that param.
*
* @param ParamInterface $param
*/
public function addParam(ParamInterface $param)
{
$this->parameterBag->addParam($this->getRequest(), $param);
}
/**
* @return ParamInterface[]
*/
public function getParams()
{
return $this->parameterBag->getParams($this->getRequest());
}
/**
* {@inheritdoc}
*/
public function get($name, $strict = null)
{
$params = $this->getParams();
if (!array_key_exists($name, $params)) {
throw new \InvalidArgumentException(sprintf("No @ParamInterface configuration for parameter '%s'.", $name));
}
/** @var ParamInterface $param */
$param = $params[$name];
$default = $param->getDefault();
$strict = (null !== $strict ? $strict : $param->isStrict());
$paramValue = $param->getValue($this->getRequest(), $default);
return $this->cleanParamWithRequirements($param, $paramValue, $strict);
}
/**
* @param ParamInterface $param
* @param mixed $paramValue
* @param bool $strict
*
* @throws BadRequestHttpException
* @throws \RuntimeException
*
* @return mixed
*
* @internal
*/
protected function cleanParamWithRequirements(ParamInterface $param, $paramValue, $strict)
{
$default = $param->getDefault();
$default = $this->resolveValue($this->container, $default);
$this->checkNotIncompatibleParams($param);
if ($default !== null && $default === $paramValue) {
return $paramValue;
}
$constraints = $param->getConstraints();
$this->resolveConstraints($constraints);
if (empty($constraints)) {
return $paramValue;
}
if (null === $this->validator) {
throw new \RuntimeException(
'The ParamFetcher requirements feature requires the symfony/validator component.'
);
}
try {
$errors = $this->validator->validate($paramValue, $constraints);
} catch (ValidatorException $e) {
$violation = new ConstraintViolation(
$e->getMessage(),
$e->getMessage(),
array(),
$paramValue,
'',
null,
null,
$e->getCode()
);
$errors = new ConstraintViolationList(array($violation));
}
if (0 < count($errors)) {
if ($strict) {
throw InvalidParameterException::withViolations($param, $errors);
}
return null === $default ? '' : $default;
}
return $paramValue;
}
/**
* {@inheritdoc}
*/
public function all($strict = null)
{
$configuredParams = $this->getParams();
$params = [];
foreach ($configuredParams as $name => $param) {
$params[$name] = $this->get($name, $strict);
}
return $params;
}
/**
* Check if current param is not in conflict with other parameters
* according to the "incompatibles" field.
*
* @param ParamInterface $param the configuration for the param fetcher
*
* @throws InvalidArgumentException
* @throws BadRequestHttpException
*
* @internal
*/
protected function checkNotIncompatibleParams(ParamInterface $param)
{
$params = $this->getParams();
foreach ($param->getIncompatibilities() as $incompatibleParamName) {
if (!array_key_exists($incompatibleParamName, $params)) {
throw new \InvalidArgumentException(sprintf("No @ParamInterface configuration for parameter '%s'.", $incompatibleParamName));
}
$incompatibleParam = $params[$incompatibleParamName];
if ($incompatibleParam->getValue($this->getRequest(), null) !== null) {
$exceptionMessage = sprintf(
"'%s' param is incompatible with %s param.",
$param->getName(),
$incompatibleParam->getName()
);
throw new BadRequestHttpException($exceptionMessage);
}
}
}
/**
* @param Constraint[] $constraints
*/
private function resolveConstraints(array $constraints)
{
foreach ($constraints as $constraint) {
if ($constraint instanceof ResolvableConstraintInterface) {
$constraint->resolve($this->container);
}
}
}
/**
* @throws \RuntimeException
*
* @return Request
*/
private function getRequest()
{
$request = $this->requestStack->getCurrentRequest();
if ($request === null) {
throw new \RuntimeException('There is no current request.');
}
return $request;
}
}
Request/ParamFetcherInterface.php 0000666 00000002241 13052362131 0013066 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Request;
/**
* Helper interface to validate query parameters from the active request.
*
* @author Alexander
* @author Lukas Kahwe Smith
*/
interface ParamFetcherInterface
{
/**
* Sets the controller.
*
* @param callable $controller
*/
public function setController($controller);
/**
* Gets a validated parameter.
*
* @param string $name Name of the parameter
* @param bool $strict Whether a requirement mismatch should cause an exception
*
* @return mixed Value of the parameter
*/
public function get($name, $strict = null);
/**
* Gets all validated parameter.
*
* @param bool $strict Whether a requirement mismatch should cause an exception
*
* @return array Values of all the parameters
*/
public function all($strict = false);
}
Request/ParamReader.php 0000666 00000004625 13052362131 0011077 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Request;
use Doctrine\Common\Annotations\Reader;
use FOS\RestBundle\Controller\Annotations\ParamInterface;
/**
* Class loading "@ParamInterface" annotations from methods.
*
* @author Alexander
* @author Lukas Kahwe Smith
* @author Boris Guéry
*/
class ParamReader implements ParamReaderInterface
{
private $annotationReader;
/**
* Initializes controller reader.
*
* @param Reader $annotationReader
*/
public function __construct(Reader $annotationReader)
{
$this->annotationReader = $annotationReader;
}
/**
* {@inheritdoc}
*/
public function read(\ReflectionClass $reflection, $method)
{
if (!$reflection->hasMethod($method)) {
throw new \InvalidArgumentException(sprintf("Class '%s' has no method '%s'.", $reflection->getName(), $method));
}
$methodParams = $this->getParamsFromMethod($reflection->getMethod($method));
$classParams = $this->getParamsFromClass($reflection);
return array_merge($methodParams, $classParams);
}
/**
* {@inheritdoc}
*/
public function getParamsFromMethod(\ReflectionMethod $method)
{
$annotations = $this->annotationReader->getMethodAnnotations($method);
return $this->getParamsFromAnnotationArray($annotations);
}
/**
* {@inheritdoc}
*/
public function getParamsFromClass(\ReflectionClass $class)
{
$annotations = $this->annotationReader->getClassAnnotations($class);
return $this->getParamsFromAnnotationArray($annotations);
}
/**
* Fetches parameters from provided annotation array (fetched from annotationReader).
*
* @param array $annotations
*
* @return ParamInterface[]
*/
private function getParamsFromAnnotationArray(array $annotations)
{
$params = array();
foreach ($annotations as $annotation) {
if ($annotation instanceof ParamInterface) {
$params[$annotation->getName()] = $annotation;
}
}
return $params;
}
}
Request/ParamReaderInterface.php 0000666 00000002623 13052362131 0012714 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Request;
use FOS\RestBundle\Controller\Annotations\ParamInterface;
/**
* Interface for loading query parameters for a method.
*
* @author Alexander
* @author Lukas Kahwe Smith
*/
interface ParamReaderInterface
{
/**
* Read annotations for a given method.
*
* @param \ReflectionClass $reflection Reflection class
* @param string $method Method name
*
* @return ParamInterface[] Param annotation objects of the method. Indexed by parameter name
*/
public function read(\ReflectionClass $reflection, $method);
/**
* Read annotations for a given method.
*
* @param \ReflectionMethod $method Reflection method
*
* @return ParamInterface[] Param annotation objects of the method. Indexed by parameter name
*/
public function getParamsFromMethod(\ReflectionMethod $method);
/**
* @param \ReflectionClass $class
*
* @return ParamInterface[] Param annotation objects of the class. Indexed by parameter name
*/
public function getParamsFromClass(\ReflectionClass $class);
}
Request/ParameterBag.php 0000666 00000004626 13052362131 0011247 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Request;
use Doctrine\Common\Util\ClassUtils;
use FOS\RestBundle\Controller\Annotations\ParamInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Contains the {@link ParamFetcher} params and links them to a request.
*
* @internal
*/
final class ParameterBag
{
private $paramReader;
private $params = array();
public function __construct(ParamReaderInterface $paramReader)
{
$this->paramReader = $paramReader;
}
public function getParams(Request $request)
{
$requestId = spl_object_hash($request);
if (!isset($this->params[$requestId]) || empty($this->params[$requestId]['controller'])) {
throw new \InvalidArgumentException('Controller and method needs to be set via setController.');
}
if ($this->params[$requestId]['params'] === null) {
return $this->initParams($requestId);
}
return $this->params[$requestId]['params'];
}
public function addParam(Request $request, ParamInterface $param)
{
$requestId = spl_object_hash($request);
$this->getParams($request);
$this->params[$requestId]['params'][$param->getName()] = $param;
}
public function setController(Request $request, $controller)
{
$requestId = spl_object_hash($request);
$this->params[$requestId] = array(
'controller' => $controller,
'params' => null,
);
}
/**
* Initialize the parameters.
*
* @param string $requestId
*
* @throws \InvalidArgumentException
*/
private function initParams($requestId)
{
$controller = $this->params[$requestId]['controller'];
if (!is_array($controller) || empty($controller[0]) || !is_object($controller[0])) {
throw new \InvalidArgumentException(
'Controller needs to be set as a class instance (closures/functions are not supported)'
);
}
return $this->params[$requestId]['params'] = $this->paramReader->read(
new \ReflectionClass(ClassUtils::getClass($controller[0])),
$controller[1]
);
}
}
Request/RequestBodyParamConverter.php 0000666 00000014143 13052362131 0014027 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Request;
use FOS\RestBundle\Context\Context;
use FOS\RestBundle\Serializer\Serializer;
use JMS\Serializer\Exception\Exception as JMSSerializerException;
use JMS\Serializer\Exception\UnsupportedFormatException;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Sensio\Bundle\FrameworkExtraBundle\Request\ParamConverter\ParamConverterInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Serializer\Exception\ExceptionInterface as SymfonySerializerException;
use Symfony\Component\Validator\Validator\ValidatorInterface;
/**
* @author Tyler Stroud
*/
class RequestBodyParamConverter implements ParamConverterInterface
{
private $serializer;
private $context = [];
private $validator;
/**
* The name of the argument on which the ConstraintViolationList will be set.
*
* @var null|string
*/
private $validationErrorsArgument;
/**
* @param Serializer $serializer
* @param array|null $groups An array of groups to be used in the serialization context
* @param string|null $version A version string to be used in the serialization context
* @param ValidatorInterface $validator
* @param string|null $validationErrorsArgument
*
* @throws \InvalidArgumentException
*/
public function __construct(
Serializer $serializer,
$groups = null,
$version = null,
ValidatorInterface $validator = null,
$validationErrorsArgument = null
) {
$this->serializer = $serializer;
if (!empty($groups)) {
$this->context['groups'] = (array) $groups;
}
if (!empty($version)) {
$this->context['version'] = $version;
}
if (null !== $validator && null === $validationErrorsArgument) {
throw new \InvalidArgumentException('"$validationErrorsArgument" cannot be null when using the validator');
}
$this->validator = $validator;
$this->validationErrorsArgument = $validationErrorsArgument;
}
/**
* {@inheritdoc}
*/
public function apply(Request $request, ParamConverter $configuration)
{
$options = (array) $configuration->getOptions();
if (isset($options['deserializationContext']) && is_array($options['deserializationContext'])) {
$arrayContext = array_merge($this->context, $options['deserializationContext']);
} else {
$arrayContext = $this->context;
}
$this->configureContext($context = new Context(), $arrayContext);
try {
$object = $this->serializer->deserialize(
$request->getContent(),
$configuration->getClass(),
$request->getContentType(),
$context
);
} catch (UnsupportedFormatException $e) {
return $this->throwException(new UnsupportedMediaTypeHttpException($e->getMessage(), $e), $configuration);
} catch (JMSSerializerException $e) {
return $this->throwException(new BadRequestHttpException($e->getMessage(), $e), $configuration);
} catch (SymfonySerializerException $e) {
return $this->throwException(new BadRequestHttpException($e->getMessage(), $e), $configuration);
}
$request->attributes->set($configuration->getName(), $object);
if (null !== $this->validator) {
$validatorOptions = $this->getValidatorOptions($options);
$errors = $this->validator->validate($object, null, $validatorOptions['groups']);
$request->attributes->set(
$this->validationErrorsArgument,
$errors
);
}
return true;
}
/**
* {@inheritdoc}
*/
public function supports(ParamConverter $configuration)
{
return null !== $configuration->getClass();
}
/**
* @param Context $context
* @param array $options
*/
protected function configureContext(Context $context, array $options)
{
foreach ($options as $key => $value) {
if ($key === 'groups') {
$context->addGroups($options['groups']);
} elseif ($key === 'version') {
$context->setVersion($options['version']);
} elseif ($key === 'maxDepth') {
@trigger_error('Context attribute "maxDepth" is deprecated since version 2.1 and will be removed in 3.0. Use "enable_max_depth" instead.', E_USER_DEPRECATED);
$context->setMaxDepth($options['maxDepth']);
} elseif ($key === 'enableMaxDepth') {
$context->enableMaxDepth($options['enableMaxDepth']);
} elseif ($key === 'serializeNull') {
$context->setSerializeNull($options['serializeNull']);
} else {
$context->setAttribute($key, $value);
}
}
}
/**
* Throws an exception or return false if a ParamConverter is optional.
*/
private function throwException(\Exception $exception, ParamConverter $configuration)
{
if ($configuration->isOptional()) {
return false;
}
throw $exception;
}
/**
* @param array $options
*
* @return array
*/
private function getValidatorOptions(array $options)
{
$resolver = new OptionsResolver();
$resolver->setDefaults([
'groups' => null,
'traverse' => false,
'deep' => false,
]);
return $resolver->resolve(isset($options['validator']) ? $options['validator'] : []);
}
}
Resources/config/access_denied_listener.xml 0000666 00000001234 13052362131 0015163 0 ustar 00
Resources/config/allowed_methods_listener.xml 0000666 00000001743 13052362131 0015571 0 ustar 00
%kernel.debug%
Resources/config/body_listener.xml 0000666 00000002704 13052362131 0013352 0 ustar 00
Resources/config/exception_listener.xml 0000666 00000004635 13052362131 0014420 0 ustar 00
fos_rest.exception.controller:showAction
Resources/config/format_listener.xml 0000666 00000001647 13052362131 0013712 0 ustar 00
Resources/config/forms.xml 0000666 00000001460 13052362131 0011634 0 ustar 00
Resources/config/mime_type_listener.xml 0000666 00000001113 13052362131 0014376 0 ustar 00
Resources/config/param_fetcher_listener.xml 0000666 00000001235 13052362131 0015213 0 ustar 00
false
Resources/config/request.xml 0000666 00000001606 13052362131 0012200 0 ustar 00
Resources/config/request_body_param_converter.xml 0000666 00000001650 13052362131 0016463 0 ustar 00
Resources/config/routing.xml 0000666 00000006166 13052362131 0012205 0 ustar 00
Resources/config/schema/routing-1.0.xsd 0000666 00000004461 13052362131 0013733 0 ustar 00
Resources/config/schema/routing/rest_routing-1.0.xsd 0000666 00000003546 13052362131 0016462 0 ustar 00
Resources/config/serializer.xml 0000666 00000002454 13052362131 0012663 0 ustar 00
FOS\RestBundle\Serializer\Normalizer\FormErrorHandler
Resources/config/versioning.xml 0000666 00000003100 13052362131 0012662 0 ustar 00
Resources/config/view.xml 0000666 00000002255 13052362131 0011463 0 ustar 00
Resources/config/view_response_listener.xml 0000666 00000001133 13052362131 0015300 0 ustar 00
Resources/config/zone_matcher_listener.xml 0000666 00000001264 13052362131 0015073 0 ustar 00
Resources/doc/1-setting_up_the_bundle.rst 0000666 00000003260 13052362131 0014526 0 ustar 00 Step 1: Setting up the bundle
=============================
A) Download the Bundle
----------------------
Open a command console, enter your project directory and execute the
following command to download the latest stable version of this bundle:
.. code-block:: bash
$ composer require friendsofsymfony/rest-bundle
This command requires you to have Composer installed globally, as explained
in the `installation chapter`_ of the Composer documentation.
B) Enable the Bundle
--------------------
Then, enable the bundle by adding the following line in the ``app/AppKernel.php``
file of your project:
.. code-block:: php
// app/AppKernel.php
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = array(
// ...
new FOS\RestBundle\FOSRestBundle(),
);
// ...
}
}
C) Enable a Serializer
----------------------
This bundle needs a serializer to work correctly. In most cases,
you'll need to enable a serializer or install one. This bundle tries
the following (in the given order) to determine the serializer to use:
#. The one you configured using ``fos_rest.service.serializer`` (if you did).
#. The JMS serializer, if the `JMSSerializerBundle`_ is available (and registered).
#. The `Symfony Serializer`_ if it's enabled (or any service called ``serializer``).
That was it!
.. _`installation chapter`: https://getcomposer.org/doc/00-intro.md
.. _`JMSSerializer`: https://github.com/schmittjoh/serializer
.. _`JMSSerializerBundle`: https://github.com/schmittjoh/JMSSerializerBundle
.. _`Symfony Serializer`: http://symfony.com/doc/current/cookbook/serializer.html
Resources/doc/2-the-view-layer.rst 0000666 00000042733 13052362131 0013027 0 ustar 00 Step 2: The view layer
======================
Introduction
------------
The view layer makes it possible to write ``format`` (html, json, xml, etc)
agnostic controllers, by placing a layer between the Controller and the
generation of the final output via the templating or a serializer.
The bundle works both with the `Symfony Serializer Component`_ and the more
sophisticated `serializer`_ created by Johannes Schmitt and integrated via the
`JMSSerializerBundle`_.
In your controller action you will then need to create a ``View`` instance that
is then passed to the ``fos_rest.view_handler`` service for processing. The
``View`` is somewhat modeled after the ``Response`` class, but as just stated
it simply works as a container for all the data/configuration for the
``ViewHandler`` class for this particular action. So the ``View`` instance
must always be processed by a ``ViewHandler`` (see the below section on the
"view response listener" for how to get this processing applied automatically)
FOSRestBundle ships with a controller extending the default Symfony controller,
which adds several convenience methods:
.. code-block:: php
view($data, 200)
->setTemplate("MyBundle:Users:getUsers.html.twig")
->setTemplateVar('users')
;
return $this->handleView($view);
}
public function redirectAction()
{
$view = $this->redirectView($this->generateUrl('some_route'), 301);
// or
$view = $this->routeRedirectView('some_route', array(), 301);
return $this->handleView($view);
}
}
.. versionadded:: 1.6
The ``setTemplateData`` method was added in 1.6.
There is also a trait called ``ControllerTrait`` for anyone that prefers to not
inject the container into their controller. This requires using setter injection
to set a ``ViewHandlerInterface`` instance via the ``setViewHandler`` method.
.. versionadded:: 2.0
The ``ControllerTrait`` trait was added in 2.0.
If you need to pass more data in template, not for serialization, you can use ``setTemplateData`` method:
.. code-block:: php
get('category_manager')->getBySlug($categorySlug);
$products = ...; // get data, in this case list of products.
$templateData = array('category' => $category);
$view = $this->view($products, 200)
->setTemplate("MyBundle:Category:show.html.twig")
->setTemplateVar('products')
->setTemplateData($templateData)
;
return $this->handleView($view);
}
}
or it is possible to use lazy-loading:
.. code-block:: php
get('category_manager');
$view = $this->view($products, 200)
->setTemplate("MyBundle:Category:show.html.twig")
->setTemplateVar('products')
->setTemplateData(function (ViewHandlerInterface $viewHandler, ViewInterface $view) use ($categoryManager, $categorySlug) {
$category = $categoryManager->getBySlug($categorySlug);
return array(
'category' => $category,
);
})
;
return $this->handleView($view);
}
}
To simplify this even more: If you rely on the ``ViewResponseListener`` in
combination with SensioFrameworkExtraBundle you can even omit the calls to
``$this->handleView($view)`` and directly return the view objects. See chapter
3 on listeners for more details on the View Response Listener.
As the purpose is to create a format-agnostic controller, data assigned to the
``View`` instance should ideally be an object graph, though any data type is
acceptable. Note that when rendering templating formats, the ``ViewHandler``
will wrap data types other than associative arrays in an associative array with
a single key (default ``'data'``), which will become the variable name of the
object in the respective template. You can change this variable by calling
the ``setTemplateVar()`` method on the view object.
There are also two specialized methods for redirect in the ``View`` classes.
``View::createRedirect`` redirects to an URL called ``RedirectView`` and
``View::createRouteRedirect`` redirects to a route. Note that whether these
classes actually cause a redirect or not is determined by the ``force_redirects``
configuration option, which is only enabled for ``html`` by default (see below).
There are several more methods on the ``View`` class, here is a list of all
the important ones for configuring the view:
* ``setData($data)`` - Set the object graph or list of objects to serialize.
* ``setTemplateData($data)`` - Set the template data array or anonymous function. Closure should return array.
* ``setHeader($name, $value)`` - Set a header to put on the HTTP response.
* ``setHeaders(array $headers)`` - Set multiple headers to put on the HTTP response.
* ``setStatusCode($code)`` - Set the HTTP status code.
* ``getContext()`` - The serialization context to use.
* ``setTemplate($template)`` - Name of the template to use in case of HTML rendering.
* ``setTemplateVar($templateVar)`` - Name of the variable the data is in, when passed
to HTML template. Defaults to ``'data'``.
* ``setEngine($engine)`` - Name of the engine to render HTML template. Can be
autodetected.
* ``setFormat($format)`` - The format the response is supposed to be rendered in.
Can be autodetected using HTTP semantics.
* ``setLocation($location)`` - The location to redirect to with a response.
* ``setRoute($route)`` - The route to redirect to with a response.
* ``setRouteParameters($parameters)`` - Set the parameters for the route.
* ``setResponse(Response $response)`` - The response instance that is populated
by the ``ViewHandler``.
See `this example code`_ for more details.
Forms and Views
---------------
Symfony Forms have special handling inside the view layer. Whenever you:
- return a Form from the controller.
- Set the form as only data of the view.
- return an array with a 'form' key, containing a form.
- return a form with validation errors.
Then:
- If the form is bound and no status code is set explicitly, an invalid form
leads to a "validation failed" response.
- In a rendered template, the form is passed as 'form' and ``createView()``
is called automatically.
- ``$form->getData()`` is passed into the view as template as ``'data'`` if the
form is the only view data.
- An invalid form will be wrapped into an exception.
A response example of an invalid form:
.. code-block:: javascript
{
"code": 400,
"message": "Validation Failed";
"errors": {
"children": {
"username": {
"errors": [
"This value should not be blank."
]
}
}
}
}
If you don't like the default exception structure, you can provide your own
normalizers.
You can look at `FOSRestBundle normalizers`_ for examples.
.. _`FOSRestBundle normalizers`: https://github.com/FriendsOfSymfony/FOSRestBundle/tree/master/Serializer/Normalizer
Data Transformation
-------------------
As we have seen in the section before, the FOSRestBundle relies on the form
component (http://symfony.com/doc/current/components/form/introduction.html) to
handle submission of view data. In fact, the form builder
(http://symfony.com/doc/current/book/forms.html#building-the-form) basically
defines the structure of the expected view data which shall be used for further
processing - which most of the time relates to a PUT or POST request. This
brings a lot of flexibility and allows to exactly define the structure of data
to be received by the api.
Most of the time the requirements regarding a PUT/POST request are, in
terms of data structure, fairly simple. The payload within a PUT or POST request
oftentimes will have the exact same structure as received by a previous GET
request, but only with modified value fields. Thus, the fields to be defined
within the form builder process will be the same as the fields marked to be
serialized within an entity.
However, there is a common use case where straightforward updating of data,
received by a serialized object (GET request), will not work out of the box using
the given implementation of the form component: Simple assignment of a reference
using an object.
Let's take an entity ``Task`` that holds a reference to a ``Person`` as
an example. The serialized Task object will looks as follows:
.. code-block:: json
{"task_form":{"name":"Task1", "person":{"id":1, "name":"Fabien"}}}
In a traditional Symfony application we simply define the property of the
related class and it would perfectly assign the person to our task - in this
case based on the id:
.. code-block:: php
$builder
->add('name', 'text')
...
->add('person', 'entity', array(
'class' => 'Acme\DemoBundle\Entity\Person',
'property' => 'id'
))
Unfortunately, this form builder does not accept our serialized object as it is
- even though it contains the necessary id. In fact, the object would have to
contain the id directly assigned to the person field to be accepted by the
form validation process:
.. code-block:: json
{"task_form":{"name":"Task1", "person":1}}
Well, this is somewhat useless since we not only want to display the name of the
person but also do not want to do some client side trick to extract the id
before updating the data, right? Instead, we rather update the data the same way
as we received it in our GET request and thus, extend the form builder with a
data transformer. Fortunately, the FOSRestBundle comes with an
``EntityToIdObjectTransformer``, which can be applied to any form builder:
.. code-block:: php
$personTransformer = new EntityToIdObjectTransformer($this->om, "AcmeDemoBundle:Person");
$builder
->add('name', 'text')
...
->add($builder->create('person', 'text')->addModelTransformer($personTransformer))
This way, the data structure remains untouched and the person can be assigned to
the task without any client modifications.
Configuration
-------------
The ``formats`` and ``templating_formats`` settings determine which formats are
respectively supported by the serializer and by the template layer. In other
words any format listed in ``templating_formats`` will require a template for
rendering using the ``templating`` service, while any format listed in
``formats`` will use the serializer for rendering. For both settings a
value of ``false`` means that the given format is disabled.
When using ``RouteRedirectView::create()`` the default behavior of forcing a
redirect to the route for html is enabled, but needs to be enabled for other
formats if needed.
Finally the HTTP response status code for failed validation defaults to
``400``. Note when changing the default you can use name constants of
``Symfony\Component\HttpFoundation\Response`` class or an integer status code.
You can also set the default templating engine to something different than the
default of ``twig``:
.. code-block:: yaml
# app/config/config.yml
fos_rest:
view:
formats:
rss: true
xml: false
templating_formats:
html: true
force_redirects:
html: true
failed_validation: HTTP_BAD_REQUEST
default_engine: twig
See `this example configuration`_ for more details.
Custom handler
--------------
While many things should be possible via the serializer in some cases
it might not be enough. For example you might need some custom logic to be
executed in the ``ViewHandler``. For these cases one might want to register a
custom handler for a specific format. The custom handler can either be
registered by defining a custom service, via a compiler pass or it can even be
registered from inside the controller action.
The callable will receive 3 parameters:
* the instance of the ``ViewHandler``
* the instance of the ``View``
* the instance of the ``Request``
Note there are several public methods on the ``ViewHandler`` which can be helpful:
* ``isFormatTemplating()``
* ``createResponse()``
* ``createRedirectResponse()``
* ``renderTemplate()``
There is an example inside LiipHelloBundle to show how to register a custom handler:
https://github.com/liip/LiipHelloBundle/blob/master/View/RSSViewHandler.php
https://github.com/liip/LiipHelloBundle/blob/master/Resources/config/config.yml
There is another example in ``Resources\doc\examples``:
https://github.com/FriendsOfSymfony/FOSRestBundle/blob/master/Resources/doc/examples/RssHandler.php
Here is an example using a closure registered inside a Controller action:
.. code-block:: php
get('fos_rest.view_handler');
if (!$handler->isFormatTemplating($view->getFormat())) {
$templatingHandler = function ($handler, $view, $request) {
// if a template is set, render it using the 'params'
// and place the content into the data
if ($view->getTemplate()) {
$data = $view->getData();
if (empty($data['params'])) {
$params = array();
} else {
$params = $data['params'];
unset($data['params']);
}
$view->setData($params);
$data['html'] = $handler->renderTemplate($view, 'html');
$view->setData($data);
}
return $handler->createResponse($view, $request, $format);
};
$handler->registerHandler($view->getFormat(), $templatingHandler);
}
return $handler->handle($view);
}
}
JSONP custom handler
~~~~~~~~~~~~~~~~~~~~
To enable the common use case of creating JSONP responses this Bundle provides an
easy solution to handle a custom handler for this use case. Enabling this setting
also automatically uses the mime type listener (see the next chapter) to register
a mime type for JSONP.
Simply add the following to your configuration
.. code-block:: yaml
# app/config/config.yml
fos_rest:
view:
jsonp_handler: ~
It is also possible to customize both the name of the GET parameter with the
callback, as well as the filter pattern that validates if the provided callback
is valid or not.
.. code-block:: yaml
# app/config/config.yml
fos_rest:
view:
jsonp_handler:
callback_param: mycallback
Finally the filter can also be disabled by setting it to false.
.. code-block:: yaml
# app/config/config.yml
fos_rest:
view:
jsonp_handler:
callback_param: false
When working with JSONP, be aware of `CVE-2014-4671`_ (full explanation can be
found here: `Abusing JSONP with Rosetta Flash`_. You SHOULD use `NelmioSecurityBundle`_
and `disable the content type sniffing for script resources`_.
CSRF validation
~~~~~~~~~~~~~~~
When building a single application that should handle forms both via HTML forms
as well as via a REST API, one runs into a problem with CSRF token validation.
In most cases it is necessary to enable them for HTML forms, but it makes no
sense to use them for a REST API. For this reason there is a form extension to
disable CSRF validation for users with a specific role. This of course requires
that REST API users authenticate themselves and get a special role assigned.
.. code-block:: yaml
fos_rest:
disable_csrf_role: ROLE_API
That was it!
.. _`Symfony Serializer Component`: http://symfony.com/doc/current/components/serializer.html
.. _`serializer`: https://github.com/schmittjoh/serializer
.. _`JMSSerializerBundle`: https://github.com/schmittjoh/JMSSerializerBundle
.. _`this example code`: https://github.com/liip/LiipHelloBundle/blob/master/Controller/HelloController.php
.. _`this example configuration`: https://github.com/liip-forks/symfony-standard/blob/techtalk/app/config/config.yml
.. _`CVE-2014-4671`: http://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2014-4671
.. _`Abusing JSONP with Rosetta Flash`: http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
.. _`NelmioSecurityBundle`: https://github.com/nelmio/NelmioSecurityBundle
.. _`disable the content type sniffing for script resources`: https://github.com/nelmio/NelmioSecurityBundle#content-type-sniffing
Resources/doc/3-listener-support.rst 0000666 00000022442 13052362131 0013520 0 ustar 00 Step 3: Listener support
========================
`Listeners`_ are a way to hook into the request handling. This Bundle provides
various events from decoding the request content in the request (body listener),
determining the correct response format (format listener), reading parameters
from the request (parameter fetcher listener), to formatting the response either
with a template engine like twig or to e.g. xml or json using a serializer (view
response listener) as well as automatically setting the accepted HTTP methods
in the response (accept listener).
With this in mind we now turn to explain each one of them.
All listeners except the ``mime_type`` listener are disabled by default. You
can enable one or more of these listeners. For example, below you can see how
to enable a few additional listeners:
.. code-block:: yaml
# app/config/config.yml
fos_rest:
param_fetcher_listener: true
body_listener: true
format_listener:
enabled: true
rules:
- { path: '^/', priorities: ['json', 'xml'], fallback_format: 'html' }
versioning: true
view:
view_response_listener: 'force'
It is possible to replace the service used for each of the listener if needed.
In this case, the Bundle listener will still be configured, however it will
not be registered in the kernel. The custom service listener will however not
be registered in the kernel, so it is up to the user to register it for the
appropriate event:
.. code-block:: yaml
# app/config/config.yml
fos_rest:
body_listener:
service: my_body_listener
my_body_listener:
class: Acme\BodyListener
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: 10 }
arguments: ['@fos_rest.decoder_provider', '%fos_rest.throw_exception_on_unsupported_content_type%']
calls:
- [setDefaultFormat, ['%fos_rest.body_default_format%']]
View Response Listener
----------------------
The view response listener makes it possible to simply return a ``View``
instance from action controllers. The final output will then automatically be
processed via the listener by the ``fos_rest.view_handler`` service.
This requires adding the `SensioFrameworkExtraBundle`_ to your vendors.
For details see :doc:`View Response Listener `.
Body Listener
-------------
The Request body listener makes it possible to decode the contents of a request
in order to populate the "request" parameter bag of the Request. This, for
example, allows to receive data that normally would be sent via POST as
``application/x-www-form-urlencoded`` in a different format (for example
``application/json``) in a PUT. Please note that this listener is supposed to
allow you to decode and normalize data. If you want to deserialize data,
meaning getting an object of your choice, you will be better off using the
request body converter listener, documented below.
For details see :doc:`Body Listener `.
Request Body Converter Listener
-------------------------------
`ParamConverters`_ are a way to populate objects and inject them as controller
method arguments. The Request body converter makes it possible to deserialize
the request body into an object.
This converter requires that you have installed `SensioFrameworkExtraBundle`_
and have the converters enabled.
For details see :doc:`Request Body Converter Listener `.
Format Listener
---------------
The Request format listener attempts to determine the best format for the
request based on the HTTP 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).
For details see :doc:`Format Listener `.
Versioning
----------
This listener attemps to determine the current api version from different parameters of the ``Request``:
* the uri ``/{version}/users``
* a query parameter ``/users?version=v1``
* an ``Accept`` header ``Accept: appication/json; version=1.0``
* a custom header ``X-Accept-Version: v1``
For details see :doc:`Versioning `.
Mime Type Listener
------------------
This listener allows registering additional mime types in the ``Request``
class. It works similar to the `mime type listener`_ available in Symfony
since 2.5.
.. code-block:: yaml
# app/config/config.yml
fos_rest:
view:
mime_types: {'jsonp': ['application/javascript+jsonp']}
Param Fetcher Listener
----------------------
The param fetcher listener simply sets the ParamFetcher instance as a request attribute
configured for the matched controller so that the user does not need to do this manually.
For details see :doc:`Param Fetcher Listener `.
Allowed Http Methods Listener
-----------------------------
This listener adds the ``Allow`` HTTP header to each request appending all
allowed methods for a given resource.
Let's say we have the following routes:
.. code-block:: text
api_get_users
api_post_users
api_get_user
A ``GET`` request to ``api_get_users`` will respond with:
.. code-block:: text
HTTP/1.0 200 OK
Date: Sat, 16 Jun 2012 15:17:22 GMT
Server: Apache/2.2.22 (Ubuntu)
Allow: GET, POST
You need to enable this listener as follows, as it is disabled by default:
.. code-block:: yaml
fos_rest:
allowed_methods_listener: true
Security Exception Listener
---------------------------
By default it is the responsibility of firewall access points to deal with
AccessDeniedExceptions. For example the ``form`` entry point will redirect to
the login page. However, for a RESTful application proper response HTTP status
codes should be provided. This listener is triggered before the normal exception
listener and firewall entry points and forces returning either a 403 or 401
status code for any of the formats configured.
It will return 401 for
``Symfony\Component\Security\Core\Exception\AuthenticationException`` or 403 for
``Symfony\Component\Security\Core\Exception\AccessDeniedException``.
As a 401-response requires an authentication-challenge, you can set one using
the configuration ``unauthorized_challenge`` or leave it blank if you don't want
to send a challenge in the ``WWW-Authenticate`` header to the client.
If you want to use an advanced value in this header, it's worth looking at this:
`Test Cases for HTTP Test Cases for the HTTP WWW-Authenticate header field`_.
You need to enable this listener as follows, as it is disabled by default:
.. code-block:: yaml
fos_rest:
unauthorized_challenge: "Basic realm=\"Restricted Area\""
access_denied_listener:
# all requests using the 'json' format will return a 403 on an access denied violation
json: true
Note: The access_denied_listener doesn't return a response itself and must be coupled with an exception listener returning a response (see the :doc:`FOSRestBundle exception controller <4-exception-controller-support>`. or the `twig exception controller`_).
Zone Listener
=============
As you can see, FOSRestBundle provides multiple event listeners to enable REST-related features.
By default, these listeners will be registered to all requests and may conflict with other parts of your application.
Using the ``zone`` configuration, you can specify where the event listeners will be enabled. The zone configuration
allows to configure multiple zones in which the above listeners will be active. If no zone is configured, it means
that the above listeners will not be limited. If at least one zone is configured then the above listeners will
be skipped for all requests that do not match at least one zone. For a single zone config entry can contain matching
rules on the request ``path``, ``host``, ``methods`` and ``ip``.
.. code-block:: yaml
# app/config/config.yml
fos_rest:
zone:
- { path: ^/api/* }
Priorities
----------
========================== ===================== ========
Listener Event Priority
========================== ===================== ========
``ZoneMatcherListener`` ``kernel.request`` 248
``MimeTypeListener`` ``kernel.request`` 200
``FormatListener`` ``kernel.request`` 34
``VersionListener`` ``kernel.request`` 33
``BodyListener`` ``kernel.request`` 10
``ParamFetcherListener`` ``kernel.controller`` 5
``ViewResponseListener`` ``kernel.controller`` -10
``ViewResponseListener`` ``kernel.view`` 100
``AllowedMethodsListener`` ``kernel.response`` 0
========================== ===================== ========
That was it!
.. _`Listeners`: http://symfony.com/doc/master/cookbook/service_container/event_listener.html
.. _`SensioFrameworkExtraBundle`: http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index.html
.. _`ParamConverters`: http://symfony.com/doc/master/bundles/SensioFrameworkExtraBundle/annotations/converters.html
.. _`mime type listener`: http://symfony.com/doc/current/cookbook/request/mime_type.html
.. _`Test Cases for HTTP Test Cases for the HTTP WWW-Authenticate header field`: http://greenbytes.de/tech/tc/httpauth/
.. _`twig exception controller`: https://symfony.com/doc/current/cookbook/controller/error_pages.html
Resources/doc/4-exception-controller-support.rst 0000666 00000006342 13052362131 0016054 0 ustar 00 Step 4: ExceptionController support
===================================
When implementing an API it is also necessary to handle exceptions in a RESTful
way, while ensuring that no security sensitive information leaks out. This
bundle provides an extra controller for that job. Using this custom
ExceptionController it is possible to leverage the View layer when building
responses for uncaught Exceptions.
The ExceptionController can be enabled via the FOSRestBundle
configuration:
.. code-block:: yaml
# app/config/config.yml
fos_rest:
exception:
enabled: true
exception_controller: 'Acme\DemoBundle\Controller\ExceptionController::showAction'
.. note::
The FOSRestBundle ExceptionController is executed before the one of the TwigBundle.
.. note::
FOSRestBundle defines two services for exception rendering, by default it
configures ``fos_rest.exception.controller`` which only supports rendering
via a serializer. In case no explicit controller is configured by the user
and TwigBundle is detected it will automatically configure
``fos_rest.exception.twig_controller`` which additionally also supports
rendering via Twig.
To map Exception classes to HTTP response status codes an *exception map* may
be configured, where the keys match a fully qualified class name and the values
are either an integer HTTP response status code or a string matching a class
constant of the ``Symfony\Component\HttpFoundation\Response`` class:
.. code-block:: yaml
# app/config/config.yml
fos_rest:
exception:
codes:
'Symfony\Component\Routing\Exception\ResourceNotFoundException': 404
'Doctrine\ORM\OptimisticLockException': HTTP_CONFLICT
messages:
'Acme\HelloBundle\Exception\MyExceptionWithASafeMessage': true
If you want to display the message from the exception in the content of the
response, add the exception to the messages map as well. If not only the status
code will be returned.
If you know what status code you want to return you do not have to add a
mapping, you can do this in your controller:
.. code-block:: php
validate($slug)) {
throw new HttpException(400, "New comment is not valid.");
}
}
}
In order to make the serialization format of exceptions customizable it is possible to
use serializer normalizers.
See `how to create handlers`_ for the JMS serializer and `how to create normalizers`_ for the Symfony serializer.
That was it!
.. note::
If you are receiving a 500 error where you would expect a different response, the issue
is likely caused by an exception inside the ExceptionController. For example a template
is not found or the serializer failed. You should take a look at the logs of your app to see if an uncaught exception has been logged.
.. _`how to create handlers`: http://jmsyst.com/libs/serializer/master/handlers
.. _`how to create normalizers`: http://thomas.jarrand.fr/blog/serialization/
Resources/doc/5-automatic-route-generation_single-restful-controller.rst 0000666 00000036103 13052362131 0022641 0 ustar 00 Routing
=======
The RestBundle provides custom route loaders to help in defining REST friendly
routes as well as reducing the manual work of configuring routes and the given
requirements (like making sure that only GET may be used in certain routes
etc.).
You may specify a ``default_format`` that the routing loader will use for the
``_format`` parameter if none is specified.
.. code-block:: yaml
# app/config/config.yml
fos_rest:
routing_loader:
default_format: json
Many of the features explained below are used in the following example code:
https://github.com/liip/LiipHelloBundle/blob/master/Controller/RestController.php
Single RESTful controller routes
--------------------------------
In this section we are looking at controllers for resources without sub-resources.
Handling of sub-resources requires some additional considerations which
are explained in the next section.
.. code-block:: yaml
# app/config/routing.yml
users:
type: rest
host: m.example.com
resource: Acme\HelloBundle\Controller\UsersController
This will tell Symfony to automatically generate proper REST routes from your
``UsersController`` action names. Notice ``type: rest`` option. It's required so
that the RestBundle can find which routes are supported.
Define resource actions
-----------------------
.. code-block:: php
; rel="kind_of_relation"``
* **unlink** - this action accepts *UNLINK* requests to the url ``/resources/{id}``
and is supposed to return nothing but a status code indicating that the specified
resources were unlinked. It is used to declare that some resources are not
related anymore. When calling a UNLINK url you must provide in your header at
least one link header formatted as follow :
``; rel="kind_of_relation"``
Important note about **link** and **unlink**: The implementation of the request
listener extracting the resources as entities is not provided by this bundle. A
good implementation can be found here: `REST APIs with Symfony2: The Right Way`_
It also contains some examples on how to use it. **link** and **unlink** were
obsoleted by RFC 2616, RFC 5988 aims to define it in a more clear way. Using
these methods is not risky, but remains unclear (cf. issues 323 and 325).
Conventional Actions
--------------------
HATEOAS, or Hypermedia as the Engine of Application State, is an aspect of REST
which allows clients to interact with the REST service with hypertext - most
commonly through an HTML page. There are 3 Conventional Action routings that are
supported by this bundle:
* **new** - A hypermedia representation that acts as the engine to *POST*.
Typically this is a form that allows the client to *POST* a new resource.
Shown as ``UsersController::newUsersAction()`` above.
* **edit** - A hypermedia representation that acts as the engine to *PUT*.
Typically this is a form that allows the client to *PUT*, or update, an
existing resource. Shown as ``UsersController::editUserAction()`` above.
* **remove** - A hypermedia representation that acts as the engine to *DELETE*.
Typically this is a form that allows the client to *DELETE* an existing resource.
Commonly a confirmation form. Shown as ``UsersController::removeUserAction()``
above.
Custom PATCH Actions
--------------------
All actions that do not match the ones listed in the sections above will
register as a *PATCH* action. In the controller shown above, these actions are
``UsersController::lockUserAction()``, ``UsersController::banUserAction()`` and
``UsersController::voteUserCommentAction()``. You could just as easily create a
method called ``UsersController::promoteUserAction()`` which would take a
*PATCH* request to the url ``/users/{slug}/promote``. This allows for easy
updating of aspects of a resource, without having to deal with the resource as a
whole at the standard *PATCH* or *PUT* endpoint.
Sub-Resource Actions
--------------------
Of course it's possible and common to have sub or child resources. They are
easily defined within the same controller by following the naming convention
``ResourceController::actionResourceSubResource()`` - as seen in the example
above with ``UsersController::getUserCommentsAction()``. This is a good strategy
to follow when the child resource needs the parent resource's ID in order to
look up itself.
Optional {_format} in route
---------------------------
By default, routes are generated with ``{_format}`` string. If you want to get clean
urls (``/orders`` instead ``/orders.{_format}``) then all you have to do is add
some configuration:
.. code-block:: yaml
# app/config/config.yml
fos_rest:
routing_loader:
include_format: false
The ``{_format}`` route requirement is automatically positioned using the available
listeners. So by default, the requirement will be ``{json|xml|html}``. If you want
to limit or add a custom format, you can do so by overriding it with the
``@Route`` annotation (or another one extending it, like ``@Get``, ``@Post``, ...):
.. code-block:: php
Notice ``parent: users`` option in the second case. This option specifies that
the comments resource is child of the users resource.
It is also necessary to add ``type: rest`` to the ``routing.yml`` file:
.. code-block:: yaml
# app/config/routing.yml
acme_hello:
type: rest
host: hostname.example.com
resource: "@AcmeHelloBundle/Resources/config/users_routes.yml"
In this case, your ``UsersController`` MUST always have a single resource
``get...`` action:
.. code-block:: php
=2.4`` (or the full framework) you have access to
the expression language component and can add conditions to your routing
configuration with annotations (see `Routing Conditions`_).
Example syntax:
.. code-block:: php
use FOS\RestBundle\Controller\Annotations\Route;
/**
* @Route("", condition="context.getMethod() in ['GET', 'HEAD'] and request.headers.get('User-Agent') matches '/firefox/i'")
*/
.. _`@Route Symfony annotation`: http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/routing.html
.. _`Routing Conditions`: http://symfony.com/doc/current/book/routing.html#book-routing-conditions
Resources/doc/body_listener.rst 0000666 00000007613 13052362131 0012666 0 ustar 00 Body Listener
=============
The Request body listener makes it possible to decode the contents of a request
in order to populate the "request" parameter bag of the Request. This for
example allows to receive data that normally would be sent via POST as
``application/x-www-form-urlencoded`` in a different format (for example
``application/json``) in a PUT.
Decoders
~~~~~~~~
You can add a decoder for a custom format. You can also replace the default
decoder services provided by the bundle for the ``json`` and ``xml`` formats.
Below you can see how to override the decoder for the json format (the xml
decoder is explicitly kept to its default service):
.. code-block:: yaml
# app/config/config.yml
fos_rest:
body_listener:
decoders:
json: acme.decoder.json
xml: fos_rest.decoder.xml
Your custom decoder service must use a class that implements the
``FOS\RestBundle\Decoder\DecoderInterface``.
If you want to be able to use a checkbox within a form and have true and false
values (without any issue) you have to use: ``fos_rest.decoder.jsontoform``
(available since FosRestBundle 0.8.0)
If the listener receives content that it tries to decode but the decode fails
then a BadRequestHttpException will be thrown with the message: ``'Invalid ' .
$format . ' message received'``. When combined with the :doc:`exception controller
support <4-exception-controller-support>` this means your API will provide
useful error messages to your API users if they are making invalid requests.
Array Normalizer
~~~~~~~~~~~~~~~~
Array normalizers allow to transform the data after it has been decoded in order
to facilitate its processing.
For example, you may want your API's clients to be able to send requests with
underscored keys but if you use a decoder without a normalizer, you will receive
the data as it is and it can lead to incorrect mapping if you submit the request
directly to a form. If you wish the body listener to transform underscored keys
to camel cased ones, you can use the ``camel_keys`` array normalizer:
.. code-block:: yaml
# app/config/config.yml
fos_rest:
body_listener:
array_normalizer: fos_rest.normalizer.camel_keys
.. note::
If you want to ignore leading underscores, for example in ``_username`` you can
instead use the ``fos_rest.normalizer.camel_keys_with_leading_underscore`` service.
Sometimes an array contains a key, which once normalized, will override an
existing array key. For example ``foo_bar`` and ``foo_Bar`` will both lead to
``fooBar``. If the normalizer receives this data, the listener will throw a
BadRequestHttpException with the message ``The key "foo_Bar" is invalid as it
will override the existing key "fooBar"``.
.. note::
If you use the ``camel_keys`` normalizer, you must be careful when choosing
your form name.
You can also create your own array normalizer by implementing the
``FOS\RestBundle\Normalizer\ArrayNormalizerInterface``.
.. code-block:: yaml
# app/config/config.yml
fos_rest:
body_listener:
array_normalizer: acme.normalizer.custom
By default, the array normalizer is only applied to requests with a decodable format.
If you want form data to be normalized, you can use the ``forms`` flag:
.. code-block:: yaml
# app/config/config.yml
fos_rest:
body_listener:
array_normalizer:
service: fos_rest.normalizer.camel_keys
forms: true
Using the ArrayNormalizer with login forms
------------------------------------------
If you use the default configuration for the csrf token fieldname (``_csrf_token``)
the Array normalizer will mangle the field name. To make it work, use a name that
is camelcased, like this:
.. code-block:: yaml
security:
firewalls:
admin:
# ...
form_login:
# ...
csrf_parameter: _csrfToken
Resources/doc/configuration-reference.rst 0000666 00000010237 13052362131 0014623 0 ustar 00 Full default configuration
==========================
.. code-block:: yaml
# Default configuration for extension with alias: "fos_rest"
fos_rest:
disable_csrf_role: null
access_denied_listener:
enabled: false
service: null
formats:
# Prototype
name: ~
unauthorized_challenge: null
param_fetcher_listener:
enabled: false
force: false
service: null
cache_dir: '%kernel.cache_dir%/fos_rest'
allowed_methods_listener:
enabled: false
service: null
routing_loader:
default_format: null
include_format: true
body_converter:
enabled: false
validate: false
validation_errors_argument: validationErrors
service:
router: router
templating: templating
serializer: null
view_handler: fos_rest.view_handler.default
inflector: fos_rest.inflector.doctrine
validator: validator
serializer:
version: null
groups: []
serialize_null: false
view:
default_engine: twig
force_redirects:
# Prototype
name: ~
mime_types:
enabled: false
service: null
formats:
# Prototype
name: ~
formats:
# Prototype
name: ~
templating_formats:
# Prototype
name: ~
view_response_listener:
enabled: false
force: false
service: null
failed_validation: 400
empty_content: 204
serialize_null: false
jsonp_handler:
callback_param: callback
mime_type: application/javascript+jsonp
exception:
enabled: false
exception_controller: null
codes:
# Prototype
name: ~
messages:
# Prototype
name: ~
body_listener:
enabled: true
service: null
default_format: null
throw_exception_on_unsupported_content_type: false
decoders:
# Prototype
name: ~
array_normalizer:
service: null
forms: false
format_listener:
enabled: false
service: null
rules:
# URL path info
path: null
# URL host name
host: null
# Method for URL
methods: null
stop: false
prefer_extension: true
fallback_format: html
attributes: []
priorities: []
versioning:
enabled: false
default_version: ~
resolvers:
query:
enabled: true
parameter_name: version
custom_header:
enabled: true
header_name: X-Accept-Version
media_type:
enabled: true
regex: /(v|version)=(?P[0-9\.]+)/
guessing_order:
- query
- custom_header
- media_type
Resources/doc/empty-content-status-code.rst 0000666 00000001471 13052362131 0015057 0 ustar 00 Status code when responding with no content
===========================================
In some use cases the api should not send any content, especially when deleting (*DELETE*) or updating (*PUT* or *PATCH*) a resource.
By default, ``FOSRestBundle`` will send a *204* status if the response is empty.
If you want to use another status code for empty responses, you can update your configuration file:
.. code-block:: yaml
fos_rest:
view:
empty_content: 204
.. versionadded:: 2.0
Until FOSRestBundle 2.0 this code will be used even if another code is configured manually inside the view object!
If you don't want to use the default empty content status for a specific empty ``Response``, you just
have to set a status code manually thanks to the ``@View()`` annotation or the ``View`` class.
Resources/doc/examples/RssHandler.php 0000666 00000006532 13052362131 0013665 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Examples;
use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandler;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* This is an example RSS ViewHandler.
* It also shows how to handle exceptions within the ViewHandler so that the
* client can get a decent response.
*
* Please note that you will need to install the Zend library to use this
* handler.
*
* Configuration:
*
* services:
* my.rss_handler:
* class: FOS\RestBundle\Examples\RssHandler
* arguments:
* logger: "@?logger"
*
* my.view_handler:
* parent: fos_rest.view_handler.default
* calls:
* - ['registerHandler', [ 'rss', ["@my.rss_handler", 'createResponse'] ] ]
*
* fos_rest:
* service:
* view_handler: my.view_handler
*
* @author Tarjei Huse (tarjei - at scanmine.com)
*/
class RssHandler
{
private $logger;
public function __construct(LoggerInterface $logger = null)
{
$this->logger = $logger;
}
/**
* Converts the viewdata to a RSS feed. Modify to suit your datastructure.
*
* @return Response
*/
public function createResponse(ViewHandler $handler, View $view, Request $request)
{
try {
$content = $this->createFeed($view->getData());
$code = Response::HTTP_OK;
} catch (\Exception $e) {
if ($this->logger) {
$this->logger->error($e);
}
$content = sprintf('%s:
%s
', $e->getMessage(), $e->getTraceAsString());
$code = Response::HTTP_BAD_REQUEST;
}
return new Response($content, $code, $view->getHeaders());
}
/**
* @param $data array
* @param format string, either rss or atom
*/
protected function createFeed($data, $format = 'rss')
{
$feed = new \Zend_Feed_Writer_Feed();
$feed->setTitle($data['title']);
$feed->setLink($data['link']);
$feed->setFeedLink($data['link'], 'rss');
$feed->addAuthor([
'name' => 'ZeroCMS',
'email' => 'email!',
]);
$feed->setDateModified(time());
$feed->setDescription('RSS feed from query');
// Add one or more entries. Note that entries must be manually added once created.
foreach ($data['documents'] as $document) {
$entry = $feed->createEntry();
$entry->setTitle($document['title']);
$entry->setLink($document['url']);
$entry->addAuthor([
'name' => $document['author'],
//'email' => '',
//'uri' => '',
]);
$entry->setDateModified($document['dateUpdated']->getTimestamp());
$entry->setDateCreated($document['dateCreated']->getTimestamp());
if (isset($document['summary'])) {
$entry->setDescription($document['summary']);
}
$entry->setContent($document['body']);
$feed->addEntry($entry);
}
return $feed->export($format);
}
}
Resources/doc/examples/chaplin_backbone.md 0000666 00000002004 13052362131 0014661 0 ustar 00 # Sample FOSRest application with Chaplin.js and Backbone.js
[DunglasTodoMVCBundle](https://github.com/dunglas/DunglasTodoMVCBundle) is a Symfony implementation of the popular [TodoMVC](http://todomvc.com/) project.
This example app includes:
* A REST API built with [FOSRestBundle](https://github.com/FriendsOfSymfony/FOSRestBundle) using a [body listener](https://github.com/FriendsOfSymfony/FOSRestBundle/blob/master/Resources/doc/3-listener-support.md#body-listener), a [format listener](https://github.com/FriendsOfSymfony/FOSRestBundle/blob/master/Resources/doc/3-listener-support.md#format-listener) and the `fos_rest.decoder.jsontoform` decoder
* JSON serialization of Doctrine entities through [JMSSerializerBundle](https://github.com/schmittjoh/JMSSerializerBundle)
* CSRF protection through [DunglasAngularCsrfBundle](https://github.com/dunglas/DunglasAngularCsrfBundle)
* A client built in [CoffeeScript](http://coffeescript.org/) with [Chaplin.js](http://chaplinjs.org/) and [Backbone.js](http://backbonejs.org/)
Resources/doc/format_listener.rst 0000666 00000011652 13052362131 0013217 0 ustar 00 Format Listener
===============
The Request format 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).
The ``priorities`` define the order of media types as the application
prefers. Note that if a format is provided instead of a media type, the
format is converted into a list of media types matching the format.
The algorithm iteratively examines the provided Accept header first
looking at all the options with the highest ``q``. The first priority that
matches is returned. If none match the next lowest set of Accept headers with
equal ``q`` is examined and so on until there are no more Accept headers to
check. In this case ``fallback_format`` is used.
Note that if ``_format`` is matched inside the route, then a virtual Accept
header setting is added with a ``q`` setting one lower than the lowest Accept
header, meaning that format is checked for a match in the priorities last. If
``prefer_extension`` is set to ``true`` then the virtual Accept header will be
one higher than the highest ``q`` causing the extension to be checked first.
Setting ``priorities`` to a non-empty array enables Accept header negotiations.
.. code-block:: yaml
# app/config/config.yml
fos_rest:
format_listener:
enabled: true
rules:
# setting fallback_format to json means that instead of considering the next rule in case of a priority mismatch, json will be used
- { path: '^/', host: 'api.%domain%', priorities: ['json', 'xml'], fallback_format: json, prefer_extension: false }
# setting fallback_format to false means that instead of considering the next rule in case of a priority mismatch, a 406 will be caused
- { path: '^/image', priorities: ['jpeg', 'gif'], fallback_format: false, prefer_extension: true }
# setting fallback_format to null means that in case of a priority mismatch the next rule will be considered
- { path: '^/admin', methods: ['GET', 'POST'], priorities: ['xml', 'html'], fallback_format: ~, prefer_extension: false }
# you can specifically target the exception controller
- { path: '^/api', priorities: ['xml', 'json'], fallback_format: xml, attributes: { _controller: FOS\RestBundle\Controller\ExceptionController }, prefer_extension: false }
# setting a priority to */* basically means any format will be matched
- { path: '^/', priorities: ['text/html', '*/*'], fallback_format: html, prefer_extension: true }
For example using the above configuration and the following Accept header:
.. code-block:: text
text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8,application/json
And the following route:
.. code-block:: yaml
hello:
path: /foo.{_format}
defaults: { _controller: foo.controller:indexAction, _format: ~ }
When calling:
* ``/foo.json`` will lead to setting the request format to ``json``
* ``/foo`` will lead to setting the request format to ``html``
Furthermore the listener sets a ``media_type`` attribute on the request in
case the listener is configured with a ``MediaTypeNegotiatorInterface`` instance,
which is the case by default, with the matched media type.
.. code-block:: php
// f.e. text/html or application/vnd.custom_something+json etc.
$mediaType = $request->attributes->get('media_type');
The ``priorities`` should be configured carefully, especially when the
controller actions for specific routes only handle necessary security checks
for specific formats. In such cases it might make sense to hard code the format
in the controller action.
.. code-block:: php
public function getAction(Request $request)
{
$view = new View();
// hard code the output format of the controller action
$view->setFormat('html');
// ...
}
Note that if you use custom mime types, they need to be added using the :doc:`Mime Type Listener <3-listener-support>`.
Disabling the Format Listener via Rules
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Often when integrating this Bundle with existing applications, it might be
useful to disable the format listener for some routes. In this case it is
possible to define a rule that will stop the format listener from determining a
format by setting ``stop`` to ``true`` as a rule option. Any rule containing
this setting and any rule following will not be considered and the Request
format will remain unchanged.
.. code-block:: yaml
# app/config/config.yml
fos_rest:
format_listener:
enabled: true
rules:
- { path: '^/api', priorities: ['json', 'xml'], fallback_format: json, prefer_extension: false }
- { path: '^/', stop: true } # Available for version >= 1.5
Resources/doc/index.rst 0000666 00000005344 13052362131 0011132 0 ustar 00 Getting Started With FOSRestBundle
==================================
.. toctree::
:hidden:
1-setting_up_the_bundle
2-the-view-layer
empty-content-status-code
3-listener-support
view_response_listener
body_listener
request_body_converter_listener
format_listener
versioning
param_fetcher_listener
4-exception-controller-support
5-automatic-route-generation_single-restful-controller
6-automatic-route-generation_multiple-restful-controllers
7-manual-route-definition
annotations-reference
configuration-reference
Installation
------------
Installation is a quick (I promise!) one-step process:
1. :doc:`Setting up the bundle <1-setting_up_the_bundle>`
Bundle usage
------------
Before you start using the bundle it is advised you run a quick look over the
six sections listed below. This bundle contains many features that are loosely
coupled so you may or may not need to use all of them. This bundle is just a
tool to help you in the job of creating a REST API with Symfony.
FOSRestBundle provides several tools to assist in building REST applications:
- :doc:`The view layer <2-the-view-layer>`
- :doc:`Listener support <3-listener-support>`
- :doc:`ExceptionController support <4-exception-controller-support>`
- :doc:`Automatic route generation: single RESTful controller <5-automatic-route-generation_single-restful-controller>` (for simple resources)
- :doc:`Automatic route generation: multiple RESTful controllers <6-automatic-route-generation_multiple-restful-controllers>` (for resources with child/subresources)
- :doc:`Manual definition of routes <7-manual-route-definition>`
Config reference
----------------
- :doc:`Configuration reference ` for a reference on
the available configuration options
- :doc:`Annotations reference ` for a reference on
the available configurations through annotations
Example applications
--------------------
The following bundles/applications use the FOSRestBundle and can be used as a
guideline:
- The `LiipHelloBundle`_ provides several examples for the RestBundle.
- There is also `a fork of the Symfony2 Standard Edition`_ that is configured to
show the LiipHelloBundle examples.
- The `FOSCommentBundle`_ uses FOSRestBundle for its API.
- The `Symfony2 Rest Edition`_ provides a complete example of how to build a
controller that works for both HTML as well as JSON/XML.
.. _`LiipHelloBundle`: https://github.com/liip/LiipHelloBundle
.. _`a fork of the Symfony2 Standard Edition`: https://github.com/liip-forks/symfony-standard/tree/techtalk
.. _`FOSCommentBundle`: https://github.com/FriendsOfSymfony/FOSCommentBundle
.. _`Symfony2 Rest Edition`: https://github.com/gimler/symfony-rest-edition
Resources/doc/param_fetcher_listener.rst 0000666 00000021026 13052362131 0014523 0 ustar 00 Param Fetcher Listener
======================
The param fetcher listener simply sets the ParamFetcher instance as a request attribute
configured for the matched controller so that the user does not need to do this manually.
.. code-block:: yaml
# app/config/config.yml
fos_rest:
param_fetcher_listener: true
.. code-block:: php
name = "dynamic_request";
$dynamicRequestParam->requirements = "\d+";
$paramFetcher->addParam($dynamicRequestParam);
$dynamicQueryParam = new QueryParam();
$dynamicQueryParam->name = "dynamic_query";
$dynamicQueryParam->requirements="[a-z]+";
$paramFetcher->addParam($dynamicQueryParam);
$page = $paramFetcher->get('page');
$articles = array('bim', 'bam', 'bingo');
return array('articles' => $articles, 'page' => $page);
}
.. note::
There is also ``$paramFetcher->all()`` to fetch all configured query
parameters at once. And also both ``$paramFetcher->get()`` and
``$paramFetcher->all()`` support and optional ``$strict`` parameter to throw
a ``\RuntimeException`` on a validation error.
.. note::
The ParamFetcher requirements feature requires the symfony/validator
component.
Optionally the listener can also already set all configured query parameters as
request attributes
.. code-block:: yaml
# app/config/config.yml
fos_rest:
param_fetcher_listener: force
.. code-block:: php
$articles, 'page' => $page);
}
Container parameters can be used in requirements and default field.
.. note::
The percent sign (%) in ``requirements`` and ``default`` field, must be
escaped with another percent sign
.. code-block:: php
0) {
// Handle validation errors
}
// ...
}
You can configure the validation groups used by the validator
via the ``validator`` option:
.. code-block:: php
/**
* @ParamConverter("post", converter="fos_rest.request_body", options={"validator"={"groups"={"foo", "bar"}}})
*/
public function putPostAction(Post $post, ConstraintViolationListInterface $validationErrors)
{
if (count($validationErrors) > 0) {
// Handle validation errors
}
// ...
}
.. _`ParamConverters`: http://symfony.com/doc/master/bundles/SensioFrameworkExtraBundle/annotations/converters.html
.. _`SensioFrameworkExtraBundle`: http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index.html
Resources/doc/versioning.rst 0000666 00000012200 13052362131 0012173 0 ustar 00 API versioning
==============
Think about versioning your API
-------------------------------
If you introduce changes to your current API, your users might not upgrade their applications right away, so versioning is important to prevent existing applications to break when such changes are made.
How to version your API ?
-------------------------
There are several ways, none is standard:
* Use an uri parameter ``/v1/users``
* Use a query parameter ``/users?version=v1``
* Use a custom mime-type with an ``Accept`` header ``Accept: application/json; version=1.0``
* Use a custom header ``X-Accept-Version: v1``
The ``FOSRestBundle`` allows you to use several of them at the same time or to choose one of them.
URI API versioning
------------------
If you want to version your api with the uri, you can simply use the symfony router:
.. code-block:: yaml
# app/config/routing.yml
my_route:
# ...
path: /{version}/foo/route
Note: this will override the ``version`` attribute of the request if you use the ``FOSRestBundle`` versioning.
Configure ``FOSRestBundle`` to use the api versioning
-----------------------------------------------------
You should activate the versioning in your config.yml:
.. code-block:: yaml
# app/config/config.yml
fos_rest:
versioning: true
If you do not want to allow all the methods described above, you should choose which version resolver to enable:
.. code-block:: yaml
#app/config/config.yml
fos_rest:
versioning:
enabled: true
resolvers:
query: true # Query parameter: /users?version=v1
custom_header: true # X-Accept-Version header
media_type: # Accept header
enabled: true
regex: '/(v|version)=(?P[0-9\.]+)/'
You can also choose the guessing order:
.. code-block:: yaml
# app/config/config.yml
fos_rest:
versioning:
enabled: true
guessing_order:
- query
- custom_header
- media_type
The matched version is set as a Request attribute with the name ``version``,
and when using JMS serializer it is also set as an exclusion strategy
automatically in the ``ViewHandler``.
If you want to version by Accept header, you will need to do the following:
#. The format listener must be enabled
See :doc:`Format Listener `
#. The client must pass the requested version in his header like this :
.. code-block:: yaml
Accept:application/json;version=1.0
#. You must configure the possible mime types for all supported versions:
.. code-block:: yaml
fos_rest:
view:
mime_types:
json: ['application/json', 'application/json;version=1.0', 'application/json;version=1.1']
Note: If you have to handle huge versions and mime types, you can simplify the configuration with a php script:
.. code-block:: php
// app/config/fos_rest_mime_types.php
$versions = array(
'1.0',
'1.1',
'2.0',
);
$mimeTypes = array(
'json' => array(
'application/json',
),
'yml' => array(
'application/yaml',
'text/yaml',
),
);
array_walk($mimeTypes, function (&$mimeTypes, $format, $versions) {
$versionMimeTypes = array();
foreach ($mimeTypes as $mimeType) {
foreach ($versions as $version) {
array_push($versionMimeTypes, sprintf('%s;version=%s', $mimeType, $version));
array_push($versionMimeTypes, sprintf('%s;v=%s', $mimeType, $version));
}
}
$mimeTypes = array_merge($mimeTypes, $versionMimeTypes);
}, $versions);
$container->loadFromExtension('fos_rest', array(
'view' => array(
'mime_types' => $mimeTypes,
),
));
And then, import it from your config.yml file:
.. code-block:: yaml
imports:
- { resource: assets_version.php }
Use the ``JMSSerializer`` with the API versioning
-------------------------------------------------
You should have tagged your entities with version information (@Since, @Until ...)
See `this JMS Serializer article`_ for details about versioning objects.
.. _`this JMS Serializer article`: http://jmsyst.com/libs/serializer/master/cookbook/exclusion_strategies#versioning-objects
That's it, it should work now.
How to match a specific version in my routing ?
-----------------------------------------------
You can use conditions on your request to check for the version that was determined:
.. code-block:: yaml
my_route:
# ...
condition: "request.attributes.get('version') == 'v2'"
When using the :doc:`automatic route generation <5-automatic-route-generation_single-restful-controller>`,
you can also use the ``@Version`` annotation to set the above condition automatically on all methods
in the given controller.
.. code-block:: php
use FOS\RestBundle\Controller\Annotations\Version;
/**
* @Version("v2")
*
* or if you support multiple versions in this controller
* @Version({"v1", "v2"})
*/
class MyController
{
}
Resources/doc/view_response_listener.rst 0000666 00000011503 13052362131 0014612 0 ustar 00 View Response listener
======================
The view response listener makes it possible to simply return a ``View``
instance from action controllers. The final output will then automatically be
processed via the listener by the ``fos_rest.view_handler`` service.
This requires adding the `SensioFrameworkExtraBundle`_ to your vendors.
Now inside a controller it's possible to simply return a ``View`` instance.
.. code-block:: php
setData($data);
return $view;
}
}
As this feature is heavily based on the `SensioFrameworkExtraBundle`_, the
example can further be simplified by using the various annotations supported by
that bundle. There is also one additional annotation called ``@View()`` which
extends from the ``@Template()`` annotation.
Note: `SensioFrameworkExtraBundle`_ must be in your kernel if you want to use the annotations and ``sensio_framework_extra.view.annotations`` must be set to true.
The ``@View()`` and ``@Template()`` annotations behave essentially the same with
a minor difference. When ``view_response_listener`` is set to ``true`` instead
of ``force`` and ``@View()`` is not used, then rendering will be delegated to
`SensioFrameworkExtraBundle`_ (you must enable the view annotations in
`SensioFrameworkExtraBundle`_ for that case, use the default configuration).
.. code-block:: php
setData($data)
->setTemplateData($templateData)
;
return $view;
}
If ``@View()`` is used, the template variable name used to render templating
formats can be configured (default ``'data'``):
.. code-block:: php
setVersion('1.0');
$context->addGroup('user');
$view->setSerializationContext($context);
// ...
$view
->setData($data)
->setTemplateData($templateData)
;
return $view;
}
See `this example code`_ for more details.
The ViewResponse listener will automatically populate your view with request
attributes if you do not provide any data when returning a view object. This
behaviour comes from `SensioFrameworkExtraBundle`_ and will automatically add
any variables listed in the ``_template_default_vars`` request attribute when no
data is supplied. In some cases, this is not desirable and can be disabled by
either supplying the data you want or disabling the automatic population of data
with the ``@View`` annotation:
.. code-block:: php
/**
* $user will no longer end up in the View's data.
*
* @View(populateDefaultVars=false)
*/
public function getUserDetails(User $user)
{
}
.. _`SensioFrameworkExtraBundle`: http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index.html
.. _`this example code`: https://github.com/liip/LiipHelloBundle/blob/master/Controller/ExtraController.php
Resources/meta/LICENSE 0000666 00000002105 13052362131 0010447 0 ustar 00 Copyright (c) FriendsOfSymfony
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
Response/AllowedMethodsLoader/AllowedMethodsLoaderInterface.php 0000666 00000001304 13052362131 0020776 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Response\AllowedMethodsLoader;
/**
* AllowedMethodsLoaderInterface.
*
* @author Boris Guéry
*/
interface AllowedMethodsLoaderInterface
{
/**
* Returns the allowed http methods.
*
* array(
* 'some_route' => array('GET', 'POST'),
* 'another_route' => array('DELETE', 'PUT'),
* );
*
* @return array
*/
public function getAllowedMethods();
}
Response/AllowedMethodsLoader/AllowedMethodsRouterLoader.php 0000666 00000005266 13052362131 0020371 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Response\AllowedMethodsLoader;
use Symfony\Component\Config\ConfigCache;
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
use Symfony\Component\Routing\RouterInterface;
/**
* AllowedMethodsRouterLoader implementation using RouterInterface to fetch
* allowed http methods.
*
* @author Boris Guéry
*/
class AllowedMethodsRouterLoader implements AllowedMethodsLoaderInterface, CacheWarmerInterface
{
private $router;
private $cache;
/**
* Constructor.
*
* @param RouterInterface $router
* @param string $cacheDir
* @param bool $isDebug Kernel debug flag
*/
public function __construct(RouterInterface $router, $cacheDir, $isDebug)
{
$this->router = $router;
$this->cache = new ConfigCache(sprintf('%s/allowed_methods.cache.php', $cacheDir), $isDebug);
}
/**
* {@inheritdoc}
*/
public function getAllowedMethods()
{
if (!$this->cache->isFresh()) {
$this->warmUp(null);
}
return require $this->cache->getPath();
}
/**
* {@inheritdoc}
*/
public function isOptional()
{
return true;
}
/**
* {@inheritdoc}
*/
public function warmUp($cacheDir)
{
$processedRoutes = [];
$routeCollection = $this->router->getRouteCollection();
foreach ($routeCollection->all() as $name => $route) {
if (!isset($processedRoutes[$route->getPath()])) {
$processedRoutes[$route->getPath()] = [
'methods' => [],
'names' => [],
];
}
$processedRoutes[$route->getPath()]['names'][] = $name;
$processedRoutes[$route->getPath()]['methods'] = array_merge(
$processedRoutes[$route->getPath()]['methods'],
$route->getMethods()
);
}
$allowedMethods = [];
foreach ($processedRoutes as $processedRoute) {
if (count($processedRoute['methods']) > 0) {
foreach ($processedRoute['names'] as $name) {
$allowedMethods[$name] = array_unique($processedRoute['methods']);
}
}
}
$this->cache->write(
sprintf('getResources()
);
}
}
Routing/ClassResourceInterface.php 0000666 00000001012 13052362131 0013274 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Routing;
/**
* Implement interface to define that missing resources in the methods should
* use the class name to identify the resource.
*
* @author Lukas Kahwe Smith
*/
interface ClassResourceInterface
{
}
Routing/Loader/ClassUtils.php 0000666 00000003063 13052362131 0012202 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Routing\Loader;
/**
* @internal
*/
class ClassUtils
{
/**
* Returns the full class name for the first class in the file.
*
* @param string $file A PHP file path
*
* @return string|false Full class name if found, false otherwise
*/
public static function findClassInFile($file)
{
$class = false;
$namespace = false;
$tokens = token_get_all(file_get_contents($file));
for ($i = 0, $count = count($tokens); $i < $count; ++$i) {
$token = $tokens[$i];
if (!is_array($token)) {
continue;
}
if (true === $class && T_STRING === $token[0]) {
return $namespace.'\\'.$token[1];
}
if (true === $namespace && T_STRING === $token[0]) {
$namespace = '';
do {
$namespace .= $token[1];
$token = $tokens[++$i];
} while ($i < $count && is_array($token) && in_array($token[0], array(T_NS_SEPARATOR, T_STRING)));
}
if (T_CLASS === $token[0]) {
$class = true;
}
if (T_NAMESPACE === $token[0]) {
$namespace = true;
}
}
return false;
}
}
Routing/Loader/DirectoryRouteLoader.php 0000666 00000004072 13052362131 0014227 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Routing\Loader;
use Symfony\Component\Config\FileLocatorInterface;
use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Routing\RouteCollection;
/**
* Parse annotated controller classes from all files of a directory.
*
* @author Christian Flothmann
*/
class DirectoryRouteLoader extends Loader
{
private $fileLocator;
private $processor;
public function __construct(FileLocatorInterface $fileLocator, RestRouteProcessor $processor)
{
$this->fileLocator = $fileLocator;
$this->processor = $processor;
}
/**
* {@inheritdoc}
*/
public function load($resource, $type = null)
{
if (isset($resource[0]) && '@' === $resource[0]) {
$resource = $this->fileLocator->locate($resource);
}
if (!is_dir($resource)) {
throw new \InvalidArgumentException(sprintf('Given resource of type "%s" is no directory.', $resource));
}
$collection = new RouteCollection();
$finder = new Finder();
foreach ($finder->in($resource)->name('*.php')->files() as $file) {
if($class = ClassUtils::findClassInFile($file)) {
$imported = $this->processor->importResource($this, $class, array(), null, null, 'rest');
$collection->addCollection($imported);
}
}
return $collection;
}
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
{
if ('rest' !== $type || !is_string($resource)) {
return false;
}
if (isset($resource[0]) && '@' === $resource[0]) {
$resource = $this->fileLocator->locate($resource);
}
return is_dir($resource);
}
}
Routing/Loader/Reader/RestActionReader.php 0000666 00000051023 13052362131 0014513 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Routing\Loader\Reader;
use Doctrine\Common\Annotations\Reader;
use FOS\RestBundle\Controller\Annotations\Route as RouteAnnotation;
use FOS\RestBundle\Inflector\InflectorInterface;
use FOS\RestBundle\Request\ParamReaderInterface;
use FOS\RestBundle\Routing\RestRouteCollection;
use Symfony\Component\Routing\Route;
/**
* REST controller actions reader.
*
* @author Konstantin Kudryashov
*/
class RestActionReader
{
const COLLECTION_ROUTE_PREFIX = 'c';
/**
* @var Reader
*/
private $annotationReader;
/**
* @var ParamReaderInterface
*/
private $paramReader;
/**
* @var InflectorInterface
*/
private $inflector;
/**
* @var array
*/
private $formats;
/**
* @var bool
*/
private $includeFormat;
/**
* @var string|null
*/
private $routePrefix;
/**
* @var string|null
*/
private $namePrefix;
/**
* @var array|string|null
*/
private $versions;
/**
* @var bool|null
*/
private $pluralize;
/**
* @var array
*/
private $parents = [];
/**
* @var array
*/
private $availableHTTPMethods = [
'get',
'post',
'put',
'patch',
'delete',
'link',
'unlink',
'head',
'options',
'mkcol',
'propfind',
'proppatch',
'move',
'copy',
'lock',
'unlock',
];
/**
* @var array
*/
private $availableConventionalActions = ['new', 'edit', 'remove'];
/**
* Initializes controller reader.
*
* @param Reader $annotationReader
* @param ParamReaderInterface $paramReader
* @param InflectorInterface $inflector
* @param bool $includeFormat
* @param array $formats
*/
public function __construct(Reader $annotationReader, ParamReaderInterface $paramReader, InflectorInterface $inflector, $includeFormat, array $formats = [])
{
$this->annotationReader = $annotationReader;
$this->paramReader = $paramReader;
$this->inflector = $inflector;
$this->includeFormat = $includeFormat;
$this->formats = $formats;
}
/**
* Sets routes prefix.
*
* @param string $prefix Routes prefix
*/
public function setRoutePrefix($prefix = null)
{
$this->routePrefix = $prefix;
}
/**
* Returns route prefix.
*
* @return string
*/
public function getRoutePrefix()
{
return $this->routePrefix;
}
/**
* Sets route names prefix.
*
* @param string $prefix Route names prefix
*/
public function setNamePrefix($prefix = null)
{
$this->namePrefix = $prefix;
}
/**
* Returns name prefix.
*
* @return string
*/
public function getNamePrefix()
{
return $this->namePrefix;
}
/**
* Sets route names versions.
*
* @param array|string|null $versions Route names versions
*/
public function setVersions($versions = null)
{
$this->versions = (array) $versions;
}
/**
* Returns versions.
*
* @return array|null
*/
public function getVersions()
{
return $this->versions;
}
/**
* Sets pluralize.
*
* @param bool|null $pluralize Specify if resource name must be pluralized
*/
public function setPluralize($pluralize)
{
$this->pluralize = $pluralize;
}
/**
* Returns pluralize.
*
* @return bool|null
*/
public function getPluralize()
{
return $this->pluralize;
}
/**
* Set parent routes.
*
* @param array $parents Array of parent resources names
*/
public function setParents(array $parents)
{
$this->parents = $parents;
}
/**
* Returns parents.
*
* @return array
*/
public function getParents()
{
return $this->parents;
}
/**
* Reads action route.
*
* @param RestRouteCollection $collection
* @param \ReflectionMethod $method
* @param string[] $resource
*
* @throws \InvalidArgumentException
*
* @return Route
*/
public function read(RestRouteCollection $collection, \ReflectionMethod $method, $resource)
{
// check that every route parent has non-empty singular name
foreach ($this->parents as $parent) {
if (empty($parent) || '/' === substr($parent, -1)) {
throw new \InvalidArgumentException(
"Every parent controller must have `get{SINGULAR}Action(\$id)` method\n".
'where {SINGULAR} is a singular form of associated object'
);
}
}
// if method is not readable - skip
if (!$this->isMethodReadable($method)) {
return;
}
// if we can't get http-method and resources from method name - skip
$httpMethodAndResources = $this->getHttpMethodAndResourcesFromMethod($method, $resource);
if (!$httpMethodAndResources) {
return;
}
list($httpMethod, $resources, $isCollection, $isInflectable) = $httpMethodAndResources;
$arguments = $this->getMethodArguments($method);
// if we have only 1 resource & 1 argument passed, then it's object call, so
// we can set collection singular name
if (1 === count($resources) && 1 === count($arguments) - count($this->parents)) {
$collection->setSingularName($resources[0]);
}
// if we have parents passed - merge them with own resource names
if (count($this->parents)) {
$resources = array_merge($this->parents, $resources);
}
if (empty($resources)) {
$resources[] = null;
}
$routeName = $httpMethod.$this->generateRouteName($resources);
$urlParts = $this->generateUrlParts($resources, $arguments, $httpMethod);
// if passed method is not valid HTTP method then it's either
// a hypertext driver, a custom object (PUT) or collection (GET)
// method
if (!in_array($httpMethod, $this->availableHTTPMethods)) {
$urlParts[] = $httpMethod;
$httpMethod = $this->getCustomHttpMethod($httpMethod, $resources, $arguments);
}
// generated parameters
$routeName = strtolower($routeName);
$path = implode('/', $urlParts);
$defaults = ['_controller' => $method->getName()];
$requirements = [];
$options = [];
$host = '';
$versionCondition = $this->getVersionCondition();
$annotations = $this->readRouteAnnotation($method);
if (!empty($annotations)) {
foreach ($annotations as $annotation) {
$path = implode('/', $urlParts);
$defaults = ['_controller' => $method->getName()];
$requirements = [];
$options = [];
$methods = explode('|', $httpMethod);
$annoRequirements = $annotation->getRequirements();
$annoMethods = $annotation->getMethods();
if (!empty($annoMethods)) {
$methods = $annoMethods;
}
$path = $annotation->getPath() !== null ? $this->routePrefix.$annotation->getPath() : $path;
$requirements = array_merge($requirements, $annoRequirements);
$options = array_merge($options, $annotation->getOptions());
$defaults = array_merge($defaults, $annotation->getDefaults());
$host = $annotation->getHost();
$schemes = $annotation->getSchemes();
$combinedCondition = $this->combineConditions($versionCondition, $annotation->getCondition());
$this->includeFormatIfNeeded($path, $requirements);
// add route to collection
$route = new Route(
$path, $defaults, $requirements, $options, $host, $schemes, $methods, $combinedCondition
);
$this->addRoute($collection, $routeName, $route, $isCollection, $isInflectable, $annotation);
}
} else {
$this->includeFormatIfNeeded($path, $requirements);
$methods = explode('|', strtoupper($httpMethod));
// add route to collection
$route = new Route(
$path, $defaults, $requirements, $options, $host, [], $methods, $versionCondition
);
$this->addRoute($collection, $routeName, $route, $isCollection, $isInflectable);
}
}
/**
* @return string|null
*/
private function getVersionCondition()
{
if (empty($this->versions)) {
return;
}
return sprintf("request.attributes.get('version') in ['%s']", implode("', '", $this->versions));
}
/**
* @param string|null $conditionOne
* @param string|null $conditionTwo
*
* @return string|null
*/
private function combineConditions($conditionOne, $conditionTwo)
{
if (null === $conditionOne) {
return $conditionTwo;
}
if (null === $conditionTwo) {
return $conditionOne;
}
return sprintf('(%s) and (%s)', $conditionOne, $conditionTwo);
}
/**
* Include the format in the path and requirements if its enabled.
*
* @param string $path
* @param array $requirements
*/
private function includeFormatIfNeeded(&$path, &$requirements)
{
if ($this->includeFormat === true) {
$path .= '.{_format}';
if (!isset($requirements['_format']) && !empty($this->formats)) {
$requirements['_format'] = implode('|', array_keys($this->formats));
}
}
}
/**
* Checks whether provided method is readable.
*
* @param \ReflectionMethod $method
*
* @return bool
*/
private function isMethodReadable(\ReflectionMethod $method)
{
// if method starts with _ - skip
if ('_' === substr($method->getName(), 0, 1)) {
return false;
}
$hasNoRouteMethod = (bool) $this->readMethodAnnotation($method, 'NoRoute');
$hasNoRouteClass = (bool) $this->readClassAnnotation($method->getDeclaringClass(), 'NoRoute');
$hasNoRoute = $hasNoRouteMethod || $hasNoRouteClass;
// since NoRoute extends Route we need to exclude all the method NoRoute annotations
$hasRoute = (bool) $this->readMethodAnnotation($method, 'Route') && !$hasNoRouteMethod;
// if method has NoRoute annotation and does not have Route annotation - skip
if ($hasNoRoute && !$hasRoute) {
return false;
}
return true;
}
/**
* Returns HTTP method and resources list from method signature.
*
* @param \ReflectionMethod $method
* @param string[] $resource
*
* @return bool|array
*/
private function getHttpMethodAndResourcesFromMethod(\ReflectionMethod $method, $resource)
{
// if method doesn't match regex - skip
if (!preg_match('/([a-z][_a-z0-9]+)(.*)Action/', $method->getName(), $matches)) {
return false;
}
$httpMethod = strtolower($matches[1]);
$resources = preg_split(
'/([A-Z][^A-Z]*)/', $matches[2], -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE
);
$isCollection = false;
$isInflectable = true;
if (0 === strpos($httpMethod, self::COLLECTION_ROUTE_PREFIX)
&& in_array(substr($httpMethod, 1), $this->availableHTTPMethods)
) {
$isCollection = true;
$httpMethod = substr($httpMethod, 1);
} elseif ('options' === $httpMethod) {
$isCollection = true;
}
if ($isCollection && !empty($resource)) {
$resourcePluralized = $this->generateResourceName(end($resource));
$isInflectable = ($resourcePluralized != $resource[count($resource) - 1]);
$resource[count($resource) - 1] = $resourcePluralized;
}
$resources = array_merge($resource, $resources);
return [$httpMethod, $resources, $isCollection, $isInflectable];
}
/**
* Returns readable arguments from method.
*
* @param \ReflectionMethod $method
*
* @return \ReflectionParameter[]
*/
private function getMethodArguments(\ReflectionMethod $method)
{
// ignore all query params
$params = $this->paramReader->getParamsFromMethod($method);
// ignore several type hinted arguments
$ignoreClasses = [
\Symfony\Component\HttpFoundation\Request::class,
\FOS\RestBundle\Request\ParamFetcherInterface::class,
\Symfony\Component\Validator\ConstraintViolationListInterface::class,
\Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter::class,
];
$arguments = [];
foreach ($method->getParameters() as $argument) {
if (isset($params[$argument->getName()])) {
continue;
}
$argumentClass = $argument->getClass();
if ($argumentClass) {
$className = $argumentClass->getName();
foreach ($ignoreClasses as $class) {
if ($className === $class || is_subclass_of($className, $class)) {
continue 2;
}
}
}
$arguments[] = $argument;
}
return $arguments;
}
/**
* Generates final resource name.
*
* @param string|bool $resource
*
* @return string
*/
private function generateResourceName($resource)
{
if (false === $this->pluralize) {
return $resource;
}
return $this->inflector->pluralize($resource);
}
/**
* Generates route name from resources list.
*
* @param string[] $resources
*
* @return string
*/
private function generateRouteName(array $resources)
{
$routeName = '';
foreach ($resources as $resource) {
if (null !== $resource) {
$routeName .= '_'.basename($resource);
}
}
return $routeName;
}
/**
* Generates URL parts for route from resources list.
*
* @param string[] $resources
* @param \ReflectionParameter[] $arguments
* @param string $httpMethod
*
* @return array
*/
private function generateUrlParts(array $resources, array $arguments, $httpMethod)
{
$urlParts = [];
foreach ($resources as $i => $resource) {
// if we already added all parent routes paths to URL & we have
// prefix - add it
if (!empty($this->routePrefix) && $i === count($this->parents)) {
$urlParts[] = $this->routePrefix;
}
// if we have argument for current resource, then it's object.
// otherwise - it's collection
if (isset($arguments[$i])) {
if (null !== $resource) {
$urlParts[] =
strtolower($this->generateResourceName($resource))
.'/{'.$arguments[$i]->getName().'}';
} else {
$urlParts[] = '{'.$arguments[$i]->getName().'}';
}
} elseif (null !== $resource) {
if ((0 === count($arguments) && !in_array($httpMethod, $this->availableHTTPMethods))
|| 'new' === $httpMethod
|| 'post' === $httpMethod
) {
$urlParts[] = $this->generateResourceName(strtolower($resource));
} else {
$urlParts[] = strtolower($resource);
}
}
}
return $urlParts;
}
/**
* Returns custom HTTP method for provided list of resources, arguments, method.
*
* @param string $httpMethod current HTTP method
* @param string[] $resources resources list
* @param \ReflectionParameter[] $arguments list of method arguments
*
* @return string
*/
private function getCustomHttpMethod($httpMethod, array $resources, array $arguments)
{
if (in_array($httpMethod, $this->availableConventionalActions)) {
// allow hypertext as the engine of application state
// through conventional GET actions
return 'get';
}
if (count($arguments) < count($resources)) {
// resource collection
return 'get';
}
// custom object
return 'patch';
}
/**
* Returns first route annotation for method.
*
* @param \ReflectionMethod $reflectionMethod
*
* @return RouteAnnotation[]
*/
private function readRouteAnnotation(\ReflectionMethod $reflectionMethod)
{
$annotations = [];
if ($newAnnotations = $this->readMethodAnnotations($reflectionMethod, 'Route')) {
$annotations = array_merge($annotations, $newAnnotations);
}
return $annotations;
}
/**
* Reads class annotations.
*
* @param \ReflectionClass $reflectionClass
* @param string $annotationName
*
* @return RouteAnnotation|null
*/
private function readClassAnnotation(\ReflectionClass $reflectionClass, $annotationName)
{
$annotationClass = "FOS\\RestBundle\\Controller\\Annotations\\$annotationName";
if ($annotation = $this->annotationReader->getClassAnnotation($reflectionClass, $annotationClass)) {
return $annotation;
}
}
/**
* Reads method annotations.
*
* @param \ReflectionMethod $reflectionMethod
* @param string $annotationName
*
* @return RouteAnnotation|null
*/
private function readMethodAnnotation(\ReflectionMethod $reflectionMethod, $annotationName)
{
$annotationClass = "FOS\\RestBundle\\Controller\\Annotations\\$annotationName";
if ($annotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, $annotationClass)) {
return $annotation;
}
}
/**
* Reads method annotations.
*
* @param \ReflectionMethod $reflectionMethod
* @param string $annotationName
*
* @return RouteAnnotation[]
*/
private function readMethodAnnotations(\ReflectionMethod $reflectionMethod, $annotationName)
{
$annotations = [];
$annotationClass = "FOS\\RestBundle\\Controller\\Annotations\\$annotationName";
if ($annotations_new = $this->annotationReader->getMethodAnnotations($reflectionMethod)) {
foreach ($annotations_new as $annotation) {
if ($annotation instanceof $annotationClass) {
$annotations[] = $annotation;
}
}
}
return $annotations;
}
/**
* @param RestRouteCollection $collection
* @param string $routeName
* @param Route $route
* @param bool $isCollection
* @param bool $isInflectable
* @param RouteAnnotation $annotation
*/
private function addRoute(RestRouteCollection $collection, $routeName, $route, $isCollection, $isInflectable, RouteAnnotation $annotation = null)
{
if ($annotation && null !== $annotation->getName()) {
$options = $annotation->getOptions();
if (isset($options['method_prefix']) && false === $options['method_prefix']) {
$routeName = $annotation->getName();
} else {
$routeName = $routeName.$annotation->getName();
}
}
$fullRouteName = $this->namePrefix.$routeName;
if ($isCollection && !$isInflectable) {
$collection->add($this->namePrefix.self::COLLECTION_ROUTE_PREFIX.$routeName, $route);
if (!$collection->get($fullRouteName)) {
$collection->add($fullRouteName, clone $route);
}
} else {
$collection->add($fullRouteName, $route);
}
}
}
Routing/Loader/Reader/RestControllerReader.php 0000666 00000007444 13052362131 0015431 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Routing\Loader\Reader;
use Doctrine\Common\Annotations\Reader;
use FOS\RestBundle\Controller\Annotations;
use FOS\RestBundle\Routing\ClassResourceInterface;
use FOS\RestBundle\Routing\RestRouteCollection;
use Symfony\Component\Config\Resource\FileResource;
/**
* REST controller reader.
*
* @author Konstantin Kudryashov
*/
class RestControllerReader
{
private $actionReader;
private $annotationReader;
/**
* Initializes controller reader.
*
* @param RestActionReader $actionReader action reader
* @param Reader $annotationReader annotation reader
*/
public function __construct(RestActionReader $actionReader, Reader $annotationReader)
{
$this->actionReader = $actionReader;
$this->annotationReader = $annotationReader;
}
/**
* Returns action reader.
*
* @return RestActionReader
*/
public function getActionReader()
{
return $this->actionReader;
}
/**
* Reads controller routes.
*
* @param \ReflectionClass $reflectionClass
*
* @throws \InvalidArgumentException
*
* @return RestRouteCollection
*/
public function read(\ReflectionClass $reflectionClass)
{
$collection = new RestRouteCollection();
$collection->addResource(new FileResource($reflectionClass->getFileName()));
// read prefix annotation
if ($annotation = $this->annotationReader->getClassAnnotation($reflectionClass, Annotations\Prefix::class)) {
$this->actionReader->setRoutePrefix($annotation->value);
}
// read name-prefix annotation
if ($annotation = $this->annotationReader->getClassAnnotation($reflectionClass, Annotations\NamePrefix::class)) {
$this->actionReader->setNamePrefix($annotation->value);
}
// read version annotation
if ($annotation = $this->annotationReader->getClassAnnotation($reflectionClass, Annotations\Version::class)) {
$this->actionReader->setVersions($annotation->value);
}
$resource = [];
// read route-resource annotation
if ($annotation = $this->annotationReader->getClassAnnotation($reflectionClass, Annotations\RouteResource::class)) {
$resource = explode('_', $annotation->resource);
$this->actionReader->setPluralize($annotation->pluralize);
} elseif ($reflectionClass->implementsInterface(ClassResourceInterface::class)) {
$resource = preg_split(
'/([A-Z][^A-Z]*)Controller/', $reflectionClass->getShortName(), -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE
);
if (empty($resource)) {
throw new \InvalidArgumentException("Controller '{$reflectionClass->name}' does not identify a resource");
}
}
// trim '/' at the start of the prefix
if ('/' === substr($prefix = $this->actionReader->getRoutePrefix(), 0, 1)) {
$this->actionReader->setRoutePrefix(substr($prefix, 1));
}
// read action routes into collection
foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
$this->actionReader->read($collection, $method, $resource);
}
$this->actionReader->setRoutePrefix(null);
$this->actionReader->setNamePrefix(null);
$this->actionReader->setVersions(null);
$this->actionReader->setPluralize(null);
$this->actionReader->setParents([]);
return $collection;
}
}
Routing/Loader/RestRouteLoader.php 0000666 00000011265 13052362131 0013202 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Routing\Loader;
use FOS\RestBundle\Routing\Loader\Reader\RestControllerReader;
use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser;
use Symfony\Component\Config\FileLocatorInterface;
use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* RestRouteLoader REST-enabled controller router loader.
*
* @author Konstantin Kudryashov
* @author Bulat Shakirzyanov
*/
class RestRouteLoader extends Loader
{
protected $container;
protected $controllerParser;
protected $controllerReader;
protected $defaultFormat;
protected $locator;
/**
* Initializes loader.
*
* @param ContainerInterface $container
* @param FileLocatorInterface $locator
* @param ControllerNameParser $controllerParser
* @param RestControllerReader $controllerReader
* @param string $defaultFormat
*/
public function __construct(
ContainerInterface $container,
FileLocatorInterface $locator,
ControllerNameParser $controllerParser,
RestControllerReader $controllerReader, $defaultFormat = 'html'
) {
$this->container = $container;
$this->locator = $locator;
$this->controllerParser = $controllerParser;
$this->controllerReader = $controllerReader;
$this->defaultFormat = $defaultFormat;
}
/**
* Returns controller reader.
*
* @return RestControllerReader
*/
public function getControllerReader()
{
return $this->controllerReader;
}
/**
* {@inheritdoc}
*/
public function load($controller, $type = null)
{
list($prefix, $class) = $this->getControllerLocator($controller);
$collection = $this->controllerReader->read(new \ReflectionClass($class));
$collection->prependRouteControllersWithPrefix($prefix);
$collection->setDefaultFormat($this->defaultFormat);
return $collection;
}
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
{
return is_string($resource)
&& 'rest' === $type
&& !in_array(pathinfo($resource, PATHINFO_EXTENSION), ['xml', 'yml']
);
}
/**
* Returns controller locator by it's id.
*
* @param string $controller
*
* @throws \InvalidArgumentException
*
* @return array
*/
private function getControllerLocator($controller)
{
$class = null;
$prefix = null;
if (0 === strpos($controller, '@')) {
$file = $this->locator->locate($controller);
$controllerClass = ClassUtils::findClassInFile($file);
if (false === $controllerClass) {
throw new \InvalidArgumentException(sprintf('Can\'t find class for controller "%s"', $controller));
}
$controller = $controllerClass;
}
if ($this->container->has($controller)) {
// service_id
$prefix = $controller.':';
$useScope = method_exists($this->container, 'enterScope') && $this->container->hasScope('request');
if ($useScope) {
$this->container->enterScope('request');
$this->container->set('request', new Request());
}
$class = get_class($this->container->get($controller));
if ($useScope) {
$this->container->leaveScope('request');
}
} elseif (class_exists($controller)) {
// full class name
$class = $controller;
$prefix = $class.'::';
} elseif (false !== strpos($controller, ':')) {
// bundle:controller notation
try {
$notation = $this->controllerParser->parse($controller.':method');
list($class) = explode('::', $notation);
$prefix = $class.'::';
} catch (\Exception $e) {
throw new \InvalidArgumentException(
sprintf('Can\'t locate "%s" controller.', $controller)
);
}
}
if (empty($class)) {
throw new \InvalidArgumentException(sprintf(
'Class could not be determined for Controller identified by "%s".', $controller
));
}
return [$prefix, $class];
}
}
Routing/Loader/RestRouteProcessor.php 0000666 00000004003 13052362131 0013743 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Routing\Loader;
use Symfony\Component\Config\Loader\FileLoader;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\Routing\RouteCollection;
/**
* Processes resource in provided loader.
*
* @author Donald Tyler
* @author Konstantin Kudryashov
*/
class RestRouteProcessor
{
/**
* Import & return routes collection from a resource.
*
* @param LoaderInterface $loader The Loader
* @param mixed $resource A Resource
* @param array $parents Array of parent resources names
* @param string $routePrefix Current routes prefix
* @param string $namePrefix Routes names prefix
* @param string $type The resource type
* @param string $currentDir Current directory of the loader
*
* @return RouteCollection A RouteCollection instance
*/
public function importResource(
LoaderInterface $loader,
$resource,
array $parents = [],
$routePrefix = null,
$namePrefix = null,
$type = null,
$currentDir = null
) {
$loader = $loader->resolve($resource, $type);
if ($loader instanceof FileLoader && null !== $currentDir) {
$resource = $loader->getLocator()->locate($resource, $currentDir);
} elseif ($loader instanceof RestRouteLoader) {
$loader->getControllerReader()->getActionReader()->setParents($parents);
$loader->getControllerReader()->getActionReader()->setRoutePrefix($routePrefix);
$loader->getControllerReader()->getActionReader()->setNamePrefix($namePrefix);
}
return $loader->load($resource, $type);
}
}
Routing/Loader/RestXmlCollectionLoader.php 0000666 00000023005 13052362131 0014653 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Routing\Loader;
use FOS\RestBundle\Routing\RestRouteCollection;
use Symfony\Component\Config\FileLocatorInterface;
use Symfony\Component\Config\Util\XmlUtils;
use Symfony\Component\Routing\Loader\XmlFileLoader;
use Symfony\Component\Routing\RouteCollection;
/**
* RestXmlCollectionLoader XML file collections loader.
*
* @author Donald Tyler
*/
class RestXmlCollectionLoader extends XmlFileLoader
{
protected $collectionParents = [];
private $processor;
private $includeFormat;
private $formats;
private $defaultFormat;
/**
* Initializes xml loader.
*
* @param FileLocatorInterface $locator
* @param RestRouteProcessor $processor
* @param bool $includeFormat
* @param string[] $formats
* @param string $defaultFormat
*/
public function __construct(
FileLocatorInterface $locator,
RestRouteProcessor $processor,
$includeFormat = true,
array $formats = [],
$defaultFormat = null
) {
parent::__construct($locator);
$this->processor = $processor;
$this->includeFormat = $includeFormat;
$this->formats = $formats;
$this->defaultFormat = $defaultFormat;
}
/**
* {@inheritdoc}
*/
protected function parseNode(RouteCollection $collection, \DOMElement $node, $path, $file)
{
switch ($node->tagName) {
case 'route':
$this->parseRoute($collection, $node, $path);
break;
case 'import':
$name = (string) $node->getAttribute('id');
$resource = (string) $node->getAttribute('resource');
$prefix = (string) $node->getAttribute('prefix');
$namePrefix = (string) $node->getAttribute('name-prefix');
$parent = (string) $node->getAttribute('parent');
$type = (string) $node->getAttribute('type');
$host = isset($config['host']) ? $config['host'] : null;
$currentDir = dirname($path);
$parents = [];
if (!empty($parent)) {
if (!isset($this->collectionParents[$parent])) {
throw new \InvalidArgumentException(sprintf('Cannot find parent resource with name %s', $parent));
}
$parents = $this->collectionParents[$parent];
}
$imported = $this->processor->importResource($this, $resource, $parents, $prefix, $namePrefix, $type, $currentDir);
if (!empty($name) && $imported instanceof RestRouteCollection) {
$parents[] = (!empty($prefix) ? $prefix.'/' : '').$imported->getSingularName();
$prefix = null;
$this->collectionParents[$name] = $parents;
}
if (!empty($host)) {
$imported->setHost($host);
}
$imported->addPrefix($prefix);
$collection->addCollection($imported);
break;
default:
throw new \InvalidArgumentException(sprintf('Unable to parse tag "%s"', $node->tagName));
}
}
/**
* {@inheritdoc}
*/
protected function parseRoute(RouteCollection $collection, \DOMElement $node, $path)
{
if ($this->includeFormat) {
$path = $node->getAttribute('path');
// append format placeholder if not present
if (false === strpos($path, '{_format}')) {
$node->setAttribute('path', $path.'.{_format}');
}
// set format requirement if configured globally
$requirements = $node->getElementsByTagNameNS(self::NAMESPACE_URI, 'requirement');
$format = null;
for ($i = 0; $i < $requirements->length; ++$i) {
$item = $requirements->item($i);
if ($item instanceof \DOMElement && $item->hasAttribute('_format')) {
$format = $item->getAttribute('_format');
break;
}
}
if (null === $format && !empty($this->formats)) {
$requirement = $node->ownerDocument->createElementNs(
self::NAMESPACE_URI,
'requirement',
implode('|', array_keys($this->formats))
);
$requirement->setAttribute('key', '_format');
$node->appendChild($requirement);
}
}
// set the default format if configured
if (null !== $this->defaultFormat) {
$defaultFormatNode = $node->ownerDocument->createElementNS(
self::NAMESPACE_URI,
'default',
$this->defaultFormat
);
$defaultFormatNode->setAttribute('key', '_format');
$node->appendChild($defaultFormatNode);
}
$options = $this->getOptions($node);
foreach ($options as $option) {
$node->appendChild($option);
}
$length = $node->childNodes->length;
for ($i = 0; $i < $length; ++$i) {
$loopNode = $node->childNodes->item($i);
if ($loopNode->nodeType === XML_TEXT_NODE) {
continue;
}
$newNode = $node->ownerDocument->createElementNS(
self::NAMESPACE_URI,
$loopNode->nodeName,
$loopNode->nodeValue
);
foreach ($loopNode->attributes as $value) {
$newNode->setAttribute($value->name, $value->value);
}
$node->appendChild($newNode);
}
parent::parseRoute($collection, $node, $path);
}
private function getOptions(\DOMElement $node)
{
$options = [];
foreach ($node->childNodes as $child) {
if ($child instanceof \DOMElement && $child->tagName === 'option') {
$option = $node->ownerDocument->createElementNs(
self::NAMESPACE_URI,
'option',
$child->nodeValue
);
$option->setAttribute('key', $child->getAttribute('key'));
$options[] = $option;
}
}
return $options;
}
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
{
return is_string($resource) &&
'xml' === pathinfo($resource, PATHINFO_EXTENSION) &&
'rest' === $type;
}
/**
* @param \DOMDocument $dom
*
* @throws \InvalidArgumentException When xml doesn't validate its xsd schema
*/
protected function validate(\DOMDocument $dom)
{
$restRoutinglocation = realpath(__DIR__.'/../../Resources/config/schema/routing/rest_routing-1.0.xsd');
$restRoutinglocation = rawurlencode(str_replace('\\', '/', $restRoutinglocation));
$routinglocation = realpath(__DIR__.'/../../Resources/config/schema/routing-1.0.xsd');
$routinglocation = rawurlencode(str_replace('\\', '/', $routinglocation));
$source = <<
EOF;
$current = libxml_use_internal_errors(true);
libxml_clear_errors();
if (!$dom->schemaValidateSource($source)) {
throw new \InvalidArgumentException(implode("\n", $this->getXmlErrors_($current)));
}
libxml_use_internal_errors($current);
}
/**
* {@inheritdoc}
*
* @internal
*/
protected function loadFile($file)
{
if (class_exists('Symfony\Component\Config\Util\XmlUtils')) {
$dom = XmlUtils::loadFile($file);
$this->validate($dom);
return $dom;
}
return parent::loadFile($file);
}
/**
* Retrieves libxml errors and clears them.
*
* Note: The underscore postfix on the method name is to ensure compatibility with versions
* before 2.0.16 while working around a bug in PHP https://bugs.php.net/bug.php?id=62956
*
* @param bool $internalErrors The previous state of internal errors to reset it
*
* @return array An array of libxml error strings
*/
private function getXmlErrors_($internalErrors)
{
$errors = [];
foreach (libxml_get_errors() as $error) {
$errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)',
LIBXML_ERR_WARNING === $error->level ? 'WARNING' : 'ERROR',
$error->code,
trim($error->message),
$error->file ? $error->file : 'n/a',
$error->line,
$error->column
);
}
libxml_clear_errors();
libxml_use_internal_errors($internalErrors);
return $errors;
}
}
Routing/Loader/RestYamlCollectionLoader.php 0000666 00000015673 13052362131 0015031 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Routing\Loader;
use FOS\RestBundle\Routing\RestRouteCollection;
use Symfony\Component\Config\FileLocatorInterface;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\Routing\Loader\YamlFileLoader;
use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
/**
* RestYamlCollectionLoader YAML file collections loader.
*/
class RestYamlCollectionLoader extends YamlFileLoader
{
protected $collectionParents = [];
private $processor;
private $includeFormat;
private $formats;
private $defaultFormat;
/**
* Initializes yaml loader.
*
* @param FileLocatorInterface $locator
* @param RestRouteProcessor $processor
* @param bool $includeFormat
* @param string[] $formats
* @param string $defaultFormat
*/
public function __construct(
FileLocatorInterface $locator,
RestRouteProcessor $processor,
$includeFormat = true,
array $formats = [],
$defaultFormat = null
) {
parent::__construct($locator);
$this->processor = $processor;
$this->includeFormat = $includeFormat;
$this->formats = $formats;
$this->defaultFormat = $defaultFormat;
}
/**
* {@inheritdoc}
*/
public function load($file, $type = null)
{
$path = $this->locator->locate($file);
try {
$config = Yaml::parse(file_get_contents($path));
} catch (ParseException $e) {
throw new \InvalidArgumentException(sprintf('The file "%s" does not contain valid YAML.', $path), 0, $e);
}
$collection = new RouteCollection();
$collection->addResource(new FileResource($path));
// empty file
if (null === $config) {
return $collection;
}
// not an array
if (!is_array($config)) {
throw new \InvalidArgumentException(sprintf('The file "%s" must contain a Yaml mapping (an array).', $path));
}
// process routes and imports
foreach ($config as $name => $config) {
if (isset($config['resource'])) {
$resource = $config['resource'];
$prefix = isset($config['prefix']) ? $config['prefix'] : null;
$namePrefix = isset($config['name_prefix']) ? $config['name_prefix'] : null;
$parent = isset($config['parent']) ? $config['parent'] : null;
$type = isset($config['type']) ? $config['type'] : null;
$host = isset($config['host']) ? $config['host'] : null;
$requirements = isset($config['requirements']) ? $config['requirements'] : [];
$defaults = isset($config['defaults']) ? $config['defaults'] : [];
$options = isset($config['options']) ? $config['options'] : [];
$currentDir = dirname($path);
$parents = [];
if (!empty($parent)) {
if (!isset($this->collectionParents[$parent])) {
throw new \InvalidArgumentException(sprintf('Cannot find parent resource with name %s', $parent));
}
$parents = $this->collectionParents[$parent];
}
$imported = $this->processor->importResource($this, $resource, $parents, $prefix, $namePrefix, $type, $currentDir);
if ($imported instanceof RestRouteCollection) {
$parents[] = ($prefix ? $prefix.'/' : '').$imported->getSingularName();
$prefix = null;
$namePrefix = null;
$this->collectionParents[$name] = $parents;
}
$imported->addRequirements($requirements);
$imported->addDefaults($defaults);
$imported->addOptions($options);
if (!empty($host)) {
$imported->setHost($host);
}
$imported->addPrefix($prefix);
// Add name prefix from parent config files
$imported = $this->addParentNamePrefix($imported, $namePrefix);
$collection->addCollection($imported);
} elseif (isset($config['pattern']) || isset($config['path'])) {
// the YamlFileLoader of the Routing component only checks for
// the path option
if (!isset($config['path'])) {
$config['path'] = $config['pattern'];
@trigger_error(sprintf('The "pattern" option at "%s" in file "%s" is deprecated. Use the "path" option instead.', $name, $path), E_USER_DEPRECATED);
}
if ($this->includeFormat) {
// append format placeholder if not present
if (false === strpos($config['path'], '{_format}')) {
$config['path'] .= '.{_format}';
}
// set format requirement if configured globally
if (!isset($config['requirements']['_format']) && !empty($this->formats)) {
$config['requirements']['_format'] = implode('|', array_keys($this->formats));
}
}
// set the default format if configured
if (null !== $this->defaultFormat) {
$config['defaults']['_format'] = $this->defaultFormat;
}
$this->parseRoute($collection, $name, $config, $path);
} else {
throw new \InvalidArgumentException(sprintf('Unable to parse the "%s" route.', $name));
}
}
return $collection;
}
/**
* {@inheritdoc}
*/
public function supports($resource, $type = null)
{
return is_string($resource) &&
'yml' === pathinfo($resource, PATHINFO_EXTENSION) &&
'rest' === $type;
}
/**
* Adds a name prefix to the route name of all collection routes.
*
* @param RouteCollection $collection Route collection
* @param array $namePrefix NamePrefix to add in each route name of the route collection
*
* @return RouteCollection
*/
public function addParentNamePrefix(RouteCollection $collection, $namePrefix)
{
if (!isset($namePrefix) || ($namePrefix = trim($namePrefix)) === '') {
return $collection;
}
$iterator = $collection->getIterator();
foreach ($iterator as $key1 => $route1) {
$collection->add($namePrefix.$key1, $route1);
$collection->remove($key1);
}
return $collection;
}
}
Routing/RestRouteCollection.php 0000666 00000004356 13052362131 0012664 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Routing;
use Symfony\Component\Routing\RouteCollection;
/**
* Restful route collection.
*
* @author Konstantin Kudryashov
*/
class RestRouteCollection extends RouteCollection
{
private $singularName;
/**
* Sets collection singular name.
*
* @param string $name Singular name
*/
public function setSingularName($name)
{
$this->singularName = $name;
}
/**
* Returns collection singular name.
*
* @return string
*/
public function getSingularName()
{
return $this->singularName;
}
/**
* Adds controller prefix to all collection routes.
*
* @param string $prefix
*/
public function prependRouteControllersWithPrefix($prefix)
{
foreach (parent::all() as $route) {
$route->setDefault('_controller', $prefix.$route->getDefault('_controller'));
}
}
/**
* Sets default format of routes.
*
* @param string $format
*/
public function setDefaultFormat($format)
{
foreach (parent::all() as $route) {
// Set default format only if not set already (could be defined in annotation)
if (!$route->getDefault('_format')) {
$route->setDefault('_format', $format);
}
}
}
/**
* Returns routes sorted by custom HTTP methods first.
*
* @return array
*/
public function all()
{
$regex = '/
(_|^)
(get|post|put|delete|patch|head|options|mkcol|propfind|proppatch|lock|unlock|move|copy|link|unlink)_ # allowed http methods
/i';
$routes = parent::all();
$customMethodRoutes = [];
foreach ($routes as $routeName => $route) {
if (!preg_match($regex, $routeName)) {
$customMethodRoutes[$routeName] = $route;
unset($routes[$routeName]);
}
}
return $customMethodRoutes + $routes;
}
}
Serializer/JMSHandlerRegistry.php 0000666 00000003026 13052362131 0013047 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Serializer;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\Handler\HandlerRegistryInterface;
/**
* Search in the class parents to find an adapted handler.
*
* @author Ener-Getick
*
* @internal do not depend on this class directly
*/
class JMSHandlerRegistry implements HandlerRegistryInterface
{
private $registry;
public function __construct(HandlerRegistryInterface $registry)
{
$this->registry = $registry;
}
/**
* {@inheritdoc}
*/
public function registerSubscribingHandler(SubscribingHandlerInterface $handler)
{
return $this->registry->registerSubscribingHandler($handler);
}
/**
* {@inheritdoc}
*/
public function registerHandler($direction, $typeName, $format, $handler)
{
return $this->registry->registerHandler($direction, $typeName, $format, $handler);
}
/**
* {@inheritdoc}
*/
public function getHandler($direction, $typeName, $format)
{
do {
$handler = $this->registry->getHandler($direction, $typeName, $format);
if (null !== $handler) {
return $handler;
}
} while ($typeName = get_parent_class($typeName));
}
}
Serializer/JMSSerializerAdapter.php 0000666 00000005521 13052362131 0013355 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Serializer;
use FOS\RestBundle\Context\Context;
use JMS\Serializer\Context as JMSContext;
use JMS\Serializer\DeserializationContext as JMSDeserializationContext;
use JMS\Serializer\SerializationContext as JMSSerializationContext;
use JMS\Serializer\SerializerInterface;
/**
* Adapter to plug the JMS serializer into the FOSRestBundle Serializer API.
*
* @author Christian Flothmann
*/
class JMSSerializerAdapter implements Serializer
{
/**
* @internal
*/
const SERIALIZATION = 0;
/**
* @internal
*/
const DESERIALIZATION = 1;
private $serializer;
public function __construct(SerializerInterface $serializer)
{
$this->serializer = $serializer;
}
/**
* {@inheritdoc}
*/
public function serialize($data, $format, Context $context)
{
$context = $this->convertContext($context, self::SERIALIZATION);
return $this->serializer->serialize($data, $format, $context);
}
/**
* {@inheritdoc}
*/
public function deserialize($data, $type, $format, Context $context)
{
$context = $this->convertContext($context, self::DESERIALIZATION);
return $this->serializer->deserialize($data, $type, $format, $context);
}
/**
* @param Context $context
* @param int $direction {@see self} constants
*
* @return JMSContext
*/
private function convertContext(Context $context, $direction)
{
if ($direction === self::SERIALIZATION) {
$jmsContext = JMSSerializationContext::create();
} else {
$jmsContext = JMSDeserializationContext::create();
$maxDepth = $context->getMaxDepth(false);
if (null !== $maxDepth) {
for ($i = 0; $i < $maxDepth; ++$i) {
$jmsContext->increaseDepth();
}
}
}
foreach ($context->getAttributes() as $key => $value) {
$jmsContext->attributes->set($key, $value);
}
if (null !== $context->getVersion()) {
$jmsContext->setVersion($context->getVersion());
}
if (null !== $context->getGroups()) {
$jmsContext->setGroups($context->getGroups());
}
if (null !== $context->getMaxDepth(false) || null !== $context->isMaxDepthEnabled()) {
$jmsContext->enableMaxDepthChecks();
}
if (null !== $context->getSerializeNull()) {
$jmsContext->setSerializeNull($context->getSerializeNull());
}
return $jmsContext;
}
}
Serializer/Normalizer/AbstractExceptionNormalizer.php 0000666 00000002661 13052362131 0017202 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Serializer\Normalizer;
use FOS\RestBundle\Util\ExceptionValueMap;
use Symfony\Component\HttpFoundation\Response;
/**
* @author Ener-Getick
*
* @internal do not use this class in your code
*/
class AbstractExceptionNormalizer
{
/**
* @var ExceptionValueMap
*/
private $messagesMap;
/**
* @var bool
*/
private $debug;
/**
* @param array $messagesMap
* @param bool $debug
*/
public function __construct(ExceptionValueMap $messagesMap, $debug)
{
$this->messagesMap = $messagesMap;
$this->debug = $debug;
}
/**
* Extracts the exception message.
*
* @param \Exception $exception
* @param int|null $statusCode
*
* @return string
*/
protected function getExceptionMessage(\Exception $exception, $statusCode = null)
{
$showMessage = $this->messagesMap->resolveException($exception);
if ($showMessage || $this->debug) {
return $exception->getMessage();
}
return array_key_exists($statusCode, Response::$statusTexts) ? Response::$statusTexts[$statusCode] : 'error';
}
}
Serializer/Normalizer/ExceptionHandler.php 0000666 00000006337 13052362131 0014755 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Serializer\Normalizer;
use JMS\Serializer\Context;
use JMS\Serializer\GraphNavigator;
use JMS\Serializer\Handler\SubscribingHandlerInterface;
use JMS\Serializer\JsonSerializationVisitor;
use JMS\Serializer\XmlSerializationVisitor;
class ExceptionHandler extends AbstractExceptionNormalizer implements SubscribingHandlerInterface
{
/**
* @return array
*/
public static function getSubscribingMethods()
{
return [
[
'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
'format' => 'json',
'type' => \Exception::class,
'method' => 'serializeToJson',
],
[
'direction' => GraphNavigator::DIRECTION_SERIALIZATION,
'format' => 'xml',
'type' => \Exception::class,
'method' => 'serializeToXml',
],
];
}
/**
* @param JsonSerializationVisitor $visitor
* @param Exception $exception
* @param array $type
* @param Context $context
*
* @return array
*/
public function serializeToJson(
JsonSerializationVisitor $visitor,
\Exception $exception,
array $type,
Context $context
) {
$data = $this->convertToArray($exception, $context);
return $visitor->visitArray($data, $type, $context);
}
/**
* @param XmlSerializationVisitor $visitor
* @param Exception $exception
* @param array $type
* @param Context $context
*/
public function serializeToXml(
XmlSerializationVisitor $visitor,
\Exception $exception,
array $type,
Context $context
) {
$data = $this->convertToArray($exception, $context);
if (null === $visitor->document) {
$visitor->document = $visitor->createDocument(null, null, true);
}
foreach ($data as $key => $value) {
$entryNode = $visitor->document->createElement($key);
$visitor->getCurrentNode()->appendChild($entryNode);
$visitor->setCurrentNode($entryNode);
$node = $context->getNavigator()->accept($value, null, $context);
if (null !== $node) {
$visitor->getCurrentNode()->appendChild($node);
}
$visitor->revertCurrentNode();
}
}
/**
* @param \Exception $exception
*
* @return array
*/
protected function convertToArray(\Exception $exception, Context $context)
{
$data = [];
$templateData = $context->attributes->get('template_data');
if ($templateData->isDefined()) {
$data['code'] = $statusCode = $templateData->get()['status_code'];
}
$data['message'] = $this->getExceptionMessage($exception, isset($statusCode) ? $statusCode : null);
return $data;
}
}
Serializer/Normalizer/ExceptionNormalizer.php 0000666 00000002201 13052362131 0015504 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Serializer\Normalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Normalizes Exception instances.
*
* @author Ener-Getick
*/
class ExceptionNormalizer extends AbstractExceptionNormalizer implements NormalizerInterface
{
/**
* {@inheritdoc}
*/
public function normalize($object, $format = null, array $context = [])
{
$data = [];
if (isset($context['template_data']['status_code'])) {
$data['code'] = $statusCode = $context['template_data']['status_code'];
}
$data['message'] = $this->getExceptionMessage($object, isset($statusCode) ? $statusCode : null);
return $data;
}
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, $format = null)
{
return $data instanceof \Exception;
}
}
Serializer/Normalizer/FormErrorHandler.php 0000666 00000007347 13052362131 0014736 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Serializer\Normalizer;
use JMS\Serializer\Context;
use JMS\Serializer\Handler\FormErrorHandler as JMSFormErrorHandler;
use JMS\Serializer\JsonSerializationVisitor;
use JMS\Serializer\XmlSerializationVisitor;
use Symfony\Component\Form\Form;
use JMS\Serializer\YamlSerializationVisitor;
use Symfony\Component\Translation\TranslatorInterface;
/**
* Extend the JMS FormErrorHandler to include more informations when using the ViewHandler.
*/
class FormErrorHandler extends JMSFormErrorHandler
{
private $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
parent::__construct($translator);
}
public function serializeFormToXml(XmlSerializationVisitor $visitor, Form $form, array $type, Context $context = null)
{
if ($context) {
$statusCode = $context->attributes->get('status_code');
if ($statusCode->isDefined()) {
if (null === $visitor->document) {
$visitor->document = $visitor->createDocument(null, null, true);
}
$codeNode = $visitor->document->createElement('code');
$visitor->getCurrentNode()->appendChild($codeNode);
$codeNode->appendChild($context->getNavigator()->accept($statusCode->get(), null, $context));
$messageNode = $visitor->document->createElement('message');
$visitor->getCurrentNode()->appendChild($messageNode);
$messageNode->appendChild($context->getNavigator()->accept('Validation Failed', null, $context));
$errorsNode = $visitor->document->createElement('errors');
$visitor->getCurrentNode()->appendChild($errorsNode);
$visitor->setCurrentNode($errorsNode);
parent::serializeFormToXml($visitor, $form, $type);
$visitor->revertCurrentNode();
return;
}
}
return parent::serializeFormToXml($visitor, $form, $type);
}
public function serializeFormToJson(JsonSerializationVisitor $visitor, Form $form, array $type, Context $context = null)
{
$isRoot = null === $visitor->getRoot();
$result = $this->adaptFormArray(parent::serializeFormToJson($visitor, $form, $type), $context);
if ($isRoot) {
$visitor->setRoot($result);
}
return $result;
}
public function serializeFormToYml(YamlSerializationVisitor $visitor, Form $form, array $type, Context $context = null)
{
$isRoot = null === $visitor->getRoot();
$result = $this->adaptFormArray(parent::serializeFormToYml($visitor, $form, $type), $context);
if ($isRoot) {
$visitor->setRoot($result);
}
return $result;
}
private function adaptFormArray(\ArrayObject $serializedForm, Context $context = null)
{
$statusCode = $this->getStatusCode($context);
if (null !== $statusCode) {
return [
'code' => $statusCode,
'message' => 'Validation Failed',
'errors' => $serializedForm,
];
}
return $serializedForm;
}
private function getStatusCode(Context $context = null)
{
if (null === $context) {
return;
}
$statusCode = $context->attributes->get('status_code');
if ($statusCode->isDefined()) {
return $statusCode->get();
}
}
}
Serializer/Normalizer/FormErrorNormalizer.php 0000666 00000004642 13052362131 0015476 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Serializer\Normalizer;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Translation\TranslatorInterface;
/**
* Normalizes invalid Form instances.
*
* @author Ener-Getick
*/
class FormErrorNormalizer implements NormalizerInterface
{
private $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* {@inheritdoc}
*/
public function normalize($object, $format = null, array $context = [])
{
return [
'code' => isset($context['status_code']) ? $context['status_code'] : null,
'message' => 'Validation Failed',
'errors' => $this->convertFormToArray($object),
];
}
/**
* {@inheritdoc}
*/
public function supportsNormalization($data, $format = null)
{
return $data instanceof FormInterface && $data->isSubmitted() && !$data->isValid();
}
/**
* This code has been taken from JMSSerializer.
*/
private function convertFormToArray(FormInterface $data)
{
$form = $errors = [];
foreach ($data->getErrors() as $error) {
$errors[] = $this->getErrorMessage($error);
}
if ($errors) {
$form['errors'] = $errors;
}
$children = [];
foreach ($data->all() as $child) {
if ($child instanceof FormInterface) {
$children[$child->getName()] = $this->convertFormToArray($child);
}
}
if ($children) {
$form['children'] = $children;
}
return $form;
}
private function getErrorMessage(FormError $error)
{
if (null !== $error->getMessagePluralization()) {
return $this->translator->transChoice($error->getMessageTemplate(), $error->getMessagePluralization(), $error->getMessageParameters(), 'validators');
}
return $this->translator->trans($error->getMessageTemplate(), $error->getMessageParameters(), 'validators');
}
}
Serializer/Serializer.php 0000666 00000001543 13052362131 0011502 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Serializer;
use FOS\RestBundle\Context\Context;
/**
* @author Christian Flothmann
*/
interface Serializer
{
/**
* @param mixed $data
* @param string $format
* @param Context $context
*
* @return string
*/
public function serialize($data, $format, Context $context);
/**
* @param string $data
* @param string $type
* @param string $format
* @param Context $context
*
* @return mixed
*/
public function deserialize($data, $type, $format, Context $context);
}
Serializer/SymfonySerializerAdapter.php 0000666 00000003577 13052362131 0014401 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Serializer;
use FOS\RestBundle\Context\Context;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Adapter to plug the Symfony serializer into the FOSRestBundle Serializer API.
*
* @author Christian Flothmann
*/
class SymfonySerializerAdapter implements Serializer
{
private $serializer;
public function __construct(SerializerInterface $serializer)
{
$this->serializer = $serializer;
}
/**
* {@inheritdoc}
*/
public function serialize($data, $format, Context $context)
{
$newContext = $this->convertContext($context);
$newContext['serializeNull'] = $context->getSerializeNull();
return $this->serializer->serialize($data, $format, $newContext);
}
/**
* {@inheritdoc}
*/
public function deserialize($data, $type, $format, Context $context)
{
$newContext = $this->convertContext($context);
return $this->serializer->deserialize($data, $type, $format, $newContext);
}
/**
* @param Context $context
*/
private function convertContext(Context $context)
{
$newContext = array();
foreach ($context->getAttributes() as $key => $value) {
$newContext[$key] = $value;
}
if (null !== $context->getGroups()) {
$newContext['groups'] = $context->getGroups();
}
$newContext['version'] = $context->getVersion();
$newContext['maxDepth'] = $context->getMaxDepth(false);
$newContext['enable_max_depth'] = $context->isMaxDepthEnabled();
return $newContext;
}
}
Tests/Context/ContextTest.php 0000666 00000005456 13052362131 0012301 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Tests\Context;
use FOS\RestBundle\Context\Context;
/**
* @author Ener-Getick
*/
class ContextTest extends \PHPUnit_Framework_TestCase
{
protected $context;
public function setUp()
{
$this->context = new Context();
}
public function testDefaultValues()
{
$this->assertEquals([], $this->context->getAttributes());
$this->assertEquals(null, $this->context->getGroups());
}
public function testAttributes()
{
// Define attributes and check if that's the good return value.
$this->assertEquals($this->context, $this->context->setAttribute('foo', 'bar'));
$this->assertEquals($this->context, $this->context->setAttribute('foobar', 'foo'));
$this->assertTrue($this->context->hasAttribute('foo'));
$this->assertTrue($this->context->hasAttribute('foobar'));
$this->assertFalse($this->context->hasAttribute('bar'));
$this->assertEquals('bar', $this->context->getAttribute('foo'));
$this->assertEquals('foo', $this->context->getAttribute('foobar'));
$this->assertEquals(['foo' => 'bar', 'foobar' => 'foo'], $this->context->getAttributes());
}
public function testGroupAddition()
{
$this->context->addGroups(array('quz', 'foo'));
$this->context->addGroup('foo');
$this->context->addGroup('bar');
$this->assertEquals(['quz', 'foo', 'bar'], $this->context->getGroups());
}
public function testSetGroups()
{
$this->context->setGroups(array('quz', 'foo'));
$this->assertEquals(array('quz', 'foo'), $this->context->getGroups());
$this->context->setGroups(array('foo'));
$this->assertEquals(array('foo'), $this->context->getGroups());
}
public function testAlreadyExistentGroupAddition()
{
$this->context->addGroup('foo');
$this->context->addGroup('foo');
$this->context->addGroup('bar');
$this->assertEquals(array('foo', 'bar'), $this->context->getGroups());
}
public function testVersion()
{
$this->context->setVersion('1.3.2');
$this->assertEquals('1.3.2', $this->context->getVersion());
}
/**
* @group legacy
*/
public function testMaxDepth()
{
$this->context->setMaxDepth(10);
$this->assertEquals(10, $this->context->getMaxDepth());
}
public function testSerializeNull()
{
$this->context->setSerializeNull(true);
$this->assertEquals(true, $this->context->getSerializeNull());
}
}
Tests/Controller/Annotations/AbstractParamTest.php 0000666 00000004654 13052362131 0016374 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Tests\Controller\Annotations;
use FOS\RestBundle\Controller\Annotations;
use Symfony\Component\Validator\Constraints;
/**
* AbstractParamTest.
*
* @author Ener-Getick
*/
class AbstractParamTest extends \PHPUnit_Framework_TestCase
{
public function setUp()
{
$this->param = $this->getMockForAbstractClass(Annotations\AbstractParam::class);
}
public function testInterface()
{
$this->assertInstanceOf(Annotations\ParamInterface::class, $this->param);
}
public function testDefaultValues()
{
$this->assertEquals(null, $this->param->name);
$this->assertEquals(null, $this->param->key);
$this->assertEquals(null, $this->param->default);
$this->assertEquals(null, $this->param->description);
$this->assertEquals(false, $this->param->strict);
$this->assertEquals(false, $this->param->nullable);
$this->assertEquals(array(), $this->param->incompatibles);
}
public function testNameGetter()
{
$this->param->name = 'Foo';
$this->assertEquals('Foo', $this->param->getName());
}
public function testDefaultGetter()
{
$this->param->default = 'Bar';
$this->assertEquals('Bar', $this->param->getDefault());
$this->param->default = 'foo %parameter%';
$this->assertEquals('foo %parameter%', $this->param->getDefault());
}
public function testDescriptionGetter()
{
$this->param->description = 'Bar';
$this->assertEquals('Bar', $this->param->getDescription());
}
public function testIncompatiblesGetter()
{
$this->param->incompatibles = array('c', 'd');
$this->assertEquals(array('c', 'd'), $this->param->getIncompatibilities());
}
public function testStrictGetter()
{
$this->param->strict = true;
$this->assertTrue($this->param->isStrict());
}
public function testNotNullConstraint()
{
$this->assertEquals(array(new Constraints\NotNull()), $this->param->getConstraints(''));
$this->param->nullable = true;
$this->assertEquals(array(), $this->param->getConstraints());
}
}
Tests/Controller/Annotations/AbstractScalarParamTest.php 0000666 00000006335 13052362131 0017520 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Tests\Controller\Annotations;
use FOS\RestBundle\Validator\Constraints\Regex;
use Symfony\Component\Validator\Constraints;
/**
* AbstractScalarParamTest.
*
* @author Ener-Getick
*/
class AbstractScalarParamTest extends \PHPUnit_Framework_TestCase
{
public function setUp()
{
$this->param = $this->getMockForAbstractClass('FOS\RestBundle\Controller\Annotations\AbstractScalarParam');
}
public function testInterface()
{
$this->assertInstanceOf('FOS\RestBundle\Controller\Annotations\AbstractParam', $this->param);
}
public function testDefaultValues()
{
$this->assertEquals(null, $this->param->requirements);
$this->assertFalse($this->param->map);
$this->assertTrue($this->param->allowBlank);
}
public function testScalarConstraint()
{
$this->assertEquals(array(
new Constraints\NotNull(),
), $this->param->getConstraints());
}
public function testComplexRequirements()
{
$this->param->requirements = $requirement = $this->getMockBuilder('Symfony\Component\Validator\Constraint')->getMock();
$this->assertEquals(array(
new Constraints\NotNull(),
$requirement,
), $this->param->getConstraints());
}
public function testScalarRequirements()
{
$this->param->name = 'bar';
$this->param->requirements = 'foo %bar% %%';
$this->assertEquals(array(
new Constraints\NotNull(),
new Regex(array(
'pattern' => '#^(?:foo %bar% %%)$#xsu',
'message' => "Parameter 'bar' value, does not match requirements 'foo %bar% %%'",
)),
), $this->param->getConstraints());
}
public function testArrayRequirements()
{
$this->param->requirements = array(
'rule' => 'foo',
'error_message' => 'bar',
);
$this->assertEquals(array(
new Constraints\NotNull(),
new Regex(array(
'pattern' => '#^(?:foo)$#xsu',
'message' => 'bar',
)),
), $this->param->getConstraints());
}
public function testAllowBlank()
{
$this->param->allowBlank = false;
$this->assertEquals(array(
new Constraints\NotNull(),
new Constraints\NotBlank(),
), $this->param->getConstraints());
}
public function testConstraintsTransformWhenParamIsAnArray()
{
$this->param->map = true;
$this->assertEquals(array(new Constraints\All(array(
new Constraints\NotNull(),
))), $this->param->getConstraints());
}
public function testArrayWithNoConstraintsDoesNotCreateInvalidConstraint()
{
$this->param->nullable = true;
$this->param->map = true;
$this->assertEquals(array(new Constraints\All(array(
'constraints' => [],
))), $this->param->getConstraints());
}
}
Tests/Controller/Annotations/FileParamTest.php 0000666 00000006430 13052362131 0015502 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Tests\Controller\Annotations;
use Symfony\Component\Validator\Constraints;
/**
* FileParamTest.
*
* @author Ener-Getick
*/
class FileParamTest extends \PHPUnit_Framework_TestCase
{
public function setUp()
{
$this->param = $this->getMockBuilder('FOS\RestBundle\Controller\Annotations\FileParam')
->setMethods(array('getKey'))
->getMock();
}
public function testInterface()
{
$this->assertInstanceOf('FOS\RestBundle\Controller\Annotations\AbstractParam', $this->param);
}
public function testValueGetter()
{
$this->param
->expects($this->once())
->method('getKey')
->willReturn('foo');
$request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->getMock();
$parameterBag = $this->getMockBuilder('Symfony\Component\HttpFoundation\ParameterBag')->getMock();
$parameterBag
->expects($this->once())
->method('get')
->with('foo', 'bar')
->willReturn('foobar');
$request->files = $parameterBag;
$this->assertEquals('foobar', $this->param->getValue($request, 'bar'));
}
public function testComplexRequirements()
{
$this->param->requirements = $requirement = $this->getMockBuilder('Symfony\Component\Validator\Constraint')->getMock();
$this->assertEquals(array(
new Constraints\NotNull(),
$requirement,
new Constraints\File(),
), $this->param->getConstraints());
}
public function testFileRequirements()
{
$this->param->nullable = true;
$this->param->requirements = $requirements = ['mimeTypes' => 'application/json'];
$this->assertEquals(array(
new Constraints\File($requirements),
), $this->param->getConstraints());
}
public function testImageRequirements()
{
$this->param->image = true;
$this->param->requirements = $requirements = ['mimeTypes' => 'image/gif'];
$this->assertEquals(array(
new Constraints\NotNull(),
new Constraints\Image($requirements),
), $this->param->getConstraints());
}
public function testImageConstraintsTransformWhenParamIsAnArray()
{
$this->param->image = true;
$this->param->map = true;
$this->param->requirements = $requirements = ['mimeTypes' => 'image/gif'];
$this->assertEquals(array(new Constraints\All(array(
new Constraints\NotNull(),
new Constraints\Image($requirements),
))), $this->param->getConstraints());
}
public function testFileConstraintsWhenParamIsAnArray()
{
$this->param->map = true;
$this->param->requirements = $requirements = ['mimeTypes' => 'application/pdf'];
$this->assertEquals(array(new Constraints\All(array(
new Constraints\NotNull(),
new Constraints\File($requirements),
))), $this->param->getConstraints());
}
}
Tests/Controller/Annotations/QueryParamTest.php 0000666 00000002755 13052362131 0015736 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Tests\Controller\Annotations;
/**
* QueryParamTest.
*
* @author Eduardo Oliveira
* @author Ener-Getick
*/
class QueryParamTest extends \PHPUnit_Framework_TestCase
{
public function setUp()
{
$this->param = $this->getMockBuilder('FOS\RestBundle\Controller\Annotations\QueryParam')
->setMethods(array('getKey'))
->getMock();
}
public function testInterface()
{
$this->assertInstanceOf('FOS\RestBundle\Controller\Annotations\AbstractScalarParam', $this->param);
}
public function testValueGetter()
{
$this->param
->expects($this->once())
->method('getKey')
->willReturn('foo');
$request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->getMock();
$parameterBag = $this->getMockBuilder('Symfony\Component\HttpFoundation\ParameterBag')->getMock();
$parameterBag
->expects($this->once())
->method('get')
->with('foo', 'bar')
->willReturn('foobar');
$request->query = $parameterBag;
$this->assertEquals('foobar', $this->param->getValue($request, 'bar'));
}
}
Tests/Controller/Annotations/RequestParamTest.php 0000666 00000002765 13052362131 0016262 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Tests\Controller\Annotations;
/**
* RequestParamTest.
*
* @author Eduardo Oliveira
* @author Ener-Getick
*/
class RequestParamTest extends \PHPUnit_Framework_TestCase
{
public function setUp()
{
$this->param = $this->getMockBuilder('FOS\RestBundle\Controller\Annotations\RequestParam')
->setMethods(array('getKey'))
->getMock();
}
public function testInterface()
{
$this->assertInstanceOf('FOS\RestBundle\Controller\Annotations\AbstractScalarParam', $this->param);
}
public function testValueGetter()
{
$this->param
->expects($this->once())
->method('getKey')
->willReturn('foo');
$request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->getMock();
$parameterBag = $this->getMockBuilder('Symfony\Component\HttpFoundation\ParameterBag')->getMock();
$parameterBag
->expects($this->once())
->method('get')
->with('foo', 'bar')
->willReturn('foobar');
$request->request = $parameterBag;
$this->assertEquals('foobar', $this->param->getValue($request, 'bar'));
}
}
Tests/Decoder/JsonToFormDecoderTest.php 0000666 00000002726 13052362131 0014121 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Tests\Decoder;
use FOS\RestBundle\Decoder\JsonToFormDecoder;
/**
* Tests the form-like encoder.
*
* @author Kévin Dunglas
*/
class JsonToFormDecoderTest extends \PHPUnit_Framework_TestCase
{
public function testDecodeWithRemovingFalseData()
{
$data = [
'arrayKey' => [
'falseKey' => false,
'stringKey' => 'foo',
],
'falseKey' => false,
'trueKey' => true,
'intKey' => 69,
'floatKey' => 3.14,
'stringKey' => 'bar',
];
$decoder = new JsonToFormDecoder();
$decoded = $decoder->decode(json_encode($data));
$this->assertTrue(is_array($decoded));
$this->assertTrue(is_array($decoded['arrayKey']));
$this->assertNull($decoded['arrayKey']['falseKey']);
$this->assertEquals('foo', $decoded['arrayKey']['stringKey']);
$this->assertNull($decoded['falseKey']);
$this->assertEquals('1', $decoded['trueKey']);
$this->assertEquals('69', $decoded['intKey']);
$this->assertEquals('3.14', $decoded['floatKey']);
$this->assertEquals('bar', $decoded['stringKey']);
}
}
Tests/DependencyInjection/Compiler/ConfigurationCheckPassTest.php 0000666 00000003226 13052362131 0021311 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Tests\DependencyInjection\Compiler;
use FOS\RestBundle\DependencyInjection\Compiler\ConfigurationCheckPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* ConfigurationCheckPass test.
*
* @author Eriksen Costa
*/
class ConfigurationCheckPassTest extends \PHPUnit_Framework_TestCase
{
/**
* @expectedException RuntimeException
* @expectedExceptionMessage You need to enable the parameter converter listeners in SensioFrameworkExtraBundle when using the FOSRestBundle RequestBodyParamConverter
*/
public function testShouldThrowRuntimeExceptionWhenBodyConverterIsEnabledButParamConvertersAreNotEnabled()
{
$container = new ContainerBuilder();
$container->register('fos_rest.converter.request_body');
$compiler = new ConfigurationCheckPass();
$compiler->process($container);
}
/**
* @expectedException RuntimeException
* @expectedExceptionMessage SensioFrameworkExtraBundle view annotations
*/
public function testExceptionWhenViewAnnotationsAreNotEnabled()
{
$container = new ContainerBuilder();
$container->register('fos_rest.view_response_listener');
$container->setParameter('kernel.bundles', ['SensioFrameworkExtraBundle' => '']);
$compiler = new ConfigurationCheckPass();
$compiler->process($container);
}
}
Tests/DependencyInjection/Compiler/FormatListenerRulesPassTest.php 0000666 00000007440 13052362131 0021517 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Tests\DependencyInjection\Compiler;
use FOS\RestBundle\DependencyInjection\Compiler\FormatListenerRulesPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* @author Eduardo Gulias Davis
*/
class FormatListenerRulesPassTest extends \PHPUnit_Framework_TestCase
{
public function testRulesAreAddedWhenFormatListenerAndProfilerToolbarAreEnabled()
{
$definition = $this->getMockBuilder('Symfony\Component\DependencyInjection\Definition')
->setMethods(array('addMethod'))
->getMock();
$container = $this->getMockBuilder(ContainerBuilder::class)
->setMethods(['hasDefinition', 'getDefinition', 'hasParameter', 'getParameter'])
->getMock();
$container->expects($this->exactly(3))
->method('hasDefinition')
->will($this->returnValue(true));
$container->expects($this->exactly(1))
->method('hasParameter')
->with('web_profiler.debug_toolbar.mode')
->will($this->returnValue(true));
$container->expects($this->exactly(2))
->method('getParameter')
->will($this->onConsecutiveCalls(
2,
[
[
'host' => null,
'methods' => null,
'path' => '^/',
'priorities' => ['html', 'json'],
'fallback_format' => 'html',
'attributes' => [],
'prefer_extension' => true,
],
]
)
);
$container->expects($this->exactly(2))
->method('getDefinition')
->with($this->equalTo('fos_rest.format_negotiator'))
->willReturn($definition);
$compiler = new FormatListenerRulesPass();
$compiler->process($container);
}
public function testNoRulesAreAddedWhenProfilerToolbarAreDisabled()
{
$definition = $this->getMockBuilder('Symfony\Component\DependencyInjection\Definition')
->setMethods(array('addMethod'))
->getMock();
$container = $this->getMockBuilder(ContainerBuilder::class)
->setMethods(['hasDefinition', 'getDefinition', 'hasParameter', 'getParameter'])
->getMock();
$container->expects($this->exactly(2))
->method('hasDefinition')
->will($this->returnValue(true));
$container->expects($this->exactly(1))
->method('hasParameter')
->with('web_profiler.debug_toolbar.mode')
->will($this->returnValue(false));
$container->expects($this->once())
->method('getParameter')
->with('fos_rest.format_listener.rules')
->will($this->returnValue(
[
[
'host' => null,
'methods' => null,
'path' => '^/',
'priorities' => ['html', 'json'],
'fallback_format' => 'html',
'attributes' => [],
'prefer_extension' => true,
],
])
);
$container->expects($this->once())
->method('getDefinition')
->with($this->equalTo('fos_rest.format_negotiator'))
->will($this->returnValue($definition));
$compiler = new FormatListenerRulesPass();
$compiler->process($container);
}
}
Tests/DependencyInjection/Compiler/SerializerConfigurationPassTest.php 0000666 00000006535 13052362131 0022413 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Tests\DependencyInjection\Compiler;
use FOS\RestBundle\DependencyInjection\Compiler\SerializerConfigurationPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* SerializerConfigurationPassTest test.
*/
class SerializerConfigurationPassTest extends \PHPUnit_Framework_TestCase
{
/**
* @var ContainerBuilder
*/
private $container;
protected function setUp()
{
$this->container = new ContainerBuilder();
}
public function testShouldDoNothingIfSerializerIsFound()
{
$serializer = $this->getMockBuilder('FOS\RestBundle\Serializer\Serializer')->getMock();
$this->container->register('fos_rest.serializer', get_class($serializer));
$compiler = new SerializerConfigurationPass();
$compiler->process($this->container);
$this->assertSame(get_class($serializer), $this->container->getDefinition('fos_rest.serializer')->getClass());
}
/**
* @expectedException \InvalidArgumentException
*/
public function testShouldThrowInvalidArgumentExceptionWhenNoSerializerIsFound()
{
$compiler = new SerializerConfigurationPass();
$compiler->process($this->container);
}
public function testShouldConfigureJMSSerializer()
{
$this->container->register('jms_serializer.serializer', 'JMS\Serializer\Serializer');
$compiler = new SerializerConfigurationPass();
$compiler->process($this->container);
$this->assertSame('fos_rest.serializer.jms', (string) $this->container->getAlias('fos_rest.serializer'));
}
public function testShouldConfigureCoreSerializer()
{
$this->container->register('serializer', 'Symfony\Component\Serializer\Serializer');
$this->container->register('fos_rest.serializer.exception_wrapper_serialize_handler');
$compiler = new SerializerConfigurationPass();
$compiler->process($this->container);
$this->assertSame('fos_rest.serializer.symfony', (string) $this->container->getAlias('fos_rest.serializer'));
$this->assertTrue(!$this->container->has('fos_rest.serializer.exception_wrapper_serialize_handler'));
}
public function testSerializerServiceSupersedesJmsSerializerService()
{
$this->container->register('jms_serializer.serializer', 'JMS\Serializer\Serializer');
$this->container->register('serializer', 'Symfony\Component\Serializer\Serializer');
$compiler = new SerializerConfigurationPass();
$compiler->process($this->container);
$this->assertSame('fos_rest.serializer.symfony', (string) $this->container->getAlias('fos_rest.serializer'));
}
public function testSerializerServiceCanBeJmsSerializer()
{
$this->container->register('jms_serializer.serializer', 'JMS\Serializer\Serializer');
$this->container->register('serializer', 'JMS\Serializer\Serializer');
$compiler = new SerializerConfigurationPass();
$compiler->process($this->container);
$this->assertSame('fos_rest.serializer.jms', (string) $this->container->getAlias('fos_rest.serializer'));
}
}
Tests/DependencyInjection/FOSRestExtensionTest.php 0000666 00000061104 13052362131 0016324 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Tests\DependencyInjection;
use FOS\RestBundle\DependencyInjection\FOSRestExtension;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\Reference;
/**
* FOSRestExtension test.
*
* @author Bulat Shakirzyanov
* @author Konstantin Kudryashov
*/
class FOSRestExtensionTest extends \PHPUnit_Framework_TestCase
{
/**
* @var ContainerBuilder
*/
private $container;
/**
* @var FOSRestExtension
*/
private $extension;
/**
* @var bool
*/
private $includeFormat;
/**
* @var array
*/
private $formats;
/**
* @var string
*/
private $defaultFormat;
public function setUp()
{
$this->container = new ContainerBuilder();
$this->container->setParameter('kernel.bundles', array('JMSSerializerBundle' => true));
$this->container->setParameter('kernel.debug', false);
$this->extension = new FOSRestExtension();
$this->includeFormat = true;
$this->formats = [
'json' => false,
'xml' => false,
'html' => true,
];
$this->defaultFormat = null;
}
public function tearDown()
{
unset($this->container, $this->extension);
}
public function testDisableBodyListener()
{
$config = [
'fos_rest' => ['body_listener' => false],
];
$this->extension->load($config, $this->container);
$this->assertFalse($this->container->hasDefinition('fos_rest.body_listener'));
}
public function testLoadBodyListenerWithDefaults()
{
$this->extension->load([], $this->container);
$decoders = [
'json' => 'fos_rest.decoder.json',
'xml' => 'fos_rest.decoder.xml',
];
$this->assertTrue($this->container->hasDefinition('fos_rest.body_listener'));
$this->assertEquals($decoders, $this->container->getDefinition('fos_rest.decoder_provider')->getArgument(1));
$this->assertFalse($this->container->getDefinition('fos_rest.body_listener')->getArgument(1));
$this->assertCount(2, $this->container->getDefinition('fos_rest.body_listener')->getArguments());
}
public function testLoadBodyListenerWithNormalizerString()
{
$config = [
'fos_rest' => ['body_listener' => [
'array_normalizer' => 'fos_rest.normalizer.camel_keys',
]],
];
$this->extension->load($config, $this->container);
$normalizerArgument = $this->container->getDefinition('fos_rest.body_listener')->getArgument(2);
$this->assertInstanceOf(Reference::class, $normalizerArgument);
$this->assertEquals('fos_rest.normalizer.camel_keys', (string) $normalizerArgument);
}
public function testLoadBodyListenerWithNormalizerArray()
{
$config = [
'fos_rest' => ['body_listener' => [
'array_normalizer' => [
'service' => 'fos_rest.normalizer.camel_keys',
],
]],
];
$this->extension->load($config, $this->container);
$bodyListener = $this->container->getDefinition('fos_rest.body_listener');
$normalizerArgument = $bodyListener->getArgument(2);
$normalizeForms = $bodyListener->getArgument(3);
$this->assertCount(4, $bodyListener->getArguments());
$this->assertInstanceOf(Reference::class, $normalizerArgument);
$this->assertEquals('fos_rest.normalizer.camel_keys', (string) $normalizerArgument);
$this->assertEquals(false, $normalizeForms);
}
public function testLoadBodyListenerWithNormalizerArrayAndForms()
{
$config = [
'fos_rest' => ['body_listener' => [
'array_normalizer' => [
'service' => 'fos_rest.normalizer.camel_keys',
'forms' => true,
],
]],
];
$this->extension->load($config, $this->container);
$bodyListener = $this->container->getDefinition('fos_rest.body_listener');
$normalizerArgument = $bodyListener->getArgument(2);
$normalizeForms = $bodyListener->getArgument(3);
$this->assertCount(4, $bodyListener->getArguments());
$this->assertInstanceOf(Reference::class, $normalizerArgument);
$this->assertEquals('fos_rest.normalizer.camel_keys', (string) $normalizerArgument);
$this->assertEquals(true, $normalizeForms);
}
public function testDisableFormatListener()
{
$config = [
'fos_rest' => ['format_listener' => false],
];
$this->extension->load($config, $this->container);
$this->assertFalse($this->container->hasDefinition('fos_rest.format_listener'));
}
public function testLoadFormatListenerWithDefaults()
{
$this->extension->load([], $this->container);
$this->assertFalse($this->container->hasDefinition('fos_rest.format_listener'));
}
public function testLoadFormatListenerWithSingleRule()
{
$config = [
'fos_rest' => ['format_listener' => [
'rules' => ['path' => '/'],
]],
];
$this->extension->load($config, $this->container);
$this->assertTrue($this->container->hasDefinition('fos_rest.format_listener'));
}
public function testLoadParamFetcherListener()
{
$config = [
'fos_rest' => ['param_fetcher_listener' => true],
];
$this->extension->load($config, $this->container);
$this->assertTrue($this->container->hasDefinition('fos_rest.param_fetcher_listener'));
$this->assertFalse($this->container->getDefinition('fos_rest.param_fetcher_listener')->getArgument(1));
}
public function testLoadParamFetcherListenerForce()
{
$config = [
'fos_rest' => ['param_fetcher_listener' => 'force'],
];
$this->extension->load($config, $this->container);
$this->assertTrue($this->container->hasDefinition('fos_rest.param_fetcher_listener'));
$this->assertTrue($this->container->getDefinition('fos_rest.param_fetcher_listener')->getArgument(1));
}
public function testLoadFormatListenerWithMultipleRule()
{
$config = [
'fos_rest' => ['format_listener' => [
'rules' => [
['path' => '/foo'],
['path' => '/'],
],
]],
];
$this->extension->load($config, $this->container);
$this->assertTrue($this->container->hasDefinition('fos_rest.format_listener'));
}
/**
* @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
*/
public function testLoadFormatListenerMediaTypeNoRules()
{
$config = [
'fos_rest' => ['format_listener' => [
'media_type' => true,
]],
];
$this->extension->load($config, $this->container);
}
public function testLoadServicesWithDefaults()
{
$this->extension->load([], $this->container);
$this->assertAlias('fos_rest.view_handler.default', 'fos_rest.view_handler');
}
public function testDisableViewResponseListener()
{
$config = [
'fos_rest' => ['view' => ['view_response_listener' => false]],
];
$this->extension->load($config, $this->container);
$this->assertFalse($this->container->hasDefinition('fos_rest.view_response_listener'));
}
public function testLoadViewResponseListener()
{
$config = [
'fos_rest' => ['view' => ['view_response_listener' => true]],
];
$this->extension->load($config, $this->container);
$this->assertTrue($this->container->hasDefinition('fos_rest.view_response_listener'));
$this->assertFalse($this->container->getDefinition('fos_rest.view_response_listener')->getArgument(1));
}
public function testLoadViewResponseListenerForce()
{
$config = [
'fos_rest' => ['view' => ['view_response_listener' => 'force']],
];
$this->extension->load($config, $this->container);
$this->assertTrue($this->container->hasDefinition('fos_rest.view_response_listener'));
$this->assertTrue($this->container->getDefinition('fos_rest.view_response_listener')->getArgument(1));
}
public function testForceEmptyContentDefault()
{
$this->extension->load([], $this->container);
$this->assertEquals(204, $this->container->getDefinition('fos_rest.view_handler.default')->getArgument(6));
}
public function testForceEmptyContentIs200()
{
$config = ['fos_rest' => ['view' => ['empty_content' => 200]]];
$this->extension->load($config, $this->container);
$this->assertEquals(200, $this->container->getDefinition('fos_rest.view_handler.default')->getArgument(6));
}
public function testViewSerializeNullDefault()
{
$this->extension->load([], $this->container);
$this->assertFalse($this->container->getDefinition('fos_rest.view_handler.default')->getArgument(7));
}
public function testViewSerializeNullIsTrue()
{
$config = ['fos_rest' => ['view' => ['serialize_null' => true]]];
$this->extension->load($config, $this->container);
$this->assertTrue($this->container->getDefinition('fos_rest.view_handler.default')->getArgument(7));
}
public function testValidatorAliasWhenEnabled()
{
$config = ['fos_rest' => ['body_converter' => ['validate' => true]]];
$this->extension->load($config, $this->container);
$this->assertAlias('validator', 'fos_rest.validator');
}
public function testValidatorAliasWhenDisabled()
{
$config = ['fos_rest' => ['body_converter' => ['validate' => false]]];
$this->extension->load($config, $this->container);
$this->assertFalse($this->container->has('fos_rest.validator'));
}
public function testBodyConvertorDisabledAndSerializerVersionGiven()
{
$config = ['fos_rest' => ['body_converter' => ['enabled' => false], 'serializer' => ['version' => '1.0']]];
$this->extension->load($config, $this->container);
}
public function testBodyConvertorDisabledAndSerializerGroupsGiven()
{
$config = ['fos_rest' => ['body_converter' => ['enabled' => false], 'serializer' => ['groups' => ['Default']]]];
$this->extension->load($config, $this->container);
}
/**
* Test that extension loads properly.
*/
public function testConfigLoad()
{
$controllerLoaderDefinitionName = 'fos_rest.routing.loader.controller';
$controllerLoaderClass = 'FOS\RestBundle\Routing\Loader\RestRouteLoader';
$yamlCollectionLoaderDefinitionName = 'fos_rest.routing.loader.yaml_collection';
$yamlCollectionLoaderClass = 'FOS\RestBundle\Routing\Loader\RestYamlCollectionLoader';
$xmlCollectionLoaderDefinitionName = 'fos_rest.routing.loader.xml_collection';
$xmlCollectionLoaderClass = 'FOS\RestBundle\Routing\Loader\RestXmlCollectionLoader';
$this->extension->load([], $this->container);
$this->assertTrue($this->container->hasDefinition($controllerLoaderDefinitionName));
$this->assertValidRestRouteLoader($this->container->getDefinition($controllerLoaderDefinitionName));
$this->assertTrue($this->container->hasDefinition($yamlCollectionLoaderDefinitionName));
$this->assertValidRestFileLoader(
$this->container->getDefinition($yamlCollectionLoaderDefinitionName),
$this->includeFormat,
$this->formats,
$this->defaultFormat
);
$this->assertTrue($this->container->hasDefinition($xmlCollectionLoaderDefinitionName));
$this->assertValidRestFileLoader(
$this->container->getDefinition($xmlCollectionLoaderDefinitionName),
$this->includeFormat,
$this->formats,
$this->defaultFormat
);
}
public function testIncludeFormatDisabled()
{
$this->extension->load(
[
'fos_rest' => [
'routing_loader' => [
'include_format' => false,
],
],
],
$this->container
);
$yamlCollectionLoaderDefinitionName = 'fos_rest.routing.loader.yaml_collection';
$this->assertValidRestFileLoader(
$this->container->getDefinition($yamlCollectionLoaderDefinitionName),
false,
$this->formats,
$this->defaultFormat
);
$xmlCollectionLoaderDefinitionName = 'fos_rest.routing.loader.xml_collection';
$this->assertValidRestFileLoader(
$this->container->getDefinition($xmlCollectionLoaderDefinitionName),
false,
$this->formats,
$this->defaultFormat
);
}
public function testDefaultFormat()
{
$this->extension->load(
[
'fos_rest' => [
'routing_loader' => [
'default_format' => 'xml',
],
],
],
$this->container
);
$yamlCollectionLoaderDefinitionName = 'fos_rest.routing.loader.yaml_collection';
$this->assertValidRestFileLoader(
$this->container->getDefinition($yamlCollectionLoaderDefinitionName),
$this->includeFormat,
$this->formats,
'xml'
);
$xmlCollectionLoaderDefinitionName = 'fos_rest.routing.loader.xml_collection';
$this->assertValidRestFileLoader(
$this->container->getDefinition($xmlCollectionLoaderDefinitionName),
$this->includeFormat,
$this->formats,
'xml'
);
}
public function testFormats()
{
$this->extension->load(
[
'fos_rest' => [
'view' => [
'formats' => [
'json' => false,
'xml' => true,
],
],
],
],
$this->container
);
$yamlCollectionLoaderDefinitionName = 'fos_rest.routing.loader.yaml_collection';
$this->assertValidRestFileLoader(
$this->container->getDefinition($yamlCollectionLoaderDefinitionName),
$this->includeFormat,
[
'xml' => false,
'html' => true,
],
$this->defaultFormat
);
$xmlCollectionLoaderDefinitionName = 'fos_rest.routing.loader.xml_collection';
$this->assertValidRestFileLoader(
$this->container->getDefinition($xmlCollectionLoaderDefinitionName),
$this->includeFormat,
[
'xml' => false,
'html' => true,
],
$this->defaultFormat
);
}
/**
* @expectedException \InvalidArgumentException
*/
public function testLoadBadClassThrowsException()
{
$this->extension->load([
'fos_rest' => [
'exception' => [
'messages' => [
'UnknownException' => true,
],
],
],
], $this->container);
}
/**
* @expectedException \InvalidArgumentException
* @expectedExceptionMessage Could not load class 'UnknownException' or the class does not extend from '\Exception'
*/
public function testLoadBadMessagesClassThrowsException()
{
$this->extension->load([
'fos_rest' => [
'exception' => [
'codes' => [
'UnknownException' => 404,
],
],
],
], $this->container);
}
public function testLoadOkMessagesClass()
{
$this->extension->load([
'fos_rest' => [
'exception' => [
'codes' => [
'Exception' => 404,
],
],
],
], $this->container);
$this->assertFalse($this->container->hasDefinition('fos_rest.exception.codes'));
}
/**
* @dataProvider getLoadBadCodeValueThrowsExceptionData
*
* @expectedException Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
* @expectedExceptionMessage Invalid HTTP code in fos_rest.exception.codes
*/
public function testLoadBadCodeValueThrowsException($value)
{
$this->extension->load([
'fos_rest' => [
'exception' => [
'codes' => [
'Exception' => $value,
],
],
],
], $this->container);
}
public function getLoadBadCodeValueThrowsExceptionData()
{
$data = [
null,
'HTTP_NOT_EXISTS',
'some random string',
true,
];
return array_map(function ($i) {
return [$i];
}, $data);
}
/**
* Test exception.debug config value uses kernel.debug value by default or provided value.
*
* @dataProvider getShowExceptionData
*
* @param bool $kernelDebug kernel.debug param value
* @param array $exceptionConfig Exception config
* @param bool|string $expectedValue Expected value of show_exception argument
*/
public function testExceptionDebug($kernelDebug, $exceptionConfig, $expectedValue)
{
$this->container->setParameter('kernel.debug', $kernelDebug);
$extension = new FOSRestExtension();
$extension->load(array(
'fos_rest' => array(
'exception' => $exceptionConfig,
),
), $this->container);
$definition = $this->container->getDefinition('fos_rest.exception.controller');
$this->assertSame($expectedValue, $definition->getArgument(2));
$definition = $this->container->getDefinition('fos_rest.serializer.exception_normalizer.jms');
$this->assertSame($expectedValue, $definition->getArgument(1));
$definition = $this->container->getDefinition('fos_rest.serializer.exception_normalizer.symfony');
$this->assertSame($expectedValue, $definition->getArgument(1));
}
public static function getShowExceptionData()
{
return array(
'empty config, kernel.debug is true' => array(
true,
array(),
true,
),
'empty config, kernel.debug is false' => array(
false,
array(),
false,
),
'config debug true' => array(
false,
array('debug' => true),
true,
),
'config debug false' => array(
true,
array('debug' => false),
false,
),
'config debug null, kernel.debug true' => array(
false,
array('debug' => null),
true,
),
'config debug null, kernel.debug false' => array(
false,
array('debug' => null),
true,
),
);
}
public function testGetConfiguration()
{
$configuration = $this->extension->getConfiguration(array(), $this->container);
$this->assertInstanceOf('FOS\RestBundle\DependencyInjection\Configuration', $configuration);
}
/**
* Assert that loader definition described properly.
*
* @param Definition $loader loader definition
*/
private function assertValidRestRouteLoader(Definition $loader)
{
$arguments = $loader->getArguments();
$this->assertEquals(5, count($arguments));
$this->assertEquals('service_container', (string) $arguments[0]);
$this->assertEquals('file_locator', (string) $arguments[1]);
$this->assertEquals('controller_name_converter', (string) $arguments[2]);
$this->assertEquals('fos_rest.routing.loader.reader.controller', (string) $arguments[3]);
$this->assertNull($arguments[4]);
$this->assertArrayHasKey('routing.loader', $loader->getTags());
}
/**
* Assert that loader definition described properly.
*
* @param Definition $loader loader definition
* @param bool $includeFormat whether or not the requested view format must be included in the route path
* @param string[] $formats supported view formats
* @param string $defaultFormat default view format
*/
private function assertValidRestFileLoader(
Definition $loader,
$includeFormat,
array $formats,
$defaultFormat
) {
$locatorRef = new Reference('file_locator');
$processorRef = new Reference('fos_rest.routing.loader.processor');
$arguments = $loader->getArguments();
$this->assertEquals(5, count($arguments));
$this->assertEquals($locatorRef, $arguments[0]);
$this->assertEquals($processorRef, $arguments[1]);
$this->assertSame($includeFormat, $arguments[2]);
$this->assertEquals($formats, $arguments[3]);
$this->assertSame($defaultFormat, $arguments[4]);
$this->assertArrayHasKey('routing.loader', $loader->getTags());
}
private function assertAlias($value, $key)
{
$this->assertEquals($value, (string) $this->container->getAlias($key), sprintf('%s alias is correct', $key));
}
public function testCheckViewHandlerWithJsonp()
{
$this->extension->load(['fos_rest' => ['view' => ['jsonp_handler' => null]]], $this->container);
$this->assertTrue($this->container->has('fos_rest.view_handler'));
$viewHandler = $this->container->getDefinition('fos_rest.view_handler');
$this->assertInstanceOf(DefinitionDecorator::class, $viewHandler);
}
public function testSerializerExceptionNormalizer()
{
$this->extension->load(['fos_rest' => ['exception' => true]], $this->container);
$this->assertTrue($this->container->has('fos_rest.serializer.exception_normalizer.symfony'));
}
public function testZoneMatcherListenerDefault()
{
$this->extension->load(array('fos_rest' => array()), $this->container);
$this->assertFalse($this->container->has('fos_rest.zone_matcher_listener'));
}
public function testZoneMatcherListener()
{
$config = array('fos_rest' => array(
'zone' => array(
'first' => array('path' => '/api/*'),
'second' => array('path' => '/^second', 'ips' => '127.0.0.1'),
),
));
$this->extension->load($config, $this->container);
$zoneMatcherListener = $this->container->getDefinition('fos_rest.zone_matcher_listener');
$addRequestMatcherCalls = $zoneMatcherListener->getMethodCalls();
$this->assertTrue($this->container->has('fos_rest.zone_matcher_listener'));
$this->assertEquals('FOS\RestBundle\EventListener\ZoneMatcherListener', $zoneMatcherListener->getClass());
$this->assertCount(2, $addRequestMatcherCalls);
// First zone
$this->assertEquals('addRequestMatcher', $addRequestMatcherCalls[0][0]);
$requestMatcherFirstId = (string) $addRequestMatcherCalls[0][1][0];
$requestMatcherFirst = $this->container->getDefinition($requestMatcherFirstId);
$this->assertInstanceOf('Symfony\Component\DependencyInjection\DefinitionDecorator', $requestMatcherFirst);
$this->assertEquals('/api/*', $requestMatcherFirst->getArgument(0));
// Second zone
$this->assertEquals('addRequestMatcher', $addRequestMatcherCalls[1][0]);
$requestMatcherSecondId = (string) $addRequestMatcherCalls[1][1][0];
$requestMatcherSecond = $this->container->getDefinition($requestMatcherSecondId);
$this->assertInstanceOf('Symfony\Component\DependencyInjection\DefinitionDecorator', $requestMatcherSecond);
$this->assertEquals('/^second', $requestMatcherSecond->getArgument(0));
$this->assertEquals(array('127.0.0.1'), $requestMatcherSecond->getArgument(3));
}
}
Tests/EventListener/AccessDeniedListenerTest.php 0000666 00000016624 13052362131 0016037 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Tests\EventListener;
use FOS\RestBundle\EventListener\AccessDeniedListener;
use FOS\RestBundle\FOSRestBundle;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
/**
* AccessDeniedListenerTest.
*
* @author Boris Guéry
*/
class AccessDeniedListenerTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider getFormatsDataProvider
*
* @param array $formats
* @param string $format
*/
public function testAccessDeniedExceptionIsConvertedToAnAccessDeniedHttpExceptionForFormat(array $formats, $format)
{
$request = new Request();
$request->setRequestFormat($format);
$this->doTestAccessDeniedExceptionIsConvertedToAnAccessDeniedHttpExceptionForRequest($request, $formats);
}
/**
* @dataProvider getFormatsDataProvider
*
* @param array $formats
* @param string $format
*/
public function testAccessDeniedExceptionIsConvertedToAnAccessDeniedHttpExceptionForFormatNoZone(array $formats, $format)
{
$request = new Request();
$request->attributes->set(FOSRestBundle::ZONE_ATTRIBUTE, false);
$request->setRequestFormat($format);
$this->doTestAccessDeniedExceptionIsConvertedToAnAccessDeniedHttpExceptionForRequest($request, $formats, 'Symfony\Component\Security\Core\Exception\AccessDeniedException');
}
/**
* @dataProvider getContentTypesDataProvider
*
* @param array $formats
* @param string $contentType
*/
public function testAccessDeniedExceptionIsConvertedToAnAccessDeniedHttpExceptionForContentType(array $formats, $contentType)
{
$request = new Request();
$request->headers->set('Content-Type', $contentType);
$this->doTestAccessDeniedExceptionIsConvertedToAnAccessDeniedHttpExceptionForRequest($request, $formats);
}
/**
* @param Request $request
* @param array $formats
*/
private function doTestAccessDeniedExceptionIsConvertedToAnAccessDeniedHttpExceptionForRequest(Request $request, array $formats, $exceptionClass = 'Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException')
{
$exception = new AccessDeniedException();
$event = new GetResponseForExceptionEvent(new TestKernel(), $request, 'foo', $exception);
$listener = new AccessDeniedListener($formats, null, 'foo');
// store the current error_log, and disable it temporarily
$errorLog = ini_set('error_log', file_exists('/dev/null') ? '/dev/null' : 'nul');
$listener->onKernelException($event);
// restore the old error_log
ini_set('error_log', $errorLog);
$this->assertInstanceOf($exceptionClass, $event->getException());
}
/**
* @dataProvider getFormatsDataProvider
*
* @param array $formats
*/
public function testCommonExceptionsAreBypassed($formats)
{
$request = new Request();
$request->setRequestFormat(key($formats));
$exception = new \Exception('foo');
$event = new GetResponseForExceptionEvent(new TestKernel(), $request, 'foo', $exception);
$listener = new AccessDeniedListener($formats, null, 'foo');
$listener->onKernelException($event);
$this->assertSame($exception, $event->getException());
}
/**
* @dataProvider getFormatsDataProvider
*
* @param array $formats
* @param string $format
*/
public function testAuthenticationExceptionIsConvertedToAnAccessDeniedHttpExceptionForFormat(array $formats, $format)
{
$request = new Request();
$request->setRequestFormat($format);
$this->doTestAuthenticationExceptionIsConvertedToAnHttpExceptionForRequest($request, $formats);
}
/**
* @dataProvider getContentTypesDataProvider
*
* @param array $formats
* @param string $contentType
*/
public function testAuthenticationExceptionIsConvertedToAnAccessDeniedHttpExceptionForContentType(array $formats, $contentType)
{
$request = new Request();
$request->headers->set('Content-Type', $contentType);
$this->doTestAuthenticationExceptionIsConvertedToAnHttpExceptionForRequest($request, $formats);
}
/**
* @param Request $request
* @param array $formats
*/
private function doTestAuthenticationExceptionIsConvertedToAnHttpExceptionForRequest(Request $request, array $formats)
{
$exception = new AuthenticationException();
$event = new GetResponseForExceptionEvent(new TestKernel(), $request, 'foo', $exception);
$listener = new AccessDeniedListener($formats, null, 'foo');
// store the current error_log, and disable it temporarily
$errorLog = ini_set('error_log', file_exists('/dev/null') ? '/dev/null' : 'nul');
$listener->onKernelException($event);
// restore the old error_log
ini_set('error_log', $errorLog);
$this->assertInstanceOf('Symfony\Component\HttpKernel\Exception\HttpException', $event->getException());
$this->assertEquals(401, $event->getException()->getStatusCode());
$this->assertEquals('You are not authenticated', $event->getException()->getMessage());
$this->assertArrayNotHasKey('WWW-Authenticate', $event->getException()->getHeaders());
}
/**
* @param Request $request
* @param array $formats
*/
private function doTestUnauthorizedHttpExceptionHasCorrectChallenge(Request $request, array $formats)
{
$exception = new AuthenticationException();
$event = new GetResponseForExceptionEvent(new TestKernel(), $request, 'foo', $exception);
$listener = new AccessDeniedListener($formats, 'Basic realm="Restricted Area"', 'foo');
// store the current error_log, and disable it temporarily
$errorLog = ini_set('error_log', file_exists('/dev/null') ? '/dev/null' : 'nul');
$listener->onKernelException($event);
// restore the old error_log
ini_set('error_log', $errorLog);
$this->assertInstanceOf('Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException', $event->getException());
$this->assertEquals(401, $event->getException()->getStatusCode());
$this->assertEquals('You are not authenticated', $event->getException()->getMessage());
$headers = $event->getException()->getHeaders();
$this->assertEquals('Basic realm="Restricted Area"', $headers['WWW-Authenticate']);
}
public static function getFormatsDataProvider()
{
return [
[['json' => true], 'json'],
];
}
public static function getContentTypesDataProvider()
{
return [
[['json' => true], 'application/json'],
];
}
}
class TestKernel implements HttpKernelInterface
{
public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)
{
return new Response('foo');
}
}
Tests/EventListener/BodyListenerTest.php 0000666 00000027626 13052362131 0014426 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Tests\EventListener;
use FOS\RestBundle\Decoder\ContainerDecoderProvider;
use FOS\RestBundle\EventListener\BodyListener;
use FOS\RestBundle\FOSRestBundle;
use FOS\RestBundle\Normalizer\Exception\NormalizationException;
use Symfony\Component\HttpFoundation\HeaderBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
/**
* Request listener test.
*
* @author Alain Horner
* @author Stefan Paschke
*/
class BodyListenerTest extends \PHPUnit_Framework_TestCase
{
/**
* @param bool $decode use decoder provider
* @param Request $request the original request
* @param string $method a http method (e.g. POST, GET, PUT, ...)
* @param array $expectedParameters the http parameters of the updated request
* @param string $contentType the request header content type
* @param bool $throwExceptionOnUnsupportedContentType
*
* @dataProvider testOnKernelRequestDataProvider
*/
public function testOnKernelRequest($decode, Request $request, $method, $expectedParameters, $contentType = null, $throwExceptionOnUnsupportedContentType = false)
{
$decoder = $this->getMockBuilder('FOS\RestBundle\Decoder\DecoderInterface')->getMock();
$decoder->expects($this->any())
->method('decode')
->will($this->returnValue($request->getContent()));
$container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')->getMock();
$decoderProvider = new ContainerDecoderProvider($container, ['json' => 'foo']);
$listener = new BodyListener($decoderProvider, $throwExceptionOnUnsupportedContentType);
if ($decode) {
$container
->expects($this->once())
->method('get')
->with('foo')
->will($this->returnValue($decoder));
}
$request->setMethod($method);
if ($contentType) {
$request->headers = new HeaderBag(['Content-Type' => $contentType]);
}
$event = $this->getMockBuilder(GetResponseEvent::class)
->disableOriginalConstructor()
->getMock();
$event->expects($this->once())
->method('getRequest')
->will($this->returnValue($request));
$listener->onKernelRequest($event);
$this->assertEquals($request->request->all(), $expectedParameters);
}
public static function testOnKernelRequestDataProvider()
{
return [
'Empty POST request' => [true, new Request([], [], [], [], [], [], ['foo']), 'POST', ['foo'], 'application/json'],
'Empty PUT request' => [true, new Request([], [], [], [], [], [], ['foo']), 'PUT', ['foo'], 'application/json'],
'Empty PATCH request' => [true, new Request([], [], [], [], [], [], ['foo']), 'PATCH', ['foo'], 'application/json'],
'Empty DELETE request' => [true, new Request([], [], [], [], [], [], ['foo']), 'DELETE', ['foo'], 'application/json'],
'Empty GET request' => [false, new Request([], [], [], [], [], [], ['foo']), 'GET', [], 'application/json'],
'POST request with parameters' => [false, new Request([], ['bar'], [], [], [], ['CONTENT_TYPE' => 'application/x-www-form-urlencoded'], ['foo']), 'POST', ['bar'], 'application/x-www-form-urlencoded'],
'POST request with unallowed format' => [false, new Request([], [], [], [], [], [], ['foo']), 'POST', [], 'application/fooformat'],
'POST request with no Content-Type' => [true, new Request([], [], ['_format' => 'json'], [], [], [], ['foo']), 'POST', ['foo']],
];
}
public function testOnKernelRequestNoZone()
{
$data = array('foo_bar' => 'foo_bar');
$normalizedData = array('fooBar' => 'foo_bar');
$decoder = $this->getMockBuilder('FOS\RestBundle\Decoder\DecoderInterface')->getMock();
$decoder
->expects($this->never())
->method('decode')
->will($this->returnValue($data));
$decoderProvider = $this->getMockBuilder('FOS\RestBundle\Decoder\DecoderProviderInterface')->getMock();
$decoderProvider
->expects($this->never())
->method('getDecoder')
->will($this->returnValue($decoder));
$normalizer = $this->getMockBuilder('FOS\RestBundle\Normalizer\ArrayNormalizerInterface')->getMock();
$normalizer
->expects($this->never())
->method('normalize')
->with($data)
->will($this->returnValue($normalizedData));
$request = new Request(array(), array(), array(), array(), array(), array(), 'foo');
$request->attributes->set(FOSRestBundle::ZONE_ATTRIBUTE, false);
$request->setMethod('POST');
$event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')
->disableOriginalConstructor()
->getMock();
$event->expects($this->once())
->method('getRequest')
->will($this->returnValue($request));
$listener = new BodyListener($decoderProvider, false, $normalizer);
$listener->onKernelRequest($event);
$this->assertEquals(array(), $request->request->all());
}
public function testOnKernelRequestWithNormalizer()
{
$data = ['foo_bar' => 'foo_bar'];
$normalizedData = ['fooBar' => 'foo_bar'];
$decoder = $this->getMockBuilder('FOS\RestBundle\Decoder\DecoderInterface')->getMock();
$decoder
->expects($this->any())
->method('decode')
->will($this->returnValue($data));
$decoderProvider = $this->getMockBuilder('FOS\RestBundle\Decoder\DecoderProviderInterface')->getMock();
$decoderProvider
->expects($this->any())
->method('getDecoder')
->will($this->returnValue($decoder));
$decoderProvider
->expects($this->any())
->method('supports')
->will($this->returnValue(true));
$normalizer = $this->getMockBuilder('FOS\RestBundle\Normalizer\ArrayNormalizerInterface')->getMock();
$normalizer
->expects($this->once())
->method('normalize')
->with($data)
->will($this->returnValue($normalizedData));
$request = new Request([], [], [], [], [], [], 'foo');
$request->setMethod('POST');
$event = $this->getMockBuilder(GetResponseEvent::class)
->disableOriginalConstructor()
->getMock();
$event->expects($this->once())
->method('getRequest')
->will($this->returnValue($request));
$listener = new BodyListener($decoderProvider, false, $normalizer);
$listener->onKernelRequest($event);
$this->assertEquals($normalizedData, $request->request->all());
}
/**
* @dataProvider formNormalizationProvider
*/
public function testOnKernelRequestNormalizationWithForms($method, $contentType, $mustBeNormalized)
{
$data = array('foo_bar' => 'foo_bar');
$normalizedData = array('fooBar' => 'foo_bar');
$decoderProvider = $this->getMockBuilder('FOS\RestBundle\Decoder\DecoderProviderInterface')->getMock();
$normalizer = $this->getMockBuilder('FOS\RestBundle\Normalizer\ArrayNormalizerInterface')->getMock();
if ($mustBeNormalized) {
$normalizer
->expects($this->once())
->method('normalize')
->with($data)
->will($this->returnValue($normalizedData));
} else {
$normalizer
->expects($this->never())
->method('normalize');
}
$request = new Request([], $data, [], [], [], [], 'foo');
$request->headers->set('Content-Type', $contentType);
$request->setMethod($method);
$event = $this->getMockBuilder(GetResponseEvent::class)
->disableOriginalConstructor()
->getMock();
$event->expects($this->once())
->method('getRequest')
->will($this->returnValue($request));
$listener = new BodyListener($decoderProvider, false, $normalizer, true);
$listener->onKernelRequest($event);
if ($mustBeNormalized) {
$this->assertEquals($normalizedData, $request->request->all());
} else {
$this->assertEquals($data, $request->request->all());
}
}
public function formNormalizationProvider()
{
$cases = [];
foreach (['POST', 'PUT', 'PATCH', 'DELETE'] as $method) {
$cases[] = [$method, 'multipart/form-data', true];
$cases[] = [$method, 'multipart/form-data; boundary=AaB03x', true];
$cases[] = [$method, 'application/x-www-form-urlencoded', true];
$cases[] = [$method, 'application/x-www-form-urlencoded; charset=utf-8', true];
$cases[] = [$method, 'unknown', false];
}
return $cases;
}
/**
* @expectedException \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
*/
public function testOnKernelRequestNormalizationException()
{
$decoder = $this->getMockBuilder('FOS\RestBundle\Decoder\DecoderInterface')->getMock();
$decoder
->expects($this->any())
->method('decode')
->will($this->returnValue([]));
$decoderProvider = $this->getMockBuilder('FOS\RestBundle\Decoder\DecoderProviderInterface')->getMock();
$decoderProvider
->expects($this->any())
->method('getDecoder')
->will($this->returnValue($decoder));
$decoderProvider
->expects($this->any())
->method('supports')
->will($this->returnValue(true));
$normalizer = $this->getMockBuilder('FOS\RestBundle\Normalizer\ArrayNormalizerInterface')->getMock();
$normalizer
->expects($this->once())
->method('normalize')
->will($this->throwException(new NormalizationException()));
$request = new Request([], [], [], [], [], [], 'foo');
$request->setMethod('POST');
$event = $this->getMockBuilder(GetResponseEvent::class)
->disableOriginalConstructor()
->getMock();
$event->expects($this->once())
->method('getRequest')
->will($this->returnValue($request));
$listener = new BodyListener($decoderProvider, false, $normalizer);
$listener->onKernelRequest($event);
}
/**
* Test that a malformed request will cause a BadRequestHttpException to be thrown.
*/
public function testBadRequestExceptionOnMalformedContent()
{
$this->setExpectedException('\Symfony\Component\HttpKernel\Exception\BadRequestHttpException');
$this->testOnKernelRequest(true, new Request([], [], [], [], [], [], 'foo'), 'POST', [], 'application/json');
}
/**
* Test that a unallowed format will cause a UnsupportedMediaTypeHttpException to be thrown.
*/
public function testUnsupportedMediaTypeHttpExceptionOnUnsupportedMediaType()
{
$this->setExpectedException('\Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException');
$this->testOnKernelRequest(false, new Request([], [], [], [], [], [], 'foo'), 'POST', [], 'application/foo', true);
}
public function testShouldNotThrowUnsupportedMediaTypeHttpExceptionWhenIsAnEmptyDeleteRequest()
{
$this->testOnKernelRequest(false, new Request(), 'DELETE', [], null, true);
}
}
Tests/EventListener/FormatListenerTest.php 0000666 00000015764 13052362131 0014761 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Tests\EventListener;
use FOS\RestBundle\EventListener\FormatListener;
use FOS\RestBundle\FOSRestBundle;
use FOS\RestBundle\Negotiation\FormatNegotiator;
use Negotiation\Accept;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestMatcher;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Request listener test.
*
* @author Lukas Kahwe Smith
*/
class FormatListenerTest extends \PHPUnit_Framework_TestCase
{
public function testOnKernelControllerNegotiation()
{
$event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')
->disableOriginalConstructor()
->getMock();
$request = new Request();
$event->expects($this->once())
->method('getRequest')
->will($this->returnValue($request));
$formatNegotiator = $this->getMockBuilder('FOS\RestBundle\Negotiation\FormatNegotiator')
->disableOriginalConstructor()
->setMethods(['getBest'])
->getMock();
$formatNegotiator->expects($this->once())
->method('getBest')
->willReturn(new Accept('text/xml; q=1'));
$listener = new FormatListener($formatNegotiator);
$listener->onKernelRequest($event);
$this->assertEquals('xml', $request->getRequestFormat());
}
public function testOnKernelControllerNoZone()
{
$event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')
->disableOriginalConstructor()
->getMock();
$request = new Request();
$request->attributes->set(FOSRestBundle::ZONE_ATTRIBUTE, false);
$requestStack = new RequestStack();
$requestStack->push($request);
$event->expects($this->once())
->method('getRequest')
->will($this->returnValue($request));
$formatNegotiator = new FormatNegotiator($requestStack);
$formatNegotiator->add(new RequestMatcher('/'), ['fallback_format' => 'json']);
$listener = new FormatListener($formatNegotiator);
$listener->onKernelRequest($event);
$this->assertEquals($request->getRequestFormat(), 'html');
}
public function testOnKernelControllerNegotiationStopped()
{
$event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')
->disableOriginalConstructor()
->getMock();
$request = new Request();
$request->setRequestFormat('xml');
$requestStack = new RequestStack();
$requestStack->push($request);
$event->expects($this->once())
->method('getRequest')
->will($this->returnValue($request));
$formatNegotiator = new FormatNegotiator($requestStack);
$formatNegotiator->add(new RequestMatcher('/'), ['stop' => true]);
$formatNegotiator->add(new RequestMatcher('/'), ['fallback_format' => 'json']);
$listener = new FormatListener($formatNegotiator);
$listener->onKernelRequest($event);
$this->assertEquals($request->getRequestFormat(), 'xml');
}
/**
* @expectedException \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function testOnKernelControllerException()
{
$event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')
->disableOriginalConstructor()
->getMock();
$request = new Request();
$event->expects($this->once())
->method('getRequest')
->will($this->returnValue($request));
$event->expects($this->once())
->method('getRequestType')
->will($this->returnValue(HttpKernelInterface::MASTER_REQUEST));
$formatNegotiator = $this->getMockBuilder('FOS\RestBundle\Negotiation\FormatNegotiator')
->disableOriginalConstructor()
->getMock();
$listener = new FormatListener($formatNegotiator);
$listener->onKernelRequest($event);
}
/**
* Test FormatListener won't overwrite request format when it was already specified.
*
* @dataProvider useSpecifiedFormatDataProvider
*/
public function testUseSpecifiedFormat($format, $result)
{
$event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')
->disableOriginalConstructor()
->getMock();
$request = new Request();
if ($format) {
$request->setRequestFormat($format);
}
$event->expects($this->once())
->method('getRequest')
->will($this->returnValue($request));
$formatNegotiator = $this->getMockBuilder('FOS\RestBundle\Negotiation\FormatNegotiator')
->disableOriginalConstructor()
->setMethods(['getBest'])
->getMock();
$formatNegotiator->expects($this->any())
->method('getBest')
->willReturn(new Accept('text/xml; q=1'));
$listener = new FormatListener($formatNegotiator);
$listener->onKernelRequest($event);
$this->assertEquals($request->getRequestFormat(), $result);
}
public function useSpecifiedFormatDataProvider()
{
return [
[null, 'xml'],
['json', 'json'],
];
}
/**
* Generates a request like a symfony fragment listener does.
* Set request type to master.
*/
public function testSfFragmentFormat()
{
$event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')
->disableOriginalConstructor()
->getMock();
$request = new Request();
$attributes = ['_locale' => 'en', '_format' => 'json', '_controller' => 'FooBundle:Index:featured'];
$request->attributes->add($attributes);
$request->attributes->set('_route_params', array_replace($request->attributes->get('_route_params', []), $attributes));
$event->expects($this->once())
->method('getRequest')
->will($this->returnValue($request));
$event->expects($this->any())
->method('getRequestType')
->will($this->returnValue(HttpKernelInterface::MASTER_REQUEST));
$formatNegotiator = $this->getMockBuilder('FOS\RestBundle\Negotiation\FormatNegotiator')
->disableOriginalConstructor()
->getMock();
$formatNegotiator->expects($this->any())
->method('getBest')
->will($this->returnValue('application/json'));
$listener = new FormatListener($formatNegotiator);
$listener->onKernelRequest($event);
$this->assertEquals($request->getRequestFormat(), 'json');
}
}
Tests/EventListener/MimeTypeListenerTest.php 0000666 00000006126 13052362131 0015252 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Tests\EventListener;
use FOS\RestBundle\EventListener\MimeTypeListener;
use FOS\RestBundle\FOSRestBundle;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Request listener test.
*
* @author Lukas Kahwe Smith
*/
class MimeTypeListenerTest extends \PHPUnit_Framework_TestCase
{
public function testOnKernelRequest()
{
$listener = new MimeTypeListener(['jsonp' => ['application/javascript+jsonp']]);
$request = new Request();
$event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')
->disableOriginalConstructor()->getMock();
$event->expects($this->any())
->method('getRequest')
->will($this->returnValue($request));
$this->assertNull($request->getMimeType('jsonp'));
$listener->onKernelRequest($event);
$this->assertNull($request->getMimeType('jsonp'));
$event->expects($this->once())
->method('getRequestType')
->will($this->returnValue(HttpKernelInterface::MASTER_REQUEST));
$listener->onKernelRequest($event);
$this->assertEquals('application/javascript+jsonp', $request->getMimeType('jsonp'));
}
public function testOnKernelRequestNoZone()
{
$listener = new MimeTypeListener(['soap' => ['application/soap+xml']]);
$request = new Request();
$request->attributes->set(FOSRestBundle::ZONE_ATTRIBUTE, false);
$event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')
->disableOriginalConstructor()->getMock();
$event->expects($this->any())
->method('getRequest')
->will($this->returnValue($request));
$event->expects($this->never())
->method('getRequestType')
->will($this->returnValue(HttpKernelInterface::MASTER_REQUEST));
$listener->onKernelRequest($event);
$this->assertNull($request->getMimeType('soap'));
}
public function testOnKernelRequestWithZone()
{
$listener = new MimeTypeListener(['soap' => ['application/soap+xml']]);
$request = new Request();
$request->attributes->set(FOSRestBundle::ZONE_ATTRIBUTE, true);
$event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')
->disableOriginalConstructor()->getMock();
$event->expects($this->any())
->method('getRequest')
->will($this->returnValue($request));
$event->expects($this->once())
->method('getRequestType')
->will($this->returnValue(HttpKernelInterface::MASTER_REQUEST));
$listener->onKernelRequest($event);
$this->assertEquals('application/soap+xml', $request->getMimeType('soap'));
}
}
Tests/EventListener/ParamFetcherListenerTest.php 0000666 00000014100 13052362131 0016051 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Tests\EventListener;
use FOS\RestBundle\EventListener\ParamFetcherListener;
use FOS\RestBundle\FOSRestBundle;
use FOS\RestBundle\Tests\Fixtures\Controller\ParamFetcherController;
use Symfony\Component\HttpFoundation\Request;
/**
* Param Fetcher Listener Tests.
*/
class ParamFetcherListenerTest extends \PHPUnit_Framework_TestCase
{
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
private $paramFetcher;
/**
* @var \FOS\RestBundle\EventListener\ParamFetcherListener
*/
private $paramFetcherListener;
/**
* Tests the ParamFetcher being able to set an attribute on the request
* when configured to do so and the attribute is specified as a null
* default value.
*/
public function testSettingAttributes()
{
$request = new Request();
$request->attributes->set('customer', null);
$event = $this->getEvent($request);
$this->paramFetcher->expects($this->once())
->method('all')
->will($this->returnValue([
'customer' => 5,
]));
$this->paramFetcherListener->onKernelController($event);
$this->assertEquals(5, $request->attributes->get('customer'), 'Listener set attribute as expected');
}
/**
* Tests the ParamFetcher being able to set an attribute on the request
* when configured to do so and the attribute is specified as a null
* default value.
*/
public function testSettingParamFetcherOnRequest()
{
$request = new Request();
$event = $this->getEvent($request);
$this->paramFetcher->expects($this->once())
->method('all')
->will($this->returnValue([]));
$this->paramFetcherListener->onKernelController($event);
$this->assertSame($this->paramFetcher, $request->attributes->get('paramFetcher'));
}
public function testParamFetcherOnRequestNoZone()
{
$request = new Request();
$request->attributes->set(FOSRestBundle::ZONE_ATTRIBUTE, false);
$event = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\Event\\FilterControllerEvent')
->disableOriginalConstructor()
->getMock();
$event->expects($this->atLeastOnce())
->method('getRequest')
->will($this->returnValue($request));
$this->paramFetcher->expects($this->never())
->method('all')
->will($this->returnValue(array()));
$this->paramFetcherListener->onKernelController($event);
$this->assertNull($request->attributes->get('paramFetcher'));
}
/**
* Tests that the ParamFetcher can be injected by the default name
* ($paramFetcher) or by a different name if type-hinted.
*
* @dataProvider setParamFetcherByTypehintProvider
*/
public function testSettingParamFetcherByTypehint($actionName, $expectedAttribute)
{
$request = new Request();
$event = $this->getEvent($request, $actionName);
$this->paramFetcher->expects($this->once())
->method('all')
->will($this->returnValue([]));
$this->paramFetcherListener->onKernelController($event);
$this->assertSame($this->paramFetcher, $request->attributes->get($expectedAttribute));
}
/**
* Tests that the ParamFetcher can be injected in a invokable controller.
*/
public function testSettingParamFetcherForInvokable()
{
$request = new Request();
$event = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\Event\\FilterControllerEvent')
->disableOriginalConstructor()
->getMock();
$event->expects($this->atLeastOnce())
->method('getRequest')
->will($this->returnValue($request));
$controller = new ParamFetcherController();
$event->expects($this->atLeastOnce())
->method('getController')
->will($this->returnValue($controller));
$this->paramFetcher->expects($this->once())
->method('all')
->will($this->returnValue([]));
$this->paramFetcherListener->onKernelController($event);
$this->assertSame($this->paramFetcher, $request->attributes->get('pfInvokable'));
}
public function setParamFetcherByTypehintProvider()
{
return [
// Without a typehint, the ParamFetcher should be injected as
// $paramFetcher.
['byNameAction', 'paramFetcher'],
// With a typehint, the ParamFetcher should be injected as whatever
// the parameter name is.
['byTypeAction', 'pf'],
// The user can typehint using ParamFetcherInterface, too.
['byInterfaceAction', 'pfi'],
// If there is no controller argument for the ParamFetcher, it
// should be injected as the default name.
['notProvidedAction', 'paramFetcher'],
];
}
protected function getEvent(Request $request, $actionMethod = 'byNameAction')
{
$event = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\Event\\FilterControllerEvent')
->disableOriginalConstructor()
->getMock();
$event->expects($this->atLeastOnce())
->method('getRequest')
->will($this->returnValue($request));
$controller = new ParamFetcherController();
$event->expects($this->atLeastOnce())
->method('getController')
->will($this->returnValue([$controller, $actionMethod]));
return $event;
}
public function setUp()
{
$this->paramFetcher = $this->getMockBuilder('FOS\\RestBundle\\Request\\ParamFetcher')
->disableOriginalConstructor()
->getMock();
$this->paramFetcherListener = new ParamFetcherListener($this->paramFetcher, true);
}
}
Tests/EventListener/VersionListenerTest.php 0000666 00000003602 13052362131 0015142 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Tests\EventListener;
use FOS\RestBundle\EventListener\VersionListener;
use FOS\RestBundle\FOSRestBundle;
use Symfony\Component\HttpFoundation\Request;
/**
* Version listener test.
*
* @author Ener-Getick
*/
class VersionListenerTest extends \PHPUnit_Framework_TestCase
{
/**
* @var \FOS\RestBundle\View\ConfigurableViewHandlerInterface
*/
private $viewHandler;
/**
* @var \FOS\RestBundle\Version\VersionResolverInterface
*/
private $resolver;
/**
* @var \FOS\RestBundle\EventListener\VersionListener
*/
private $listener;
public function setUp()
{
$this->viewHandler = $this->getMockBuilder('FOS\RestBundle\View\ConfigurableViewHandlerInterface')->getMock();
$this->resolver = $this->getMockBuilder('FOS\RestBundle\Version\VersionResolverInterface')->getMock();
$this->listener = new VersionListener($this->viewHandler, $this->resolver);
}
public function testDefaultVersion()
{
$this->assertEquals(false, $this->listener->getVersion());
}
public function testMatchNoZone()
{
$request = new Request();
$request->attributes->set(FOSRestBundle::ZONE_ATTRIBUTE, false);
$event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')
->disableOriginalConstructor()
->getMock();
$event
->expects($this->once())
->method('getRequest')
->willReturn($request);
$this->listener->onKernelRequest($event);
$this->assertFalse($this->listener->getVersion());
}
}
Tests/EventListener/ViewResponseListenerTest.php 0000666 00000022417 13052362131 0016153 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Tests\EventListener;
use FOS\RestBundle\Controller\Annotations\View as ViewAnnotation;
use FOS\RestBundle\EventListener\ViewResponseListener;
use FOS\RestBundle\FOSRestBundle;
use FOS\RestBundle\View\View;
use FOS\RestBundle\View\ViewHandler;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
/**
* View response listener test.
*
* @author Lukas Kahwe Smith
*/
class ViewResponseListenerTest extends \PHPUnit_Framework_TestCase
{
/**
* @var \FOS\RestBundle\EventListener\ViewResponseListener
*/
public $listener;
/**
* @var \FOS\RestBundle\View\ViewHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
public $viewHandler;
/**
* @var \Symfony\Bundle\FrameworkBundle\Templating\EngineInterface|\PHPUnit_Framework_MockObject_MockObject
*/
public $templating;
private $router;
private $serializer;
private $requestStack;
/**
* @param Request $request
*
* @return \Symfony\Component\HttpKernel\Event\FilterControllerEvent|\PHPUnit_Framework_MockObject_MockObject
*/
protected function getFilterEvent(Request $request)
{
$event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\FilterControllerEvent')
->disableOriginalConstructor()
->getMock();
$event->expects($this->once())
->method('getRequest')
->will($this->returnValue($request));
return $event;
}
/**
* @param Request $request
* @param mixed $result
*
* @return \Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent|\PHPUnit_Framework_MockObject_MockObject
*/
protected function getResponseEvent(Request $request, $result)
{
$event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent')
->disableOriginalConstructor()
->getMock();
$event->expects($this->atLeastOnce())
->method('getRequest')
->will($this->returnValue($request));
$event->expects($this->any())
->method('getControllerResult')
->will($this->returnValue($result));
return $event;
}
public function testOnKernelView()
{
$template = $this->getMockBuilder('Symfony\Component\Templating\TemplateReferenceInterface')
->disableOriginalConstructor()
->getMock();
$template->expects($this->once())
->method('set')
->with('format', null);
$annotation = new ViewAnnotation([]);
$annotation->setOwner([new FooController(), 'onKernelViewAction']);
$annotation->setTemplate($template);
$request = new Request();
$request->attributes->set('foo', 'baz');
$request->attributes->set('halli', 'galli');
$request->attributes->set('_template', $annotation);
$response = new Response();
$view = $this->getMockBuilder('FOS\RestBundle\View\View')
->disableOriginalConstructor()
->getMock();
$view->expects($this->exactly(2))
->method('getFormat')
->will($this->onConsecutiveCalls(null, 'html'));
$viewHandler = $this->getMockBuilder('FOS\RestBundle\View\ViewHandlerInterface')->getMock();
$viewHandler->expects($this->once())
->method('handle')
->with($this->isInstanceOf('FOS\RestBundle\View\View'), $this->equalTo($request))
->will($this->returnValue($response));
$viewHandler->expects($this->once())
->method('isFormatTemplating')
->with('html')
->will($this->returnValue(true));
$this->listener = new ViewResponseListener($viewHandler, false);
$event = $this->getResponseEvent($request, $view);
$event->expects($this->once())
->method('setResponse');
$this->listener->onKernelView($event);
}
public function testOnKernelViewWhenControllerResultIsNotViewObject()
{
$this->createViewResponseListener();
$request = new Request();
$event = $this->getResponseEvent($request, []);
$event->expects($this->never())
->method('setResponse');
$this->assertEquals(null, $this->listener->onKernelView($event));
}
public static function statusCodeProvider()
{
return [
[201, 200, 201],
[201, 404, 404],
[201, 500, 500],
];
}
/**
* @dataProvider statusCodeProvider
*/
public function testStatusCode($annotationCode, $viewCode, $expectedCode)
{
$this->createViewResponseListener(['json' => true]);
$viewAnnotation = new ViewAnnotation([]);
$viewAnnotation->setOwner([$this, 'statusCodeProvider']);
$viewAnnotation->setStatusCode($annotationCode);
$request = new Request();
$request->setRequestFormat('json');
$request->attributes->set('_template', $viewAnnotation);
$this->templating->expects($this->any())
->method('render')
->will($this->returnValue('foo'));
$view = new View();
$view->setStatusCode($viewCode);
$view->setData('foo');
$event = $this->getResponseEvent($request, $view);
$response = new Response();
$event->expects($this->any())
->method('setResponse')
->will($this->returnCallback(function ($r) use (&$response) {
$response = $r;
}));
$this->listener->onKernelView($event);
$this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $response);
$this->assertSame($expectedCode, $response->getStatusCode());
}
public static function serializerEnableMaxDepthChecksProvider()
{
return [
[false, null],
[true, 0],
];
}
/**
* @dataProvider serializerEnableMaxDepthChecksProvider
*/
public function testSerializerEnableMaxDepthChecks($enableMaxDepthChecks, $expectedMaxDepth)
{
$this->createViewResponseListener(['json' => true]);
$viewAnnotation = new ViewAnnotation([]);
$viewAnnotation->setOwner([$this, 'testSerializerEnableMaxDepthChecks']);
$viewAnnotation->setSerializerEnableMaxDepthChecks($enableMaxDepthChecks);
$request = new Request();
$request->setRequestFormat('json');
$request->attributes->set('_template', $viewAnnotation);
$this->templating->expects($this->any())
->method('render')
->will($this->returnValue('foo'));
$view = new View();
$event = $this->getResponseEvent($request, $view);
$this->listener->onKernelView($event);
$context = $view->getContext();
$this->assertEquals($expectedMaxDepth, $context->getMaxDepth(false));
$this->assertEquals($enableMaxDepthChecks, $context->isMaxDepthEnabled());
}
public function getDataForDefaultVarsCopy()
{
return [
[false],
[true],
];
}
/**
* @dataProvider getDataForDefaultVarsCopy
*/
public function testViewWithNoCopyDefaultVars($populateDefaultVars)
{
$this->createViewResponseListener(['html' => true]);
$request = new Request();
$request->attributes->set('customer', 'A person goes here');
$view = View::create();
$viewAnnotation = new ViewAnnotation([]);
$viewAnnotation->setOwner([new FooController(), 'viewAction']);
$viewAnnotation->setPopulateDefaultVars($populateDefaultVars);
$request->attributes->set('_template', $viewAnnotation);
$event = $this->getResponseEvent($request, $view);
$this->listener->onKernelView($event);
$data = $view->getData();
if ($populateDefaultVars) {
$this->assertArrayHasKey('customer', $data);
$this->assertEquals('A person goes here', $data['customer']);
} else {
$this->assertNull($data);
}
}
protected function setUp()
{
$this->router = $this->getMockBuilder('Symfony\Component\Routing\RouterInterface')->getMock();
$this->serializer = $this->getMockBuilder('FOS\RestBundle\Serializer\Serializer')->getMock();
$this->templating = $this->getMockBuilder('Symfony\Bundle\FrameworkBundle\Templating\EngineInterface')->getMock();
$this->requestStack = new RequestStack();
}
private function createViewResponseListener($formats = null)
{
$this->viewHandler = new ViewHandler($this->router, $this->serializer, $this->templating, $this->requestStack, $formats);
$this->listener = new ViewResponseListener($this->viewHandler, false);
}
}
class FooController
{
/**
* @see testOnKernelView()
*/
public function onKernelViewAction($foo, $halli)
{
}
/**
* @see testViewWithNoCopyDefaultVars()
*/
public function viewAction($customer)
{
}
}
Tests/EventListener/ZoneMatcherListenerTest.php 0000666 00000005027 13052362131 0015737 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Tests\EventListener;
use FOS\RestBundle\EventListener\ZoneMatcherListener;
use FOS\RestBundle\FOSRestBundle;
use Symfony\Component\HttpFoundation\Request;
class ZoneMatcherListenerTest extends \PHPUnit_Framework_TestCase
{
public function testNoRequestMatcher()
{
$request = new Request();
$event = $this->getGetResponseEvent($request);
$listener = new ZoneMatcherListener();
$listener->onKernelRequest($event);
$this->assertTrue($request->attributes->has(FOSRestBundle::ZONE_ATTRIBUTE));
}
public function testWithRequestMatcherMatch()
{
$request = new Request();
$event = $this->getGetResponseEvent($request);
$requestMatcher = $this->getMockBuilder('Symfony\Component\HttpFoundation\RequestMatcherInterface')->getMock();
$requestMatcher
->expects($this->once())
->method('matches')
->with($request)
->will($this->returnValue(true));
$listener = new ZoneMatcherListener();
$listener->addRequestMatcher($requestMatcher);
$listener->onKernelRequest($event);
$this->assertTrue($request->attributes->has(FOSRestBundle::ZONE_ATTRIBUTE));
}
public function testWithRequestMatcherNoMatch()
{
$request = new Request();
$event = $this->getGetResponseEvent($request);
$requestMatcher = $this->getMockBuilder('Symfony\Component\HttpFoundation\RequestMatcherInterface')->getMock();
$requestMatcher
->expects($this->once())
->method('matches')
->with($request)
->will($this->returnValue(false));
$listener = new ZoneMatcherListener();
$listener->addRequestMatcher($requestMatcher);
$listener->onKernelRequest($event);
$this->assertFalse($request->attributes->get(FOSRestBundle::ZONE_ATTRIBUTE));
}
private function getGetResponseEvent(Request $request)
{
$event = $this->getMockBuilder('Symfony\Component\HttpKernel\Event\GetResponseEvent')
->disableOriginalConstructor()
->getMock();
$event
->expects($this->once())
->method('getRequest')
->will($this->returnValue($request));
return $event;
}
}
Tests/FOSRestBundleTest.php 0000666 00000002004 13052362131 0011632 0 ustar 00
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace FOS\RestBundle\Tests;
use FOS\RestBundle\FOSRestBundle;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
/**
* FOSRestBundle test.
*
* @author Eriksen Costa
*/
class FOSRestBundleTest extends \PHPUnit_Framework_TestCase
{
public function testBuild()
{
$container = $this->getMockBuilder(ContainerBuilder::class)
->setMethods(['addCompilerPass'])
->getMock();
$container->expects($this->exactly(5))
->method('addCompilerPass')
->with($this->isInstanceOf(CompilerPassInterface::class));
$bundle = new FOSRestBundle();
$bundle->build($container);
}
}
Tests/Fixtures/Asset/bar.txt 0000666 00000000010 13052362131 0012032 0 ustar 00 FOO=BAR
Tests/Fixtures/Asset/cat.jpeg 0000666 00000066477 13052362131 0012200 0 ustar 00 JFIF ;CREATOR: gd-jpeg v1.0 (using IJG JPEG v62), quality = 90
C
C
"
} !1AQa"q2#BR$3br
%&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz
w !1AQaq"2B #3Rbr
$4%&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz ? ۉ5
rZwGSmQ^{5
(Q@4?{ '&ot,mPvOj6BA~xo0ctBjƫ, W=MV_/摎XxD
wO
07"yQ_MxgñZ|6ˁVg|q˵K{y8uJOA I6ZB# zVmͬܤVm|Akbd@=Z=2MlOj x真O%7clWkugjlz77颽z
?PEdYnּ
n>cbXϴd}jwZP1zn F煓Ǻe,~JD|w2p9nƻemUjE,6~z?\Iȟ