Open xavier-rodet opened 5 years ago
@xavier-rodet What you are trying to do is not in line with the REST assumptions. Skill Parameter is not included in Car resurce. You should create endpoint for resource RelationCarDriver and in list create Filters for Car and Driver. On this list you can return subresources Car and Driver.
For example: GET /relation_car_drivers?cars.id[]=2 :
{
"@context": "/api/contexts/RelationCarDriver",
"@id": "/api/relation_car_drivers",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/api/relation_car_drivers/1",
"@type": "RelationCarDriver",
"id": 1,
"car": {
"@id": "/api/cars/2",
"@type": "Car",
"id": 2,
"model": "Chevrolet"
},
"driver": {
"@id": "/api/driver/7",
"@type": "Driver",
"id": 7,
"name": "Kubica"
},
"skill": 63
}
],
"hydra:totalItems": 1
}
Yes you're right, i realized it myself after a while ...
But i don't have any interest to access this from relation_car_driver resource, i prefer accessing relation_car_driver from /cars/id/drivers and /drivers/id/cars sub-resources !
Let me share what i've done to achieve that :
@ManyToMany
but @ManyToOne
relations (as i need to handle my extra-fields)function getRelationCarDriver()
to Car & Driver entities which represent a "fake" OneToOne relation (that i will populate myself within my Repositories)See my code below :
Entity Car :
<?php
# src/Entity/Car.php
namespace App\Entity;
/**
* @ORM\Table(name="Car")
* @ORM\Entity(repositoryClass="App\Repository\CarRepository")
*
* @ApiResource(
* denormalizationContext={"groups"={"car:write"}},
* normalizationContext={"groups"={"car:read"}},
*
* collectionOperations = {
* "post",
* "get",
* "get_driver_cars"={
* "method"="GET",
* "path"="/drivers/{id}/cars",
* "controller"=App\Controller\ReadDriverCars::class,
* "read"=false,
* "normalization_context"={"groups"={"car:read", "driverCar:read"}}
* }
* },
*
* itemOperations={
* "get",
* "delete",
* },
* )
*/
class Car
{
/**
* @ORM\OneToMany(targetEntity="App\Entity\RelationCarDriver", mappedBy="car", orphanRemoval=true, cascade={"persist"})
*/
private $relationsCarDriver;
// This is added manually to access our relation extra fields (with setters & getters)
/**
* @Groups({"driverCar:read"})
*/
private $relationCarDriver;
// ...
}
Entity Driver :
<?php
# src/Entity/Driver.php
namespace App\Entity;
/**
* @ORM\Table(name="Driver")
* @ORM\Entity(repositoryClass="App\Repository\DriverRepository")
*
* @ApiResource(
* denormalizationContext={"groups"={"driver:write"}},
* normalizationContext={"groups"={"driver:read"}},
*
* collectionOperations = {
* "post",
* "get",
* "get_car_drivers"={
* "method"="GET",
* "path"="/cars/{id}/drivers",
* "controller"=App\Controller\ReadCarDrivers::class,
* "read"=false,
* "normalization_context"={"groups"={"carDriver:read", "carDriver:read"}}
* }
* },
*
* itemOperations={
* "get",
* "delete",
* },
* )
*/
class Driver
{
/**
* @ORM\OneToMany(targetEntity="App\Entity\RelationCarDriver", mappedBy="driver", orphanRemoval=true, cascade={"persist"})
*/
private $relationsCarDriver;
// This is added manually to access our relation extra fields (with setters & getters)
/**
* @Groups({"carDriver:read"})
*/
private $relationCarDriver;
// ...
}
Entity RelationCarDriver :
<?php
namespace App\Entity;
/**
* RelationCarDriver
*
* @ORM\Table(name="Relation_Car_Driver")
* @ORM\Entity
*
* @ApiResource(
*
* collectionOperations = {},
* itemOperations={
* "get"={
* "method"="GET",
* "path"="/relations_game_player/{id}",
* },
* },
* )
*/
class RelationCarDriver
{
/**
* @ORM\Column(name="skill", type="integer", nullable=false)
* @Groups({"carDriver:read", "driverCar:read"})
*/
private $skill;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Car", inversedBy="relationsCarDriver")
* @ORM\JoinColumn(name="car_id", nullable=false)
*/
private $car;
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Driver", inversedBy="relationsCarDriver")
* @ORM\JoinColumn(name="driver_id", nullable=false)
*/
private $driver;
// ...
}
Custom action for /drivers/:id/cars :
<?php
# src/Controller/ReadDriverCars.php
namespace App\Controller;
class ReadDriverCars
{
private $carRepository;
public function __construct(CarRepository $carRepository)
{
$this->carRepository= $carRepository;
}
public function __invoke(Request $data) : Paginator
{
$driverId = (int) $data->attributes->get('id');
$page = (int) $data->query->get('page', 1) ;
$cars= $this->carRepository->findByDriverId($driverId, $page);
return $cars;
}
}
Driver repository :
<?php
# src/Repository/CarRepository.php
namespace App\Repository;
/**
* @method Car|null find($id, $lockMode = null, $lockVersion = null)
* @method Car|null findOneBy(array $criteria, array $orderBy = null)
* @method Car[] findAll()
* @method Car[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class CarRepository extends ServiceEntityRepository
{
private $itemsPerPage;
public function __construct(RegistryInterface $registry, int $itemsPerPage)
{
parent::__construct($registry, Car::class);
$this->itemsPerPage = $itemsPerPage;
}
/**
* @param int $driverId
* @param int $page
* @return Paginator
*/
public function findByDriverId(int $driverId, int $page = 1) : Paginator
{
$driver = $this->getEntityManager()->find(Driver::class, $driverId);
$firstResult = ($page -1) * $this->itemsPerPage;
$queryBuilder = $this->createQueryBuilder('Car')
->select('Car, RCD')
->innerJoin('Car.relationsCarDriver', 'RCD')
->where('RCD.driver= :driver')
->setParameter('driver', $driver);
$query = $queryBuilder->getQuery()
->setFirstResult($firstResult)
->setMaxResults($this->itemsPerPage);
// Set the paginator
$doctrinePaginator = new DoctrinePaginator($query);
$paginator = new Paginator($doctrinePaginator);
// **Here we fullfill the "fake" OneToOne relation**
foreach($doctrinePaginator->getIterator() as $car) {
foreach($car->getRelationsCarDriver() as $relationCarDriver) {
if($relationCarDriver->getCar() === $car && $relationCarDriver->getDriver() === $driver) {
// set the relationCarDriver related to this Car & Driver
$car->setRelationCarDriver($relationCarDriver);
}
}
}
return $paginator;
}
}
This way, when i request /drivers/1/cars i got this :
{
"@context": "/api/contexts/Driver",
"@id": "/api/drivers/1/cars",
"@type": "hydra:Collection",
"hydra:member": [
{
"@id": "/api/cars/1",
"@type": "Car",
"id": 1,
"model": "Mustang",
"relationCarDriver": {
"@id": "/api/relations_car_driver/1",
"@type": "RelationCarDriver",
"skill": 63
}
},
{
"@id": "/api/cars/2",
"@type": "Car",
"id": 2,
"model": "Chevrolet",
"relationCarDriver": {
"@id": "/api/relations_car_driver/2",
"@type": "RelationCarDriver",
"skill": 13
}
}
],
"hydra:totalItems": 2
}
This might seems dirty, but this is the only way i found to achieve that and i think this respect REST guidelines ?
Please let me know if this you think there is something wrong about my method.
I need to display the "skill" of the "drivers" of specific "cars", which comes from a relational table "Relation_Car_Driver", this field should only appear for sub-resources calls "/cars/:id/drivers" and "/drivers/:id/cars"
I have 3 Tables :
My main API resources are :
My API sub resources are :
For those sub-resources calls i want to return the "skill" value coming from their relation !
Entity Car :
Entity Driver :
Entity Relation_Car_Driver :
Actual result from GET /drivers/:id/cars :
Expected result from GET /drivers/:id/cars :
I have no clue how to achieve that ? Thanks