Hello everyone!

We have been investing plenty of personal time and energy for many years to share our knowledge with you all. However, we now need your help to keep this blog running. All you have to do is just click one of the adverts on the site, otherwise it will sadly be taken down due to hosting etc. costs. Thank you.

In example below, the end-point accepts json and XML request payloads, validates against models, maps them into models and serialises the response content as json or XML then returns it. When preparing an example request payload, start designing XML first because json version would be easier to adapt into XML version. In the case of invalid request, json or XML 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.


Composer.json


You need to install "jms/serializer-bundle": "0.13.0" package.


Controllers.xml


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

Controllers


AbstractController


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()];
}
}

ApiController


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("/json_xml")
*
* @return JsonResponse|Response
*/
public function jsonXmlAction(Request $request)
{
$format = $this->validateContentType($request->headers->get('content_type'));
if ($format instanceof Response) {
return $format;
}

$car = $this->validatePayload(
$request->getContent(),
'Application\BackendBundle\Model\Api\JsonXml\Car',
$format
);
if ($car instanceof Response) {
return $car;
}

//print_r($car); exit;

return $this->createSuccessResponse($car, $format);
}
}

Models


Car


namespace Application\BackendBundle\Model\Api\JsonXml;

use Symfony\Component\Validator\Constraints as Assert;
use JMS\Serializer\Annotation as Serializer;

/**
* @Serializer\XmlRoot("car")
*/
class Car
{
/**
* @var string
*
* @Assert\NotBlank(message="The registration attribute is required.")
* @Assert\Length(
* max=10,
* maxMessage="The registration attribute cannot be longer than {{ limit }} character(s)."
* )
*
* @Serializer\Type("string")
* @Serializer\XmlAttribute()
*/
public $registration;

/**
* @var string
*
* @Assert\NotBlank(message="The make field is required.")
* @Assert\Length(
* max=50,
* maxMessage="The make field cannot be longer than {{ limit }} character(s)."
* )
*
* @Serializer\Type("string")
*/
public $make;

/**
* @var Model
*
* @Assert\Valid(traverse="true")
*
* @Serializer\Type("Application\BackendBundle\Model\Api\JsonXml\Model")
*/
public $model;

/**
* @var string
*
* @Assert\NotBlank(message="The year field is required.")
* @Assert\Length(
* min=4,
* max=4,
* exactMessage="The year field should have exactly {{ limit }} character(s)."
* )
*
* @Serializer\Type("string")
*/
public $year;

/**
* @var bool
*
* @Assert\NotBlank(message="The is_available field is required.")
* @Assert\Type(
* type="bool",
* message="The is_available field must contain only boolean value."
* )
*
* @Serializer\Type("boolean")
*/
public $isAvailable;

/**
* @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\JsonXml\Image>")
*/
public $images = [];

/**
* @var Cost
*
* @Assert\Valid(traverse="true")
*
* @Serializer\Type("Application\BackendBundle\Model\Api\JsonXml\Cost")
*/
public $cost;

/**
* @var Identifier[]
*
* @Assert\Valid(traverse="true")
*
* @Serializer\XmlList(inline=false, entry="identifier")
* @Serializer\Type("array<Application\BackendBundle\Model\Api\JsonXml\Identifier>")
*/
public $identifiers = [];

/**
* @var Property
*
* @Assert\Valid(traverse="true")
*
* @Serializer\Type("Application\BackendBundle\Model\Api\JsonXml\Property")
*/
public $properties;
}

Model


namespace Application\BackendBundle\Model\Api\JsonXml;

use Symfony\Component\Validator\Constraints as Assert;
use JMS\Serializer\Annotation as Serializer;

class Model
{
/**
* @var string
*
* @Assert\NotBlank(message="The series attribute is required.")
* @Assert\Length(
* max=10,
* maxMessage="The series attribute cannot be longer than {{ limit }} character(s)."
* )
*
* @Serializer\Type("string")
* @Serializer\XmlAttribute
*/
public $series;

/**
* @var string
*
* @Assert\NotBlank(message="The model value is required.")
* @Assert\Length(
* max=20,
* maxMessage="The model value cannot be longer than {{ limit }} character(s)."
* )
*
* @Serializer\Type("string")
* @Serializer\XmlValue
*/
public $value;
}

Image


namespace Application\BackendBundle\Model\Api\JsonXml;

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;
}

Cost


namespace Application\BackendBundle\Model\Api\JsonXml;

use Symfony\Component\Validator\Constraints as Assert;
use JMS\Serializer\Annotation as Serializer;

class Cost
{
/**
* @var string
*
* @Assert\NotBlank(message="The currency attribute is required.")
* @Assert\Regex(
* pattern="/^[A-Z]{3}$/",
* message="The currency attribute must comply with ISO 4217 standards."
* )
*
* @Serializer\Type("string")
* @Serializer\XmlAttribute
*/
public $currency;

/**
* @var float
*
* @Assert\NotBlank(message="The basic field is required.")
* @Assert\Regex(
* pattern="/^(?!0\d)\d{1,8}(\.[0-9]{1,2})?$/",
* message="The basic field must have a valid format."
* )
*
* @Serializer\Type("float")
*/
public $basic;

/**
* @var float
*
* @Assert\Regex(
* pattern="/^(?!0\d)\d{1,8}(\.[0-9]{1,2})?$/",
* message="The sport field must have a valid format."
* )
*
* @Serializer\Type("float")
*/
public $sport;
}

Identifier


namespace Application\BackendBundle\Model\Api\JsonXml;

use Symfony\Component\Validator\Constraints as Assert;
use JMS\Serializer\Annotation as Serializer;

class Identifier
{
/**
* @var string
*
* @Assert\NotBlank(message="The code attribute is required.")
* @Assert\Length(
* max=10,
* maxMessage="The code attribute cannot be longer than {{ limit }} character(s)."
* )
*
* @Serializer\Type("string")
* @Serializer\XmlAttribute
*/
public $code;

/**
* @var string
*
* @Assert\NotBlank(message="The identifier value is required.")
* @Assert\Length(
* max=10,
* maxMessage="The identifier value cannot be longer than {{ limit }} character(s)."
* )
*
* @Serializer\Type("string")
* @Serializer\XmlValue
*/
public $value;
}

Property


namespace Application\BackendBundle\Model\Api\JsonXml;

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\JsonXml\Manufacturer")
*/
public $manufacturer;
}

Manufacturer


namespace Application\BackendBundle\Model\Api\JsonXml;

use Symfony\Component\Validator\Constraints as Assert;
use JMS\Serializer\Annotation as Serializer;

class Manufacturer
{
/**
* @var string
*
* @Assert\NotBlank(message="The origin attribute is required.")
* @Assert\Length(
* max=50,
* maxMessage="The origin attribute cannot be longer than {{ limit }} character(s)."
* )
*
* @Serializer\Type("string")
* @Serializer\XmlAttribute
*/
public $origin;

/**
* @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;
}

Request payloads


Json


{
"registration": "AB15 CDE",
"make": "BMW",
"model": {
"series": "3.18",
"value": "Coupe"
},
"year": 2015,
"is_available": true,
"images": [
{
"uri": "http://www.inanzzz.com/123.jpg"
},
{
"uri": "http://www.inanzzz.com/456.jpg"
}
],
"cost": {
"currency": "GBP",
"basic": 29053.50,
"sport": 39269.99
},
"identifiers": [
{
"code": "BMW1",
"value": "3.18"
},
{
"code": "BMW2",
"value": "3.18TS"
}
],
"properties": {
"colour": "Silver",
"manufacturer": {
"origin": "Germany",
"name": "BMW Group",
"website": "http://www.bmw.com"
}
}
}

XML


<?xml version="1.0" encoding="UTF-8"?>
<car registration="AB15 CDE">
<make>BMW</make>
<model series="3.18">Coupe</model>
<year>2015</year>
<is_available>true</is_available>
<images>
<uri>http://www.inanzzz.com/123.jpg</uri>
<uri>http://www.inanzzz.com/456.jpg</uri>
</images>
<cost currency="GBP">
<basic>29053.50</basic>
<sport>39269.99</sport>
</cost>
<identifiers>
<identifier code="BMW1">3.18</identifier>
<identifier code="BMW2">3.18TS</identifier>
</identifiers>
<properties>
<colour>Silver</colour>
<manufacturer origin="Germany">
<name>BMW Group</name>
<website>http://www.bmw.com</website>
</manufacturer>
</properties>
</car>

Test


After POSTing the json or XML request payload above to http://football.local/app_dev.php/backend/api/json_xml and if we dump $car in controller just before returning the response, valid output would be like below.


Application\BackendBundle\Model\Api\JsonXml\Car Object
(
[registration] => AB15 CDE
[make] => BMW
[model] => Application\BackendBundle\Model\Api\JsonXml\Model Object
(
[series] => 3.18
[value] => Coupe
)
[year] => 2015
[isAvailable] => 1
[images] => Array
(
[0] => Application\BackendBundle\Model\Api\JsonXml\Image Object
(
[uri] => http://www.inanzzz.com/123.jpg
)
[1] => Application\BackendBundle\Model\Api\JsonXml\Image Object
(
[uri] => http://www.inanzzz.com/456.jpg
)
)
[cost] => Application\BackendBundle\Model\Api\JsonXml\Cost Object
(
[currency] => GBP
[basic] => 29053.5
[sport] => 39269.99
)
[identifiers] => Array
(
[0] => Application\BackendBundle\Model\Api\JsonXml\Identifier Object
(
[code] => BMW1
[value] => 3.18
)
[1] => Application\BackendBundle\Model\Api\JsonXml\Identifier Object
(
[code] => BMW2
[value] => 3.18TS
)
)
[properties] => Application\BackendBundle\Model\Api\JsonXml\Property Object
(
[colour] => Silver
[manufacturer] => Application\BackendBundle\Model\Api\JsonXml\Manufacturer Object
(
[origin] => Germany
[name] => BMW Group
[website] => http://www.bmw.com
)
)
)

Successful responses


Json


{
"registration": "AB15 CDE",
"make": "BMW",
"model": {
"series": "3.18",
"value": "Coupe"
},
"year": "2015",
"is_available": true,
"images": [
{
"uri": "http://www.inanzzz.com/123.jpg"
},
{
"uri": "http://www.inanzzz.com/456.jpg"
}
],
"cost": {
"currency": "GBP",
"basic": 29053.5,
"sport": 39269.99
},
"identifiers": [
{
"code": "BMW1",
"value": "3.18"
},
{
"code": "BMW2",
"value": "3.18TS"
}
],
"properties": {
"colour": "Silver",
"manufacturer": {
"origin": "Germany",
"name": "BMW Group",
"website": "http://www.bmw.com"
}
}
}

XML


<?xml version="1.0" encoding="UTF-8"?>
<car registration="AB15 CDE">
<make>
<![CDATA[BMW]]>
</make>
<model series="3.18">
<![CDATA[Coupe]]>
</model>
<year>
<![CDATA[2015]]>
</year>
<is_available>true</is_available>
<images>
<uri>
<![CDATA[http://www.inanzzz.com/123.jpg]]>
</uri>
<uri>
<![CDATA[http://www.inanzzz.com/456.jpg]]>
</uri>
</images>
<cost currency="GBP">
<basic>29053.5</basic>
<sport>39269.99</sport>
</cost>
<identifiers>
<identifier code="BMW1">
<![CDATA[3.18]]>
</identifier>
<identifier code="BMW2">
<![CDATA[3.18TS]]>
</identifier>
</identifiers>
<properties>
<colour>
<![CDATA[Silver]]>
</colour>
<manufacturer origin="Germany">
<name>
<![CDATA[BMW Group]]>
</name>
<website>
<![CDATA[http://www.bmw.com]]>
</website>
</manufacturer>
</properties>
</car>

Error responses


Json


{
"errors": {
"make": "The make field is required.",
"images": "You can provide maximum 2 image(s).",
"cost.currency": "The currency attribute must comply with ISO 4217 standards.",
"cost.basic": "The basic field is required.",
"identifiers[0].value": "The identifier value is required.",
"identifiers[1].code": "The code attribute is required.",
"properties.colour": "The colour field is required.",
"properties.manufacturer.name": "The name field is required."
}
}

XML


<?xml version="1.0" encoding="UTF-8"?>
<result>
<entry>
<entry>
<![CDATA[The make field is required.]]>
</entry>
<entry>
<![CDATA[You can provide maximum 2 image(s).]]>
</entry>
<entry>
<![CDATA[The currency attribute must comply with ISO 4217 standards.]]>
</entry>
<entry>
<![CDATA[The basic field is required.]]>
</entry>
<entry>
<![CDATA[The identifier value is required.]]>
</entry>
<entry>
<![CDATA[The code attribute is required.]]>
</entry>
<entry>
<![CDATA[The colour field is required.]]>
</entry>
<entry>
<![CDATA[The name field is required.]]>
</entry>
</entry>
</result>