Aşağıdaki örnekte olduğu gibi, kodumuzu temiz tutmak için bir tane map servis class oluşturup, model ve entity ilişkilendirme işlemini gizli olarak yapabiliriz. BCCAutoMapperBundle hakkında daha fazla bilgi için, GitHub sayfasını ziyaret edebilirsiniz.


Composer.json


Composer ile bcc/auto-mapper-bundle paketini yükleyin.


AppKernel.php


Kernel içinde new BCC\AutoMapperBundle\BCCAutoMapperBundle() kaydını yapın.


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


Gerçek hayatta aşağıdaki işlemlerin çoğu, servis ve factory classlar içinde yapılır. Controller mümkün olduğunca sade olmalıdır ama şu an için bu tavsiyeye uymayacağım.


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 = [];
}

Tenant 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


Bu örnek Json ve XML isteklerini kabul edebildiği için, siz isterseniz XML ile deneme yapabilirsiniz.


{
"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 sonucu


Eğer yukarıdaki isteği http://...../api/address adresine gönderirseniz, aşağıdaki sonucu alırsınız.


// 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 sonucu


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)