Search code examples
symfonyurlentitysymfony4business-logic

Symfony 4 - How to add business logic to an Entity


I have an Article entity and actually I use the path() function in 3 twig templates, providing as second parameter the slug of the article, to get the url to show one article.

I know it's not the best way to do that, because, for example, if I want to provide the id instead of the slug I should change the code in more than one template.

I thought to use a getUrl() method in the Article class but than I can't use the Route service to generate the url.

Is there a better way to do that in Symfony 4?

This is the a portion of the ArticleController:

<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

use App\Entity\Article;
use App\Repository\ArticleRepository;

class ArticleController extends AbstractController
{
    ...

    /**
     * @Route("/article/{slug}", name="article_show")
     */
    public function show(Article $article) {
        return $this->render('article/show.html.twig', [
            'article' => $article
        ]);
    }

}

Then in the templates I have a similar code like this:

 {% for article in articles %}
    ...

        <a href="{{ path('article_show',{ 'slug': article.slug } ) }}">
            {{ article.title }}
        </a>
    ...
 {% endfor %}

What I would like to do is to have a code like this:

 {% for article in articles %}
    ...

        <a href="{{ article.getUrl() }}">
            {{ article.title }}
        </a>
    ...
 {% endfor %}

where getUrl does the work of the path() method, so if I change something in the route it will be reflected in all the templates, but I can't do this because I can't fetch the Route service in the Article entity.

So is there an alternative way to accomplish the same goal?


Solution

  • What you are looking for is a wrong way of achieving the desired effect. For this to work you will have to inject the router into the entity which essentially turns it into "business logic" (e.g. controller) rather than "persistence" and breaks the single responsibility principle. Besides it's hard to achieve technically as you'll have to modify Doctrine's internals

    There are two ways to handle this properly and both of them involve custom Twig extension:

    The simplest one is to define a custom Twig filter which takes care of generating the correct URL:

    <?php
    
    namespace App\Twig\Extension;
    
    use App\Entity\Article;
    
    use Symfony\Component\Routing\RouterInterface;
    use Twig\Extension\AbstractExtension;
    use Twig\TwigFilter;
    
    class ArticleExtension extends AbstractExtension
    {
        private $router;
    
        public function __construct(RouterInterface $router)
        {
            $this->router = $router;
        }
    
        public function getFilters()
        {
            return [
                new TwigFilter('article_url', [$this, 'getArticleUrl']),
            ];
        }
    
        public function getArticleUrl(Article $article): string
        {
            return $this->router->generate('article_show', ['slug' => $article->getSlug()]);
        }
    }
    
    

    Then in twig you just use the filter like this:

    {% for article in articles %}
        ...
    
        <a href="{{ article|article_url }}">
            {{ article.title }}
        </a>
        ...
    {% endfor %}
    

    If you are using Symfony 3.4+ with awtowiring/autoconfigure just creating the class will be enough otherwise you need to register it as twig extension in the container. Please refer to Symfony's documentation for more details

    The second option mentioned is only necessary if you want to reuse the route generation outside of the view/templating. In this case the necessary logic (which is now in the Twig extension) must be moved to a separate standalone service. Then you'd have to inject this service into the extension and use it instead of directly invoking the router. Check the relevant documentation entry for a verbose walk-trough on creating/registering a service