16/08/2015 - SYMFONY
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 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 between each other 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.
Install bcc/auto-mapper-bundle
with composer and add new BCC\AutoMapperBundle\BCCAutoMapperBundle()
into AppKernel.php file.
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));
}
}
services:
application_frontend.controller.football:
class: Application\FrontendBundle\Controller\FootballController
arguments:
- @application_frontend.service.football
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;
......
......
......
}
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;
......
......
......
}
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;
......
......
......
}
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 = [];
}
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 = [];
}
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;
}
This is important! If the property names of entities don't match the property names of corresponding model or if you need a kind of manual interaction with a property then you'll end up creating dedicated mapper classes for each model class. In this example, they all match each other so I can use only one without using manual mapping at all.
namespace Application\FrontendBundle\Model;
use Application\FrontendBundle\Entity\League as LeagueEntity;
use Application\FrontendBundle\Model\League as LeagueModel;
use Application\FrontendBundle\Entity\Team as TeamEntity;
use Application\FrontendBundle\Model\Team as TeamModel;
use Application\FrontendBundle\Entity\Player as PlayerEntity;
use Application\FrontendBundle\Model\Player as PlayerModel;
interface FootballMapperInterface
{
/**
* @param LeagueEntity|TeamEntity|PlayerEntity $source
* @param LeagueModel|TeamModel|PlayerModel $destination
*
* @return LeagueModel|TeamModel|PlayerModel
*/
public function map($source, $destination);
}
namespace Application\FrontendBundle\Model;
use BCC\AutoMapperBundle\Mapper\Mapper;
class FootballMapper implements FootballMapperInterface
{
private $mapper;
public function __construct(Mapper $mapper)
{
$this->mapper = $mapper;
}
public function map($source, $destination)
{
$this->mapper->createMap(get_class($source), get_class($destination));
return $this->mapper->map($source, $destination);
}
}
services:
application_frontend.mapper.football:
class: Application\FrontendBundle\Model\FootballMapper
arguments:
- @bcc_auto_mapper.mapper
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();
}
}
services:
application_frontend.repository.league:
class: Application\FrontendBundle\Repository\LeagueRepository
factory: [@doctrine.orm.entity_manager, getRepository]
arguments: [Application\FrontendBundle\Entity\League]
namespace Application\FrontendBundle\Service;
interface FootballServiceInterface
{
public function getAll();
}
namespace Application\FrontendBundle\Service;
use Application\FrontendBundle\Factory\FootballFactoryInterface;
use Application\FrontendBundle\Repository\LeagueRepository;
class FootballService implements FootballServiceInterface
{
private $leagueRepository;
private $footballFactory;
public function __construct(
LeagueRepository $leagueRepository,
FootballFactoryInterface $footballFactory
) {
$this->leagueRepository = $leagueRepository;
$this->footballFactory = $footballFactory;
}
public function getAll()
{
$leagues = $this->leagueRepository->findAll();
$result = $this->footballFactory->createLeagueResult($leagues);
return $result;
}
}
services:
application_frontend.service.football:
class: Application\FrontendBundle\Service\FootballService
arguments:
- @application_frontend.repository.league
- @application_frontend.factory.football
namespace Application\FrontendBundle\Factory;
interface FootballFactoryInterface
{
public function createLeagueResult(array $leagues = []);
}
namespace Application\FrontendBundle\Factory;
use Application\FrontendBundle\Entity\Player as PlayerEntity;
use Application\FrontendBundle\Model\FootballMapperInterface;
use Application\FrontendBundle\Model\Player as PlayerModel;
use Application\FrontendBundle\Entity\Team as TeamEntity;
use Application\FrontendBundle\Model\Team as TeamModel;
use Application\FrontendBundle\Entity\League as LeagueEntity;
use Application\FrontendBundle\Model\League as LeagueModel;
class FootballFactory implements FootballFactoryInterface
{
private $footballMapper;
public function __construct(
FootballMapperInterface $footballMapper
) {
$this->footballMapper = $footballMapper;
}
public function createLeagueResult(array $leagues = [])
{
$result = [];
/** @var LeagueEntity $league */
foreach ($leagues as $league) {
$leagueModel = $this->footballMapper->map($league, new LeagueModel());
/** @var TeamEntity $team */
foreach ($league->getTeam() as $team) {
$teamModel = $this->footballMapper->map($team, new TeamModel());
$leagueModel->teams[] = $teamModel;
/** @var PlayerEntity $player */
foreach ($team->getPlayer() as $player) {
$playerModel = $this->footballMapper->map($player, new PlayerModel());
$teamModel->players[] = $playerModel;
}
}
$result[] = $leagueModel;
}
return $result;
}
}
services:
application_frontend.factory.football:
class: Application\FrontendBundle\Factory\FootballFactory
arguments:
- @application_frontend.mapper.football
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)
Endpoint to call: GET http://football.dev/app_dev.php/football
{
{
"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
}
]
}
]
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] =>
)
)
)
)