Funktionale Programmierung in Twig: Collections deklarativ verarbeiten

in  Basics , , , , ,

Funktionale Programmierung in Twig: Collections deklarativ verarbeiten

Seit kurzem haben neue Funktionen in die Template-Engine Twig Einzug gehalten. Diese verändern die Art und Weise, wie mit mehrwertigen Daten umgegangen wird, fundamental. Aus diesem Grund soll dieser Artikel beleuchten, wie man mit den neuen Möglichkeiten umgehen kann.

Was heißt deklarativ?

Bei “filter, map und reduce” handelt es sich um ein Muster (Pattern) aus der funktionalen Programmierung. Es erlaubt die Veränderung von Sequenzen (Listen, Arrays, Vektoren, usw.) mittels mehrerer Operationen und Manipulationen, an deren Ende ein Ergebnis steht, welches entweder ausgegeben oder weiter verwendet werden kann. Der Vorteil liegt hier darin, dass diese Veränderungen deklarativ angegeben werden können. Dies führt zu einer sehr prägnanten und lesbaren Syntax.

Das Muster wird zunehmend in zahlreichen Programmiersprachen umgesetzt. So existiert es beispielsweise seit langer Zeit in JavaScript und auch in Python.

Beispiele mit Xtend

Für ModuleStudio verwende ich seit vielen Jahren die Programmiersprache Xtend. Dabei handelt es sich um eine sehr mächtige Sprache 🚀, die zu Java-Quellcode kompiliert. Dort wird sehr viel mit deklarativen Ausdrücken gearbeitet, was man nach kurzer Zeit bereits nicht mehr missen möchte ❤️.

Hier einige Beispiele aus der offiziellen Doku, die die Mächtigkeit und Flexibilität der Filtermöglichkeiten anhand von Abfragen aus einer Liste von Filmen demonstrieren:

Wie lautet die Anzahl aller Actionfilme?

1
movies.filter[ categories.contains('Action') ].size)

In welchem Jahr wurde der beste Film aus den 80ern veröffentlicht?

1
2
3
movies.filter[ (1980..1989).contains(year) ].sortBy[ rating ].last.year)
movies.filter[ (1980..1989).contains(year) ].sortBy[ -rating ].head.year
movies.filter[ (1980..1989).contains(year) ].sortBy[ rating ].reverseView.head.year

Diese Varianten zeigen schön die weitere Verwendung der resultierenden Liste durch eine Verkettung mehrerer Funktionen.

Wie lautet die Summe aller Votes für die zwei Top-Filme?

1
val sum = movies.sortBy[ -rating ].take(2).map[ numberOfVotes ].reduce[ a, b | a + b ]

Zuerst werden die Filme nach Bewertung sortiert und die zwei besten ausgewählt. Von diesen wird mit der map-Funktion eine Liste mit der jeweiligen Anzahl an Votes erstellt. Diese Liste kann anschließend durch reduce in eine einzelne Zahl reduziert werden, wobei die Werte zusammen addiert werden.

Im ModuleStudio-Generator

Im Generator für ModuleStudio tauchen solche Ausdrücke an allen Ecken und Winkeln auf. Hier noch ein paar Beispiele aus der Praxis.

Ein PHP-Array mit Entity-Namen erzeugen

Gewünscht ist die Ausgabe eines Arrays, welches die Namen aller Entitäten enthält, auf die eine bestimmte Bedingung zutrifft - im folgenden Fall sind das die Entitäten, welche eine display-Aktion besitzen.

1
$entitiesWithDisplayAction = [getAllEntities.filter[hasDisplayAction].map[name.formatForCode].join('\', \'')»'];

Zunächst wird mit getAllEntities eine Liste aller Entitäten selektiert; anschließend wird diese via filter weiter eingegrenzt (hasDisplayAction ist hier übrigens eine Funktion, welche implizit die jeweilige Entität als Argument erhält); mit der map-Funktion wird aus der Liste der Entitäten eine Liste derer Namen; und schließlich werden diese mit join zu einem String zusammengefügt.

Prüfen, ob ein Uploadfeld mit bestimmtem Namensschema existiert

Der folgende Ausdruck liefert einen bool’schen Rückgabewert darüber, ob es mindestens eine Entität gibt, die ein Uploadfeld enthält, dessen namingScheme-Eigenschaft einen gegebenen Wert hat.

1
!entities.map[fields].flatten.filter(UploadField).filter[namingScheme == scheme && (anotherCondition || somethingElse)].empty

Der Ausdruck entities.map[fields] sorgt dafür, dass die Liste von Entitäten umgewandelt wird in eine Liste der Felder dieser Entitäten. Mittels .flatten wird diese danach verflacht, so dass eine Liste von Feldern entsteht. Anschließend wird auf die gewünschte Art des Feldes sowie auf die Bedingung gefiltert. Durch ! ... empty wird auf die Existenz mindestens eines Eintrags geprüft.

Und jetzt zu Twig

Im Folgenden wird die Syntax in Twig-Templates anhand von Beispielen aus dem Twig-Handbuch und einem Blogartikel auf symfony.com dargestellt und erklärt. Die Funktionsweise läuft ziemlich gleich wie in Xtend (und in diversen anderen Sprachen). Die Syntax unterscheidet sich geringfügig: die “innere” Funktion wird hier nicht innerhalb von Klammern angegeben, sondern in Form einer “arrow-Funktion” notiert. Diese arrow-Funktion hat Zugriff auf den aktuellen Kontext.

filter

Der filter-Filter entfernt alle Einträge, welche nicht einer gegebenen Bedingung entsprechen, aus einer Sequenz oder einer Map (assoziatives Array).

1
2
3
4
{% set sizes = [34, 36, 38, 40, 42] %}

{{ sizes|filter(v => v > 38)|join(', ') }}
{# Ausgabe: 40, 42 #}

Kombiniert mit dem for-Tag ergibt sich eine prägnante Schreibweise, um nur über bestimmte Einträge zu iterieren:

1
2
3
4
{% for v in sizes|filter(v => v > 38) -%}
    {{ v }}
{% endfor %}
{# Ausgabe: 40 42 #}

Noch ein Beispiel für eine Schleife:

1
2
3
{% for product in related_products|filter(p => p.stock > 10 and p.id not in user.recentPurchases) %}
    {# ... #}
{% endfor %}

Und nun ein Beispiel mit einer Map:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{% set sizes = {
    xs: 34,
    s:  36,
    m:  38,
    l:  40,
    xl: 42,
} %}

{% for k, v in sizes|filter(v => v > 38) -%}
    {{ k }} = {{ v }}
{% endfor %}
{# Ausgabe: l = 40 xl = 42 #}

Die arrow-Funktion erhält auch den Schlüssel als zweites Argument:

1
2
3
4
{% for k, v in sizes|filter((v, k) => v > 38 and k != "xl") -%}
    {{ k }} = {{ v }}
{% endfor %}
{# Ausgabe: l = 40 #}

Abschließend ein Beispiel mit einem etwas komplexeren Filterausdruck:

1
2
3
{% set components = all_components|filter(
    (v, k) => v.published is true and not (k starts with 'Deprecated')
) %}

map

Der map-Filter wendet eine Funktion auf die Elemente einer Sequenz oder einer Map an..

1
2
3
4
5
6
7
8
9
{% set people = [
    {first: "Bob", last: "Smith"},
    {first: "Alice", last: "Dupond"},
] %}

{{ people|map(p => "#{p.first} #{p.last}")|join(', ') }}
{# oder #}
{{ people|map(p => p.first ~ ' ' ~ p.last)|join(', ') }}
{# Ausgabe: Bob Smith, Alice Dupond #}

Die arrow-Funktion erhält auch den Schlüssel als zweites Argument:

1
2
3
4
5
6
7
{% set people = {
    "Bob": "Smith",
    "Alice": "Dupond",
} %}

{{ people|map((last, first) => "#{first} #{last}")|join(', ') }}
{# Ausgabe: Bob Smith, Alice Dupond #}

reduce

Der reduce-Filter reduziert eine Sequenz oder eine Map iterativ auf einen einzelnen Wert. Die arrow-Funktion erhält in diesem Fall den Rückgabewert aus der vorherigen Iteration sowie den aktuellen (nächsten) Wert aus der gegebenen Liste.

1
2
3
4
{% set numbers = [1, 2, 3] %}

{{ numbers|reduce((carry, v) => carry + v) }}
{# Ausgabe: 6 #}

Es ist optional möglich, einen Anfangswert anzugeben:

1
2
{{ numbers|reduce((carry, v) => carry + v, 10) }}
{# Ausgabe: 16 #}

Das folgende Beispiel ermittelt die Gesamtanzahl aller Produkte in einem Warenkorb:

1
2
{% set num_products = cart.rows|reduce((previousTotal, row) => previousTotal + row.totalUnits) %}
{{ num_products }} products in total.

column

Der column-Filter liefert die Werte einer einzelnen Spalte (sprich eines Feldes) aus dem gegebenen Array. Dies entspricht der flatten-Funktion in Xtend.

1
2
3
4
{% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
{% set fruits = items|column('fruit') %}

{# fruits enthält nun ['apple', 'orange'] #}

Ressourcen zum Nachlesen in der Übersicht

Anwendung für Zikula und ModuleStudio

Wir werden die neuen Funktionen auf jeden Fall sowohl im Zikula Core als auch im Modul-Generator einsetzen. Dies wird aber nicht mehr für Zikula 2.x passieren, sondern bleibt der neuen Linie von Zikula Core 3.x vorbehalten.

Weitere Beiträge in Kategorie Basics

Einfaches JS-Management mit ImportMaps in neuer Symfony AssetMapper-Komponente
- Im Symfony-Ökosystem werden Frontend-Komponenten seit einiger Zeit zunehmend über UX-Komponenten verarbeitet. Auch wenn hierdurch einige Anbelange vereinfacht werden, ist doch oftmals der Aufbau einer …
Symfony UX im Kontext von ModuleStudio
- Stimulus und Symfony UX Ein JavaScript-Ökosystem für Symfony wurde bereits Ende 2020 gestartet. Vereinfacht ausgedrückt wurde Symfony Flex erweitert, um auch JavaScript-Anteile in einem Bundle leicht …
Das Buch zu Symfony ist in einer neuen Version erschienen
- Bereits im Jahr 2020 haben wir das Buch “Symfony: The Fast Track” hier vorgestellt. Nun ist eine neue Auflage bereitgestellt worden, welche auf den Stand für Symfony 5.4 und 6.0 gebracht …
Symfony und API Platform starten offiziellen Docker-Support
- Seit knapp einem Monat steht eine umfangreiche Unterstützung zur Docker-basierten Arbeit mit Symfony bereit. Hierbei gibt es Neuerungen in mehreren zentralen Werkzeugen, die ineinander greifen und …
Optische Varianzen mit SCSS-Maps umsetzen
- In vielen Weblayouts ist es notwendig, unterschiedliche Ausprägungen einer bestimmten kosmetischen “Stellschraube” zu realisieren. So können etwa einzelne Akzentfarben je nach Unterseite, …