Search code examples
phpsymfonytwig

Twig : separator in a "for" tag


Is there a syntax to separate some elements in a "for" tag please ?

For example I have a list of users, and I want to display their username with a "-" separator, so the expected result would be : Mickael - Dave - Chris - ...

I have found this solution :

{% for user in entity.users %}
    {{ user.name }}{% if not loop.last %} - {% endif %}
{% endfor %}

But this is not very elegant. The join filter does not seem appropriate in a loop.


Solution

  • AFAIK, no, you can't use the join filter for your use case without workarounds. This is because you need to pull getName out of the object property. join just link each element of the Traversable instance with the given glue.

    There are however some workarounds if you need it.

    Add a __toString method

    In your User entity, you can add a __toString method to get the name of the user by default.
    You should however take care if no other object are using the current default toString as it may cause conflicts

    namespace Acme\FooBundle\Entity;
    
    class User
    {
        public function __toString()
        {
            return $this->getName();
        }
    }
    

    Then you can use in your twig the join filter

    {{ entity.users|join(' - ') }}
    

    Map the usernames in the controller

    In your controller, right before sending parameters to the view, you can map all the usernames for them to fit in an array.
    Note: I assume getUsers is a PersistentCollection, if not, use array_map instead

    // Acme/FooBundle/Controller/MyController#fooAction
    
    $users = $entity->getUsers();
    $usernames = $users->map(function(User $user) {
        return $user->getName();
    });
    
    return $this->render('AcmeFooBundle:My:foo.html.twig', array(
        'entity' => $entity,
        'usernames' => $usernames
    );
    

    Then in your twig

    {{ usernames|join(' - ') }}
    

    Create a TWIG filter

    In your application, you can create a Twig Extension. In this example, I'll create a filter called joinBy which will act like join by specifying a method to join elements by.

     Service declaration

    Straight and easy, nothing too harsh but a standard declaration like in the docs

    services.yml

    acme_foo.tools_extension:
        class: Acme\FooBundle\Twig\ToolsExtension
        tags:
            - { name: twig.extension }
    

    Extension creation

    @Acme\FooBundle\Twig\ToolsExtension

    namespace Acme\FooBundle\Twig;
    
    use Twig_Extension, Twig_Filter_Method;
    use InvalidArgumentException, UnexpectedValueException;
    use Traversable;
    
    /**
     * Tools extension provides commons function
     */
    class ToolsExtension extends Twig_Extension
    {
        /**
         * {@inheritDoc}
         */
        public function getName()
        {
            return 'acme_foo_tools_extension';
        }
    
        /**
         * {@inheritDoc}
         */
        public function getFilters()
        {
            return array(
                'joinBy' => new Twig_Filter_Method($this, 'joinBy')
            );
        }
    
        /**
         * Implode-like by specifying a value of a traversable object
         *
         * @param mixed  $data  Traversable data
         * @param string $value Value or method to call
         * @param string $join  Join string
         *
         * @return string Joined data
         */
        public function joinBy($data, $value, $join = null)
        {
            if (!is_array($data) && !($data instanceof Traversable)) {
                throw new InvalidArgumentException(sprintf(
                    "Expected array or instance of Traversable for ToolsExtension::joinBy, got %s",
                    gettype($data)
                ));
            }
    
            $formatted = array();
    
            foreach ($data as $row) {
                $formatted[] = $this->getInput($row, $value);
            }
    
            return implode($formatted, $join);
        }
    
        /**
         * Fetches the input of a given property
         *
         * @param  mixed  $row  An array or an object
         * @param  string $find Property to find
         * @return mixed  Property's value
         *
         * @throws UnexpectedValueException When no matching with $find where found
         */
        protected function getInput($row, $find)
        {
            if (is_array($row) && array_key_exists($find, $row)) {
                return $row[$find];
            }
    
            if (is_object($row)) {
                if (isset($row->$find)) {
                    return $row->$find;
                }
    
                if (method_exists($row, $find)) {
                    return $row->$find();
                }
    
                foreach (array('get%s', 'is%s') as $indic) {
                    $method = sprintf($indic, $find);
    
                    if (method_exists($row, $method)) {
                        return $row->$method();
                    }
                }
    
                if (method_exists($row, $method)) {
                    return $row->$method();
                }
            }
    
            throw new UnexpectedValueException(sprintf(
                "Could not find any method to resolve \"%s\" for %s",
                $find,
                is_array($row)
                    ? 'Array'
                    : sprintf('Object(%s)', get_class($row))
            ));
        }
    }
    

    Usage

    You can now use it in your twig by calling

    {{ entity.users|joinBy('name', ' - ') }}
    # or
    {{ entity.users|joinBy('getName', ' - ') }}