23/07/2015 - SYMFONY
In example below, we're validating XML request against models and map serialised fields to model properties. In the case of invalid request, json error response with "400 Bad request" is sent back. @Serializer\Type("....")
will convert some values automatically to match the type however, if it cannot then "500 Internal Server Error - Could not decode data ......" response will be issued. Example also uses a custom built validator constraint for datetime.
Install "jms/serializer-bundle": "0.13.0"
with composer.
services:
application_backend.controller.abstract:
class: Application\BackendBundle\Controller\AbstractController
abstract: true
arguments:
- @serializer
- @validator
- @doctrine_common_inflector
application_backend.controller.api:
class: Application\BackendBundle\Controller\ApiController
parent: application_backend.controller.abstract
doctrine_common_inflector:
class: Doctrine\Common\Inflector\Inflector
namespace Application\BackendBundle\Controller;
use Doctrine\Common\Inflector\Inflector;
use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\ConstraintViolationInterface;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
abstract class AbstractController
{
private $validContentTypes = ['json' => 'application/json', 'xml' => 'application/xml'];
protected $serializer;
protected $validator;
protected $inflector;
public function __construct(
SerializerInterface $serializer,
ValidatorInterface $validator,
Inflector $inflector
) {
$this->serializer = $serializer;
$this->validator = $validator;
$this->inflector = $inflector;
}
/**
* @param string $contentType
*
* @return string|Response
*/
protected function validateContentType($contentType)
{
if (!in_array($contentType, $this->validContentTypes)) {
return $this->createFailureResponse(
['content_type' => sprintf('Invalid content type [%s].', $contentType)],
'json',
415
);
}
return array_search($contentType, $this->validContentTypes);
}
/**
* @param string $payload
* @param string $model
* @param string $format
*
* @return object|Response
*/
protected function validatePayload($payload, $model, $format)
{
$payload = $this->serializer->deserialize($payload, $model, $format);
$errors = $this->validator->validate($payload);
if (count($errors)) {
return $this->createFailureResponse($errors, $format);
}
return $payload;
}
/**
* @param array|object $content
* @param string $format
* @param int $status
*
* @return Response
*/
protected function createSuccessResponse($content, $format = 'json', $status = 200)
{
return $this->getResponse($content, $format, $status);
}
/**
* @param array|ConstraintViolationListInterface $content
* @param string $format
* @param int $status
*
* @return Response
*/
protected function createFailureResponse($content, $format = 'json', $status = 400)
{
$errorList = null;
if ($content instanceof ConstraintViolationList) {
foreach ($content as $error) {
$error = $this->getErrorFromValidation($error);
$errorList[$error['key']] = $error['value'];
}
} else {
$errorList = $content;
}
return $this->getResponse(['errors' => $errorList], $format, $status);
}
/**
* @param array|object $content
* @param string $format
* @param int $status
*
* @return Response
*/
private function getResponse($content, $format, $status)
{
$context = new SerializationContext();
$context->setSerializeNull(false);
$response = $this->serializer->serialize($content, $format, $context);
return new Response($response, $status, ['Content-Type' => $this->validContentTypes[$format]]);
}
/**
* @param ConstraintViolationInterface $error
*
* @return array
*/
private function getErrorFromValidation($error)
{
$properties = $this->inflector->tableize($error->getPropertyPath());
return ['key' => $properties, 'value' => $error->getMessage()];
}
}
namespace Application\BackendBundle\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use JMS\Serializer\SerializerInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Doctrine\Common\Inflector\Inflector;
/**
* @Route("api", service="application_backend.controller.api")
*/
class ApiController extends AbstractController
{
public function __construct(
SerializerInterface $serializer,
ValidatorInterface $validator,
Inflector $inflector
) {
parent::__construct($serializer, $validator, $inflector);
}
/**
* @param Request $request
*
* @Method({"POST"})
* @Route("/xml")
*
* @return JsonResponse|Response
*/
public function xmlAction(Request $request)
{
$format = $this->validateContentType($request->headers->get('content_type'));
if ($format instanceof Response) {
return $format;
}
$product = $this->validatePayload(
$request->getContent(),
'Application\BackendBundle\Model\Api\Xml\Product',
$format
);
if ($product instanceof Response) {
return $product;
}
//print_r($product); exit;
return $this->createSuccessResponse($device, $format);
}
}
namespace Application\BackendBundle\Model\Api\Xml;
use Symfony\Component\Validator\Constraints as Assert;
use Application\BackendBundle\Validator\Constraints as BackendBundleAssert;
use JMS\Serializer\Annotation as Serializer;
/**
* @Serializer\XmlRoot("product")
*/
class Product
{
/**
* @var int
*
* @Assert\NotBlank(message="The id attribute field is required.")
* @Assert\Regex(
* pattern="/^[0-9]+$/",
* message="The id attribute field must contain only numeric character(s)."
* )
* @Serializer\Type("integer")
* @Serializer\XmlAttribute()
*/
public $id;
/**
* @var string
*
* @Assert\NotBlank(message="The title field is required.")
* @Assert\Length(
* max=255,
* maxMessage="The title field cannot be longer than {{ limit }} character(s)."
* )
*
* @Serializer\Type("string")
*/
public $title;
/**
* @var int
*
* @Assert\NotBlank(message="The stock field is required.")
* @Assert\Regex(
* pattern="/^[0-9]+$/",
* message="The stock field must contain only numeric character(s)."
* )
*
* @Serializer\Type("integer")
*/
public $stock;
/**
* @var bool
*
* @Assert\NotBlank(message="The is_active field is required.")
* @Assert\Type(
* type="bool",
* message="The is_active field must contain only boolean value."
* )
*
* @Serializer\Type("boolean")
*/
public $isActive;
/**
* @var string
*
* @Assert\NotBlank(message="The datetime field is required.")
* @BackendBundleAssert\DateTime
*
* @Serializer\Type("string")
*/
public $datetime;
/**
* @var Category
*
* @Assert\Valid(traverse="true")
*
* @Serializer\Type("Application\BackendBundle\Model\Api\Xml\Category")
*/
public $category;
/**
* @var Image[]
*
* @Assert\Valid(traverse="true")
* @Assert\Count(
* max="2",
* maxMessage="You can provide maximum {{ limit }} image(s)."
* )
*
* @Serializer\XmlList(inline=false, entry="uri")
* @Serializer\Type("array<Application\BackendBundle\Model\Api\Xml\Image>")
*/
public $images = [];
/**
* @var DeliveryCost
*
* @Assert\Valid(traverse="true")
*
* @Serializer\Type("Application\BackendBundle\Model\Api\Xml\DeliveryCost")
*/
public $deliveryCost;
/**
* @var Identifier[]
*
* @Assert\Valid(traverse="true")
*
* @Serializer\XmlList(inline=false, entry="identifier")
* @Serializer\Type("array<Application\BackendBundle\Model\Api\Xml\Identifier>")
*/
public $identifiers = [];
/**
* @var Property
*
* @Assert\Valid(traverse="true")
*
* @Serializer\Type("Application\BackendBundle\Model\Api\Xml\Property")
*/
public $properties;
}
namespace Application\BackendBundle\Model\Api\Xml;
use Symfony\Component\Validator\Constraints as Assert;
use JMS\Serializer\Annotation as Serializer;
class Category
{
/**
* @var string
*
* @Assert\NotBlank(message="The id field is required.")
* @Assert\Length(
* max=5,
* maxMessage="The id field cannot be longer than {{ limit }} character(s)."
* )
*
* @Serializer\Type("string")
* @Serializer\XmlAttribute
*/
public $id;
/**
* @var string
*
* @Assert\NotBlank(message="The category value is required.")
* @Assert\Length(
* max=20,
* maxMessage="The category value cannot be longer than {{ limit }} character(s)."
* )
*
* @Serializer\Type("string")
* @Serializer\XmlValue
*/
public $value;
}
namespace Application\BackendBundle\Model\Api\Xml;
use Symfony\Component\Validator\Constraints as Assert;
use JMS\Serializer\Annotation as Serializer;
class Image
{
/**
* @var string
*
* @Assert\NotBlank(message="The image uri is required.")
*
* @Serializer\Type("string")
* @Serializer\XmlValue
*/
public $uri;
}
namespace Application\BackendBundle\Model\Api\Xml;
use Symfony\Component\Validator\Constraints as Assert;
use JMS\Serializer\Annotation as Serializer;
class DeliveryCost
{
/**
* @var float
*
* @Assert\NotBlank(message="The standard field is required.")
* @Assert\Regex(
* pattern="/^(?!0\d)\d{1,8}(\.[0-9]{1,2})?$/",
* message="The standard field must have a valid format.")
*
* @Serializer\Type("float")
*/
public $standard;
/**
* @var float
*
* @Assert\Regex(
* pattern="/^(?!0\d)\d{1,8}(\.[0-9]{1,2})?$/",
* message="The express field must have a valid format.")
*
* @Serializer\Type("float")
*/
public $express;
}
namespace Application\BackendBundle\Model\Api\Xml;
use Symfony\Component\Validator\Constraints as Assert;
use JMS\Serializer\Annotation as Serializer;
class Identifier
{
/**
* @var string
*
* @Assert\NotBlank(message="The code field is required.")
* @Assert\Length(
* max=5,
* maxMessage="The code field cannot be longer than {{ limit }} character(s)."
* )
*
* @Serializer\Type("string")
* @Serializer\XmlAttribute
*/
public $code;
/**
* @var string
*
* @Assert\NotBlank(message="The category value is required.")
* @Assert\Length(
* max=10,
* maxMessage="The category value cannot be longer than {{ limit }} character(s)."
* )
*
* @Serializer\Type("string")
* @Serializer\XmlValue
*/
public $value;
}
namespace Application\BackendBundle\Model\Api\Xml;
use Symfony\Component\Validator\Constraints as Assert;
use JMS\Serializer\Annotation as Serializer;
class Property
{
/**
* @var string
*
* @Assert\NotBlank(message="The colour field is required.")
* @Assert\Length(
* max=20,
* maxMessage="The colour field cannot be longer than {{ limit }} character(s)."
* )
*
* @Serializer\Type("string")
*/
public $colour;
/**
* @var Manufacturer
*
* @Assert\Valid(traverse="true")
*
* @Serializer\Type("Application\BackendBundle\Model\Api\Xml\Manufacturer")
*/
public $manufacturer;
}
namespace Application\BackendBundle\Model\Api\Xml;
use Symfony\Component\Validator\Constraints as Assert;
use JMS\Serializer\Annotation as Serializer;
class Manufacturer
{
/**
* @var string
*
* @Assert\NotBlank(message="The name field is required.")
* @Assert\Length(
* max=100,
* maxMessage="The name field cannot be longer than {{ limit }} character(s)."
* )
*
* @Serializer\Type("string")
*/
public $name;
/**
* @var string
*
* @Assert\NotBlank(message="The website field is required.")
* @Assert\Url(message="The website field must contain a valid URL.")
*
* @Serializer\Type("string")
*/
public $website;
}
# DateTime.php
namespace Application\BackendBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
/**
* @Annotation
*/
class DateTime extends Constraint
{
const MESSAGE = 'The datetime must be in yyyy-mm-dd H:i:s format.';
public function validatedBy()
{
return get_class($this).'Validator';
}
}
# DateTimeValidator.php
namespace Application\BackendBundle\Validator\Constraints;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use DateTime;
class DateTimeValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
if (trim($value) == '') {
return;
}
$datetime = DateTime::createFromFormat('Y-m-d H:i:s', $value);
if (!$datetime || $datetime->format('Y-m-d H:i:s') != $value) {
$this->context
->buildViolation($constraint::MESSAGE)
->addViolation();
}
}
}
<?xml version="1.0" encoding="UTF-8"?>
<product id="1">
<title>Samsung Galaxy S4</title>
<stock>5</stock>
<is_active>truse</is_active>
<datetime>2015-07-17 13:25:41</datetime>
<category id="MP">Mobile Phone</category>
<images>
<uri>http://www.inanzzz.com/123.jpg</uri>
<uri>http://www.inanzzz.com/456.jpg</uri>
</images>
<delivery_cost>
<standard>253.50</standard>
<express>269.99</express>
</delivery_cost>
<identifiers>
<identifier code="A1">12345</identifier>
<identifier code="C6">C6-123</identifier>
</identifiers>
<properties>
<colour>Black</colour>
<manufacturer>
<name>Samsung Group</name>
<website>http://www.samsung.com</website>
</manufacturer>
</properties>
</product>
After POSTing the XML request payload above to http://football.local/app_dev.php/backend/api/xml
and if we dump $product
in controller just before returning the response, valid output would be like below. The values in actual XML response will be wrapped up with <![CDATA[value will appear here]]>
tags.
Application\BackendBundle\Model\Api\Xml\Product Object
(
[id] => 1
[title] => Samsung Galaxy S4
[stock] => 5
[isActive] => 1
[datetime] => 2015-07-17 13:25:41
[category] => Application\BackendBundle\Model\Api\Xml\Category Object
(
[id] => MP
[value] => Mobile Phone
)
[images] => Array
(
[0] => Application\BackendBundle\Model\Api\Xml\Image Object
(
[uri] => http://www.inanzzz.com/123.jpg
)
[1] => Application\BackendBundle\Model\Api\Xml\Image Object
(
[uri] => http://www.inanzzz.com/456.jpg
)
)
[deliveryCost] => Application\BackendBundle\Model\Api\Xml\DeliveryCost Object
(
[standard] => 253.5
[express] => 269.99
)
[identifiers] => Array
(
[0] => Application\BackendBundle\Model\Api\Xml\Identifier Object
(
[code] => A1
[value] => 12345
)
[1] => Application\BackendBundle\Model\Api\Xml\Identifier Object
(
[code] => C6
[value] => C6-123
)
)
[properties] => Application\BackendBundle\Model\Api\Xml\Property Object
(
[colour] => Black
[manufacturer] => Application\BackendBundle\Model\Api\Xml\Manufacturer Object
(
[name] => Samsung Group
[website] => http://www.samsung.com
)
)
)
# 400 Bad request
{
"errors": {
"images[1].uri": "The image uri is required.",
"datetime": "The datetime must be in yyyy-mm-dd H:i:s format.",
"properties.made_in": "The made_in field cannot be longer than 100 character(s).",
"properties.colour": "The colour field is required.",
"properties.manufacturer.email_address": "The email_address must have a valid format."
}
}
This is a simple XML request payload and its validator class.
<?xml version="1.0" encoding="UTF-8"?>
<items>
<item>ABC</item>
<item>DEF</item>
<item>GHI</item>
</items>
/**
* @Serializer\XmlRoot("items")
*/
class Item
{
/**
* @var array[]
*
* @Assert\Type(type="array", message="You must provide an array of items.")
* @Assert\Count(
* max="5",
* maxMessage="You can provide maximum {{ limit }} item(s)."
* )
*
* @Serializer\XmlList(inline=true, entry="item")
* @Serializer\Type("array<string>")
*/
public $items = [];
}