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.

When we respond to a request, we sometimes get the data from database and map it to a model class before returning as part of response. This can be a very daunting job if the model has many fields because we would do all mapping process manually. To avoid headaches, we can use BCCAutoMapperBundle to do whole mapping automatically behind the scene.

In the example below, we have three entities and there is one-to-many relationship in each one so League (1 to n) Team (1 to n) Player. In the response, League object will contain its own properties and Team object embedded into it, Team object will contain its own properties and Player object embedded into it.


Setup


Install bcc/auto-mapper-bundle with composer and add new BCC\AutoMapperBundle\BCCAutoMapperBundle() into AppKernel.php file.


FootballController


namespace Application\FrontendBundle\Controller;

use Application\FrontendBundle\Service\FootballServiceInterface;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;

/**
* @Route("football", service="application_frontend.controller.football")
*/
class FootballController extends Controller
{
private $footballService;

public function __construct(
FootballServiceInterface $footballService
) {
$this->footballService = $footballService;
}

/**
* @Route("")
* @Method({"GET"})
*/
public function indexAction()
{
$league = $this->footballService->getAll();

return new Response(json_encode($league));
}
}

Controllers.yml


services:
application_frontend.controller.football:
class: Application\FrontendBundle\Controller\FootballController
arguments:
- @application_frontend.service.football

Entities


League


namespace Application\FrontendBundle\Entity;

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

/**
* @ORM\Entity(repositoryClass="Application\FrontendBundle\Repository\LeagueRepository")
* @ORM\Table(name="league")
*/
class League
{
/**
* @var int
*
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;

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

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

/**
* @ORM\OneToMany(targetEntity="Team", mappedBy="league", cascade={"persist", "remove"})
*/
private $team;

......
......
......
}

Team


namespace Application\FrontendBundle\Entity;

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

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

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

/**
* @var string
*
* @ORM\Column(name="colours", type="json_array")
*/
private $colours;

/**
* @ORM\ManyToOne(targetEntity="League", inversedBy="team")
* @ORM\JoinColumn(name="league_id", referencedColumnName="id", onDelete="CASCADE", nullable=false)
*/
private $league;

/**
* @ORM\OneToMany(targetEntity="Player", mappedBy="team", cascade={"persist", "remove"})
*/
private $player;

......
......
......
}

Player


namespace Application\FrontendBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

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

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

/**
* @ORM\ManyToOne(targetEntity="Team", inversedBy="player")
* @ORM\JoinColumn(name="team_id", referencedColumnName="id", onDelete="CASCADE", nullable=false)
*/
private $team;

......
......
......
}

Models


League


namespace Application\FrontendBundle\Model;

use JMS\Serializer\Annotation as Serializer;

class League
{
/**
* @var int
*
* @Serializer\Type("integer")
*/
public $id;

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

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

/**
* @var Team[]
*
* @Serializer\Type("array<Application\FrontendBundle\Model\Team>")
*/
public $teams = [];
}

Team


namespace Application\FrontendBundle\Model;

use JMS\Serializer\Annotation as Serializer;

class Team
{
/**
* @var int
*
* @Serializer\Type("integer")
*/
public $id;

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

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

/**
* @var Player[]
*
* @Serializer\Type("array<Application\FrontendBundle\Model\Player>")
*/
public $players = [];
}

Player


namespace Application\FrontendBundle\Model;

use JMS\Serializer\Annotation as Serializer;

class Player
{
/**
* @var int
*
* @Serializer\Type("integer")
*/
public $id;

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

Mappers


LeagueMap


namespace Application\FrontendBundle\Model;

use Application\FrontendBundle\Entity\League as LeagueEntity;
use Application\FrontendBundle\Model\League as LeagueModel;
use BCC\AutoMapperBundle\Mapper\FieldAccessor\Closure;
use BCC\AutoMapperBundle\Mapper\Mapper;

class LeagueMap
{
const DESTINATION = 'Application\FrontendBundle\Model\League';

private $mapper;
private $teamMap;

public function __construct(
Mapper $mapper,
TeamMap $teamMap
) {
$this->mapper = $mapper;
$this->teamMap = $teamMap;
}

public function map(array $sources = [])
{
$leagues = null;

foreach ($sources as $source) {
$this
->mapper
->createMap(get_class($source), self::DESTINATION)
->forMember(
'teams',
new Closure(
function(LeagueEntity $source) {
return $this->teamMap->map($source->getTeam());
}
)
);

$leagues[] = $this->mapper->map($source, new LeagueModel());
}

return $leagues;
}
}

TeamMap


namespace Application\FrontendBundle\Model;

use Application\FrontendBundle\Entity\Team as TeamEntity;
use Application\FrontendBundle\Model\Team as TeamModel;
use BCC\AutoMapperBundle\Mapper\FieldAccessor\Closure;
use BCC\AutoMapperBundle\Mapper\Mapper;
use Doctrine\Common\Collections\Collection;

class TeamMap
{
const DESTINATION = 'Application\FrontendBundle\Model\Team';

private $mapper;
private $playerMap;

public function __construct(
Mapper $mapper,
PlayerMap $playerMap
) {
$this->mapper = $mapper;
$this->playerMap = $playerMap;
}

public function map(Collection $sources = null)
{
$teams = null;

foreach ($sources as $source) {
$this
->mapper
->createMap(get_class($source), self::DESTINATION)
->forMember(
'players',
new Closure(
function(TeamEntity $source) {
return $this->playerMap->map($source->getPlayer());
}
)
);

$teams[] = $this->mapper->map($source, new TeamModel());
}

return $teams;
}
}

PlayerMap


namespace Application\FrontendBundle\Model;

use Application\FrontendBundle\Model\Player as PlayerModel;
use BCC\AutoMapperBundle\Mapper\Mapper;
use Doctrine\Common\Collections\Collection;

class PlayerMap
{
const DESTINATION = 'Application\FrontendBundle\Model\Player';

private $mapper;

public function __construct(Mapper $mapper)
{
$this->mapper = $mapper;
}

public function map(Collection $sources = null)
{
$players = null;

foreach ($sources as $source) {
$this
->mapper
->createMap(get_class($source), self::DESTINATION);

$players[] = $this->mapper->map($source, new PlayerModel());
}

return $players;
}
}

Mappers.yml


services:
application_frontend.mapper.league:
class: Application\FrontendBundle\Model\LeagueMap
arguments:
- @bcc_auto_mapper.mapper
- @application_frontend.mapper.team

application_frontend.mapper.team:
class: Application\FrontendBundle\Model\TeamMap
arguments:
- @bcc_auto_mapper.mapper
- @application_frontend.mapper.player

application_frontend.mapper.player:
class: Application\FrontendBundle\Model\PlayerMap
arguments:
- @bcc_auto_mapper.mapper

LeagueRepository


namespace Application\FrontendBundle\Repository;

use Doctrine\ORM\EntityRepository;

class LeagueRepository extends EntityRepository
{
public function findAll()
{
return
$this
->createQueryBuilder('l')
->select('l, t, p')
->leftJoin('l.team', 't')
->leftJoin('t.player', 'p')
->getQuery()
->getResult();
}
}

Repositories.yml


services:
application_frontend.repository.league:
class: Application\FrontendBundle\Repository\LeagueRepository
factory: [@doctrine.orm.entity_manager, getRepository]
arguments: [Application\FrontendBundle\Entity\League]

FootballServiceInterface


namespace Application\FrontendBundle\Service;

interface FootballServiceInterface
{
public function getAll();
}

FootballService


namespace Application\FrontendBundle\Service;

use Application\FrontendBundle\Factory\FootballFactoryInterface;
use Application\FrontendBundle\Repository\LeagueRepository;
use Doctrine\ORM\EntityManagerInterface;

class FootballService implements FootballServiceInterface
{
private $entityManager;
private $leagueRepository;
private $footballFactory;

public function __construct(
EntityManagerInterface $entityManager,
LeagueRepository $leagueRepository,
FootballFactoryInterface $footballFactory
) {
$this->entityManager = $entityManager;
$this->leagueRepository = $leagueRepository;
$this->footballFactory = $footballFactory;
}

public function getAll()
{
$leagues = $this->leagueRepository->findAll();
$result = $this->footballFactory->createLeagueResult($leagues);

return $result;
}
}

Services.yml


services:
application_frontend.service.football:
class: Application\FrontendBundle\Service\FootballService
arguments:
- @doctrine.orm.entity_manager
- @application_frontend.repository.league
- @application_frontend.factory.football

FootballFactoryInterface


namespace Application\FrontendBundle\Factory;

interface FootballFactoryInterface
{
public function createLeagueResult(array $leagues = []);
}

FootballFactory


namespace Application\FrontendBundle\Factory;

use Application\FrontendBundle\Model\LeagueMap;

class FootballFactory implements FootballFactoryInterface
{
private $leagueMap;

public function __construct(LeagueMap $leagueMap)
{
$this->leagueMap = $leagueMap;
}

public function createLeagueResult(array $leagues = [])
{
return $this->leagueMap->map($leagues);
}
}

Factories.yml


services:
application_frontend.factory.football:
class: Application\FrontendBundle\Factory\FootballFactory
arguments:
- @application_frontend.mapper.league

Database


mysql> SELECT * FROM league;
+----+----------------+----------------+
| id | name | country |
+----+----------------+----------------+
| 1 | Premier League | United Kingdom |
| 2 | Bundesliga | Germany |
+----+----------------+----------------+
2 rows in set (0.00 sec)

mysql> SELECT * FROM team;
+----+-----------+-------------------+------------------------+
| id | league_id | name | colours |
+----+-----------+-------------------+------------------------+
| 1 | 1 | Arsenal | ["red","white"] |
| 2 | 2 | Borussia Dortmund | ["yellow","black"] |
| 3 | 2 | Bayern Munich | ["red","white","navy"] |
| 4 | 2 | Werder Bremen | ["green","white"] |
+----+-----------+-------------------+------------------------+
4 rows in set (0.00 sec)

mysql> SELECT * FROM player;
+----+---------+---------------+
| id | team_id | name |
+----+---------+---------------+
| 1 | 1 | Mesut Ozil |
| 2 | 1 | Aaron Ramsey |
| 3 | 2 | Nuri Sahin |
| 4 | 3 | Thomas Muller |
| 5 | 3 | Philipp Lahm |
+----+---------+---------------+
5 rows in set (0.00 sec)

mysql> SELECT
-> league.id AS league_id,
-> league.name AS league_name,
-> league.country AS league_country,
-> team.id AS team_id,
-> team.name AS team_name,
-> team.colours AS team_colours,
-> player.id AS player_id,
-> player.name AS player_name
-> FROM league
-> LEFT JOIN team ON league.id = team.league_id
-> LEFT JOIN player ON team.id = player.team_id;
+-----------+----------------+----------------+---------+-------------------+------------------------+-----------+---------------+
| league_id | league_name | league_country | team_id | team_name | team_colours | player_id | player_name |
+-----------+----------------+----------------+---------+-------------------+------------------------+-----------+---------------+
| 1 | Premier League | United Kingdom | 1 | Arsenal | ["red","white"] | 1 | Mesut Ozil |
| 1 | Premier League | United Kingdom | 1 | Arsenal | ["red","white"] | 2 | Aaron Ramsey |
| 2 | Bundesliga | Germany | 2 | Borussia Dortmund | ["yellow","black"] | 3 | Nuri Sahin |
| 2 | Bundesliga | Germany | 3 | Bayern Munich | ["red","white","navy"] | 4 | Thomas Muller |
| 2 | Bundesliga | Germany | 3 | Bayern Munich | ["red","white","navy"] | 5 | Philipp Lahm |
| 2 | Bundesliga | Germany | 4 | Werder Bremen | ["green","white"] | NULL | NULL |
+-----------+----------------+----------------+---------+-------------------+------------------------+-----------+---------------+
6 rows in set (0.00 sec)

Test


Endpoint to call: GET http://football.dev/app_dev.php/football


Actual response


[
{
"id": 1,
"name": "Premier League",
"country": "United Kingdom",
"teams": [
{
"id": 1,
"name": "Arsenal",
"colours": [
"red",
"white"
],
"players": [
{
"id": 1,
"name": "Mesut Ozil"
},
{
"id": 2,
"name": "Aaron Ramsey"
}
]
}
]
},
{
"id": 2,
"name": "Bundesliga",
"country": "Germany",
"teams": [
{
"id": 2,
"name": "Borussia Dortmund",
"colours": [
"yellow",
"black"
],
"players": [
{
"id": 3,
"name": "Nuri Sahin"
}
]
},
{
"id": 3,
"name": "Bayern Munich",
"colours": [
"red",
"white",
"navy"
],
"players": [
{
"id": 4,
"name": "Thomas Muller"
},
{
"id": 5,
"name": "Philipp Lahm"
}
]
},
{
"id": 4,
"name": "Werder Bremen",
"colours": [
"green",
"white"
],
"players": null
}
]
}
]

Mapping output


Array
(
[0] => Application\FrontendBundle\Model\League Object
(
[id] => 1
[name] => Premier League
[country] => United Kingdom
[teams] => Array
(
[0] => Application\FrontendBundle\Model\Team Object
(
[id] => 1
[name] => Arsenal
[colours] => Array
(
[0] => red
[1] => white
)
[players] => Array
(
[0] => Application\FrontendBundle\Model\Player Object
(
[id] => 1
[name] => Mesut Ozil
)
[1] => Application\FrontendBundle\Model\Player Object
(
[id] => 2
[name] => Aaron Ramsey
)
)
)
)
)
[1] => Application\FrontendBundle\Model\League Object
(
[id] => 2
[name] => Bundesliga
[country] => Germany
[teams] => Array
(
[0] => Application\FrontendBundle\Model\Team Object
(
[id] => 2
[name] => Borussia Dortmund
[colours] => Array
(
[0] => yellow
[1] => black
)
[players] => Array
(
[0] => Application\FrontendBundle\Model\Player Object
(
[id] => 3
[name] => Nuri Sahin
)
)
)
[1] => Application\FrontendBundle\Model\Team Object
(
[id] => 3
[name] => Bayern Munich
[colours] => Array
(
[0] => red
[1] => white
[2] => navy
)
[players] => Array
(
[0] => Application\FrontendBundle\Model\Player Object
(
[id] => 4
[name] => Thomas Muller
)
[1] => Application\FrontendBundle\Model\Player Object
(
[id] => 5
[name] => Philipp Lahm
)
)
)
[2] => Application\FrontendBundle\Model\Team Object
(
[id] => 4
[name] => Werder Bremen
[colours] => Array
(
[0] => green
[1] => white
)
[players] =>
)
)
)
)