We can create a map class to map models to entities behind the scene to make out code clean and isolated. For more information about BCCAutoMapperBundle, visit GitHub page.


Composer.json


Install bcc/auto-mapper-bundle with composer.


AppKernel.php


Register new BCC\AutoMapperBundle\BCCAutoMapperBundle() in your kernel.


Controllers.yml


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
arguments:
- @doctrine.orm.entity_manager
- @bcc_auto_mapper.mapper

Maps.yml


services:
application_backend.map.address:
class: Application\BackendBundle\Model\AddressMap
tags:
- { name: bcc_auto_mapper.map }

AbstractController.php


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.php


In real life example, the most of the logic below should live in service and factory classes. Controller must be as "thin" as possible but for now I'm doing all in controller just for demonstration purposes.


namespace Application\BackendBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Application\BackendBundle\Entity\Address;
use BCC\AutoMapperBundle\Mapper\Mapper;
use Doctrine\ORM\EntityManager;
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
{
private $entityManager;
private $mapper;

public function __construct(
SerializerInterface $serializer,
ValidatorInterface $validator,
Inflector $inflector,
EntityManager $entityManager,
Mapper $mapper
) {
parent::__construct($serializer, $validator, $inflector);

$this->entityManager = $entityManager;
$this->mapper = $mapper;
}

/**
* @param Request $request
*
* @Method({"POST"})
* @Route("/address")
*
* @return JsonResponse|Response
*/
public function addressAction(Request $request)
{
$format = $this->validateContentType($request->headers->get('content_type'));
if ($format instanceof Response) {
return $format;
}

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

$addressEntity = new Address();
$this->mapper->map($address, $addressEntity);

$this->entityManager->persist($addressEntity);
$this->entityManager->flush();

//print_r($address); exit;
//print_r($addressEntity); exit;

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

Address entity


namespace Application\BackendBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
* @ORM\Entity
* @ORM\Table(name="address")
*/
class Address
{
/**
* @var int
*
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;

/**
* @var string
*
* @ORM\Column(name="building_number", type="string", length=10)
*/
protected $buildingNumber;

/**
* @var string
*
* @ORM\Column(name="flatNumber", type="string", length=10, nullable=true)
*/
protected $flatNumber;

/**
* @var string
*
* @ORM\Column(name="line1", type="string", length=100)
*/
protected $line1;

/**
* @var string
*
* @ORM\Column(name="line2", type="string", length=100, nullable=true)
*/
protected $line2;

/**
* @var string
*
* @ORM\Column(name="town", type="string", length=100, nullable=true)
*/
protected $town;

/**
* @var string
*
* @ORM\Column(name="city", type="string", length=100)
*/
protected $city;

/**
* @var string
*
* @ORM\Column(name="postcode", type="string", length=10)
*/
protected $postcode;

/**
* @var string
*
* @ORM\Column(name="country", type="string", length=100)
*/
protected $country;

/**
* @var string
*
* @ORM\Column(name="tenant", type="json_array", nullable=true)
*/
protected $tenants;

/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}

/**
* Set buildingNumber
*
* @param integer $buildingNumber
* @return Address
*/
public function setBuildingNumber($buildingNumber)
{
$this->buildingNumber = $buildingNumber;

return $this;
}

/**
* Get buildingNumber
*
* @return integer
*/
public function getBuildingNumber()
{
return $this->buildingNumber;
}

/**
* Set flatNumber
*
* @param integer $flatNumber
* @return Address
*/
public function setFlatNumber($flatNumber)
{
$this->flatNumber = $flatNumber;

return $this;
}

/**
* Get flatNumber
*
* @return integer
*/
public function getFlatNumber()
{
return $this->flatNumber;
}

/**
* Set line1
*
* @param string $line1
* @return Address
*/
public function setLine1($line1)
{
$this->line1 = $line1;

return $this;
}

/**
* Get line1
*
* @return string
*/
public function getLine1()
{
return $this->line1;
}

/**
* Set line2
*
* @param string $line2
* @return Address
*/
public function setLine2($line2)
{
$this->line2 = $line2;

return $this;
}

/**
* Get line2
*
* @return string
*/
public function getLine2()
{
return $this->line2;
}

/**
* Set line3
*
* @param string $line3
* @return Address
*/
public function setLine3($line3)
{
$this->line3 = $line3;

return $this;
}

/**
* Get line3
*
* @return string
*/
public function getLine3()
{
return $this->line3;
}

/**
* Set town
*
* @param string $town
* @return Address
*/
public function setTown($town)
{
$this->town = $town;

return $this;
}

/**
* Get town
*
* @return string
*/
public function getTown()
{
return $this->town;
}

/**
* Set postcode
*
* @param string $postcode
* @return Address
*/
public function setPostcode($postcode)
{
$this->postcode = $postcode;

return $this;
}

/**
* Get postcode
*
* @return string
*/
public function getPostcode()
{
return $this->postcode;
}

/**
* Set country
*
* @param string $country
* @return Address
*/
public function setCountry($country)
{
$this->country = $country;

return $this;
}

/**
* Get country
*
* @return string
*/
public function getCountry()
{
return $this->country;
}

/**
* Set city
*
* @param string $city
* @return Address
*/
public function setCity($city)
{
$this->city = $city;

return $this;
}

/**
* Get city
*
* @return string
*/
public function getCity()
{
return $this->city;
}

/**
* Set tenants
*
* @param array $tenants
* @return Address
*/
public function setTenants($tenants)
{
$this->tenants = $tenants;

return $this;
}

/**
* Get tenants
*
* @return array
*/
public function getTenants()
{
return $this->tenants;
}
}

Address model class


namespace Application\BackendBundle\Model;

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

/**
* @Serializer\XmlRoot("address")
*/
class Address
{
/**
* @var string
*
* @Assert\NotBlank(message="The building_no field is required.")
* @Assert\Length(
* max=10,
* maxMessage="The building_no field cannot be longer than {{ limit }} characters."
* )
*
* @Serializer\Type("string")
*/
public $buildingNo;

/**
* @var string
*
* @Assert\Length(
* max=10,
* maxMessage="The flat_number field cannot be longer than {{ limit }} characters."
* )
*
* @Serializer\Type("string")
*/
public $flatNumber;

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

/**
* @var string
*
* @Assert\Length(
* max=100,
* maxMessage="The line2 field cannot be longer than {{ limit }} characters."
* )
*
* @Serializer\Type("string")
*/
public $line2;

/**
* @var string
*
* @Assert\Length(
* max=100,
* maxMessage="The town field cannot be longer than {{ limit }} characters."
* )
*
* @Serializer\Type("string")
*/
public $town;

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

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

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

/**
* @var Tenant[]
*
* @Assert\Valid(traverse="true")
* @Serializer\XmlList(inline=false, entry="tenants")
*
* @Serializer\Type("array<Application\BackendBundle\Model\Tenant>")
*/
public $tenants = [];
}

Tenants model class


namespace Application\BackendBundle\Model;

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

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

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

AddressMap mapping class


namespace Application\BackendBundle\Model;

use BCC\AutoMapperBundle\Mapper\AbstractMap;
use BCC\AutoMapperBundle\Mapper\FieldAccessor\Closure;

class AddressMap extends AbstractMap
{
function __construct()
{
$this
->buildDefaultMap()
->ignoreMember('id')
->route('buildingNumber', 'buildingNo')
->forMember('postcode', new Closure(function(Address $source) {
return strtoupper($source->postCode);
}))
->route('tenants', 'tenants')
->setSkipNull(true);
}

public function getDestinationType()
{
return 'Application\BackendBundle\Entity\Address';
}

public function getSourceType()
{
return 'Application\BackendBundle\Model\Address';
}
}

Request payload


Since this example accepts Json and XML payloads, you can use XML version if you want to.


{
"building_no": 77,
"flat_number": 3,
"line1": "Palm Close",
"line2": "Long Lane",
"town": "London Bridge",
"city": "London",
"post_code": "EC1 2CE",
"country": "United Kingdom",
"tenants": [
{
"name": "Robert",
"surname": "DeNiro"
},
{
"name": "Al",
"surname": "Pacino"
}
]
}

Mapping result


If you sent payload above to http://...../api/address address, you'll get result below.


// print_r($address);
Application\BackendBundle\Model\Address Object
(
[buildingNo] => 77
[flatNumber] => 3
[line1] => Palm Close
[line2] => Long Lane
[town] => London Bridge
[city] => London
[postCode] => EC1 2CE
[country] => United Kingdom
[tenants] => Array
(
[0] => Application\BackendBundle\Model\Tenant Object
(
[name] => Robert
[surname] => DeNiro
)

[1] => Application\BackendBundle\Model\Tenant Object
(
[name] => Al
[surname] => Pacino
)
)
)

// print_r($addressEntity);
Application\BackendBundle\Entity\Address Object
(
[id:protected] => 3
[buildingNumber:protected] => 77
[flatNumber:protected] => 3
[line1:protected] => Palm Close
[line2:protected] => Long Lane
[town:protected] => London Bridge
[city:protected] => London
[postcode:protected] => EC1 2CE
[country:protected] => United Kingdom
[tenants:protected] => Array
(
[0] => Application\BackendBundle\Model\Tenant Object
(
[name] => Robert
[surname] => DeNiro
)

[1] => Application\BackendBundle\Model\Tenant Object
(
[name] => Al
[surname] => Pacino
)
)
)

Database result


mysql> SELECT * FROM address;
+----+-----------------+------------+------------+-----------+---------------+----------+----------------+--------+-------------------------------------------------------------------------+
| id | building_number | flatNumber | line1 | line2 | town | postcode | country | city | tenant |
+----+-----------------+------------+------------+-----------+---------------+----------+----------------+--------+-------------------------------------------------------------------------+
| 1 | 77 | 3 | Palm Close | Long Lane | London Bridge | EC1 2CE | United Kingdom | London | [{"name":"Robert","surname":"DeNiro"},{"name":"Al","surname":"Pacino"}] |
+----+-----------------+------------+------------+-----------+---------------+----------+----------------+--------+-------------------------------------------------------------------------+
1 row in set (0.00 sec)