If you feel lazy to map class properties manually then you can use BCCAutoMapper to do it for you automatically. It also makes code look cleaner by hiding the logic. In example below, we're going to map model classes to actual entity classes for one-to-many association. 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

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 Application\BackendBundle\Entity\Message;
use Application\BackendBundle\Entity\Student;
use Application\BackendBundle\Model\Student\StudentModel;
use BCC\AutoMapperBundle\Mapper\FieldAccessor\Closure;
use BCC\AutoMapperBundle\Mapper\Mapper;
use Doctrine\ORM\EntityManager;
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
{
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("/student_message")
*
* @return JsonResponse|Response
*/
public function studentMessageAction(Request $request)
{
$format = $this->validateContentType($request->headers->get('content_type'));
if ($format instanceof Response) {
return $format;
}

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

//print_r($studentModel);exit;

// START of MAPPING
// Student
$this->mapper->createMap(
'Application\BackendBundle\Model\Student\StudentModel',
'Application\BackendBundle\Entity\Student'
)
// Entity property "id" does not exist in model so ignore it
->ignoreMember('id')
// We like to store entity property "schoolId" as upper case in DB
->forMember('schoolId', new Closure(function(StudentModel $source){
return strtoupper($source->schoolId);
}))
// Entity property "fullName" is associated with model property "name" so map them manually
->route('fullName', 'name')
// We have to handle entity relationship property "message" manually with its own mapping
->ignoreMember('message')
->setSkipNull(true);

$student = new Student();
$this->mapper->map($studentModel, $student);

// Message
$this->mapper->createMap(
'Application\BackendBundle\Model\Student\MessageModel',
'Application\BackendBundle\Entity\Message'
)
// Entity property "id" does not exist in model so ignore it
->ignoreMember('id')
// Entity property "fullName" is associated with model property "name" so map them manually
->route('title', 'subject')
// Entity property "note" is associated with model property "additionalNote" so map them manually
->route('note', 'additionalNote')
// Entity property "open" is associated with model property "isOpen" so map them manually
->route('open', 'isOpen')
->setSkipNull(true);

$message = new Message();
$this->mapper->map($studentModel->message, $message);

//print_r($student);
//print_r($message);

// END of MAPPING

$message->setStudent($student);
$this->entityManager->persist($student);
$this->entityManager->persist($message);
$this->entityManager->flush();

//print_r($studentModel);

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

Student entity


namespace Application\BackendBundle\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

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

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

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

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

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

/**
* @ORM\OneToMany(targetEntity="Message", mappedBy="student", cascade={"persist", "remove"})
*/
protected $message;

/**
* Constructor
*/
public function __construct()
{
$this->message = new ArrayCollection();
}

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

/**
* @param string $schoolId
* @return Student
*/
public function setSchoolId($schoolId)
{
$this->schoolId = $schoolId;

return $this;
}

/**
* @return string
*/
public function getSchoolId()
{
return $this->schoolId;
}

/**
* @param string $fullName
* @return Student
*/
public function setFullName($fullName)
{
$this->fullName = $fullName;

return $this;
}

/**
* @return string
*/
public function getFullName()
{
return $this->fullName;
}

/**
* @param string $nickname
* @return Student
*/
public function setNickname($nickname)
{
$this->nickname = $nickname;

return $this;
}

/**
* @return string
*/
public function getNickname()
{
return $this->nickname;
}

/**
* @param string $gender
* @return Student
*/
public function setGender($gender)
{
$this->gender = $gender;

return $this;
}

/**
* @return string
*/
public function getGender()
{
return $this->gender;
}

/**
* @param Message $message
* @return Student
*/
public function addMessage(Message $message)
{
$this->message[] = $message;

return $this;
}

/**
* @param Message $message
*/
public function removeMessage(Message $message)
{
$this->message->removeElement($message);
}

/**
* @return Collection
*/
public function getMessage()
{
return $this->message;
}
}

Message entity


namespace Application\BackendBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

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

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

/**
* @var string
*
* @ORM\Column(name="body", type="text")
*/
protected $body;

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

/**
* @var string
*
* @ORM\Column(name="is_open", type="boolean")
*/
protected $open;

/**
* @ORM\ManyToOne(targetEntity="Student", inversedBy="message")
* @ORM\JoinColumn(name="student_id", referencedColumnName="id", onDelete="CASCADE", nullable=false)
*/
protected $student;

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

/**
* @param string $title
* @return Message
*/
public function setTitle($title)
{
$this->title = $title;

return $this;
}

/**
* @return string
*/
public function getTitle()
{
return $this->title;
}

/**
* @param string $body
* @return Message
*/
public function setBody($body)
{
$this->body = $body;

return $this;
}

/**
* @return string
*/
public function getBody()
{
return $this->body;
}

/**
* @param string $note
* @return Message
*/
public function setNote($note)
{
$this->note = $note;

return $this;
}

/**
* @return string
*/
public function getNote()
{
return $this->note;
}

/**
* @param string $open
* @return Message
*/
public function setOpen($open)
{
$this->open = $open;

return $this;
}

/**
* @return string
*/
public function getOpen()
{
return $this->open;
}

/**
* @param Student $student
* @return Message
*/
public function setStudent(Student $student)
{
$this->student = $student;

return $this;
}

/**
* @return Student
*/
public function getStudent()
{
return $this->student;
}
}

StudentModel class


namespace Application\BackendBundle\Model\Student;

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

/**
* @Serializer\XmlRoot("student")
*/
class StudentModel
{
/**
* @var string
*
* @Assert\NotBlank(message="The SchoolID field is required.")
*
* @Serializer\Type("string")
*/
public $schoolId;

/**
* @var string
*
* @Assert\NotBlank(message="The Name field is required.")
*
* @Serializer\Type("string")
*/
public $name;

/**
* @var string
*
* @Serializer\Type("string")
*/
public $nickname;

/**
* @var string
*
* @Serializer\Type("string")
*/
public $gender;

/**
* @var MessageModel
*
* @Assert\NotBlank(message="The Message field is required.")
* @Assert\Valid(traverse="true")
*
* @Serializer\Type("Application\BackendBundle\Model\Student\MessageModel")
*/
public $message;
}

MessageModel class


namespace Application\BackendBundle\Model\Student;

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

class MessageModel
{
/**
* @var string
*
* @Assert\NotBlank(message="The Subject field is required.")
*
* @Serializer\Type("string")
*/
public $subject;

/**
* @var string
*
* @Assert\NotBlank(message="The Body field is required.")
*
* @Serializer\Type("string")
*/
public $body;

/**
* @var string
*
* @Serializer\Type("string")
*/
public $additionalNote;

/**
* @var bool
*
* @Serializer\Type("boolean")
*/
public $isOpen = true;
}

Request payload


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


{
"school_id": "abc123",
"name": "Inanzzz Blog",
"nickname": "hello",
"gender": "M",
"message": {
"subject": "Test",
"body": "This is a test message",
"additional_note": "No note",
"is_open": true
}
}

Mapping result


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


// print_r($studentModel);
Application\BackendBundle\Model\Student\StudentModel Object
(
[schoolId] => abc123
[name] => Inanzzz Blog
[nickname] => hello
[gender] => M
[message] => Application\BackendBundle\Model\Student\MessageModel Object
(
[subject] => Test
[body] => This is a test message
[additionalNote] => No note
[isOpen] => 1
)

)

// print_r($student);
Application\BackendBundle\Entity\Student Object
(
[id:protected] =>
[schoolId:protected] => ABC123
[fullName:protected] => Inanzzz Blog
[nickname:protected] => hello
[gender:protected] => M
[message:protected] => Doctrine\Common\Collections\ArrayCollection Object
(
[elements:Doctrine\Common\Collections\ArrayCollection:private] => Array
(
)
)
)

// print_r($message);
Application\BackendBundle\Entity\Message Object
(
[id:protected] =>
[title:protected] => Test
[body:protected] => This is a test message
[note:protected] => No note
[open:protected] => 1
[student:protected] =>
)

Database result


mysql> SELECT student.*, message.* FROM student INNER JOIN message ON message.student_id = student.id;
Empty set (0.00 sec)

mysql> SELECT student.*, message.* FROM student INNER JOIN message ON message.student_id = student.id;
+----+----------+--------------+----------+--------+----+------------+-------+------------------------+---------+---------+
| id | schoolId | full_name | nickname | gender | id | student_id | title | body | note | is_open |
+----+----------+--------------+----------+--------+----+------------+-------+------------------------+---------+---------+
| 1 | ABC123 | Inanzzz Blog | hello | M | 1 | 1 | Test | This is a test message | No note | 1 |
+----+----------+--------------+----------+--------+----+------------+-------+------------------------+---------+---------+
1 row in set (0.00 sec)