Reputation: 315
I need to render some properties of two relational entities Salarie and Contrat, in a same twig template, basically from all the Salarie records but only from one specific Contrat attached to each Salarie.
Salarie Entity
namespace App\Entity;
class Salarie
{
// ...
/**
* @ORM\OneToMany(targetEntity="App\Entity\Contrat", mappedBy="salarie")
* @ORM\OrderBy({"dateDebut" = "DESC"})
*/
private $contrats;
//...
Contrat Entity
namespace App\Entity;
class Contrat
{
// ...
/**
* @ORM\ManyToOne(targetEntity="App\Entity\Salarie", inversedBy="contrats")
* @ORM\JoinColumn(nullable=false)
*/
private $salarie;
// ...
Salarie controller
class SalarieController extends AbstractController
{
/**
* @Route("/", name="salarie_index", methods={"GET"})
*/
public function index(SalarieRepository $salarieRepository): Response
{
return $this->render('salarie/index.html.twig', [
'salaries' => $salarieRepository->findAll(), //findAllWithLastContrat(),
]);
}
At first glance I thought this would be straightforward with a custom query in Salarie repository, but I've been fighting with joins, subqueries, and other stuff. Here's a pure Twig working solution , but it is not DRY at all, since I have to repeat it for each property , and I bet it has also a performance hit because I'm querying all the Contrat when I need only some...
<tbody class="list">
{% for salarie in salaries %}
<tr>
<td>{% for contrat in salarie.contrats %}
{% if loop.first %}
{{ contrat.departement }}
{% endif %}
{% endfor %}
</td>
<td>{% for contrat in salarie.contrats %}
{% if loop.first %}
{{ contrat.service }}
{% endif %}
{% endfor %}
</td>
</tr>
<!-- AND SO ON ABOUT 12 TIMES ! -->
{% endfor %}
I also tried a cool Feature from Doctrine (Criteria) as explained in Criteria System: Champion Collection Filtering
public function getLastContrat()
{
$criteria = Criteria::create()
->orderBy(['dateDebut' => 'DESC']);
->setMaxResults(1);
return $this->contrats->matching($criteria)->current();
}
then in Twig I can {{ dump(salarie.lastContrat) }}
returns the expected object.
But no way to get the properties from there. {{ salarie.lastContrat.someProperty }}
does not work.
Has to see with the fact that {{ salarie.lastContrat }}
prints what Contrat __toString method returns.
I won't expose more tries, so please my question is : How to render the properties values from above getLastContrat()
and what should be the most DRY and performant way to achieve this ?
Upvotes: 0
Views: 530
Reputation: 723
another thing you can do is to pass into the template another variable with the related object that you want to render (last contrat in your case). So, in your controller, you first fetch the Salarie object and then fetch the desired contrat. It's not the most DRY solution but you can't really apply DRY if you don't have more use cases where you need/want to re-use a piece of code. So, this approach is good because you don't need any criteria logic on your entity and it's very performant because you only fetch what you need.
If you don't have more use cases that may re-use the code, then don't over-optimize, wait for the situation to occur, and then you can look for a way to share your code, based on real needs, not in believings :)
Cheers!
Upvotes: 1
Reputation: 8161
Instead of looping, you just can extract the first element:
{% if not empty salarie.contrats %}
{% set contrat = salarie.contrats[0] %}
{# you can also use salarie.contrats|first #}
{{ contrat.departement }}
{% endif %}
Criteria
returns a Collection
even if there's just one element, so you can apply the same principle as above.
Although you could also extract the results in your controller before passing them to twig and pass them as Entities instead of collections. In your repository above:
/**
* @returns Contrat|null
*/
public function getLastContrat()
{
$criteria = Criteria::create()
->orderBy(['dateDebut' => 'DESC'])
->setMaxResults(1);
return $this->contrats->matching($criteria)->first();
}
Upvotes: 1