Search code examples
phpsymfonytwigsymfony4doctrine-query

Symfony 4 : DRY and performant way to render OneToMany relation entities in same twig template


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 %}

EDITED with @msg suggestions

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.

enter image description here

But no way to get the properties from there. {{ salarie.lastContrat.someProperty }} does not work.

salarie.lastContrat.someProperty

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 ?


Solution

  • 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();
    }