简体   繁体   中英

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.

在此处输入图片说明

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 ?

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

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!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM