Vordefiniertes Content-Element für Redakteure

Für die Anzeige von Datensätzen eines MetaModel steht die MM-Liste als Content-Element bzw. FE-Modul zur Verfügung. Hier muss man verschiedene Auswahlen wie das MetaModel, Rendersetting, Filterung usw. treffen - das kann für Redakteure unter Umständen nicht gewünscht sein.

Gibt es den Wunsch, dass Redeakteure bei einem festgelegten MetaModel einfach ein oder mehrere Datensätze auswählen und diese angezeigt werden sollen, kann man das zum Beispiel mit den folgenden Methoden durchführen.

Auswahl und Anzeige mit Erweiterung RockSolid Custom Elements

RockSolid Custom Elements (RST-CE) geben einem die Möglichkeit, sämtliche im Contao Backend verfügbaren Eingabefelder nach belieben als Content Element und/oder Modul zur Verfügung zu stellen. Mehr dazu auf der Webseite von RockSolid oder Vortrag der CK24 von Marcus Lelle.

Im folgenden Beispiel soll ein einzelner Point-of-Interest (POI) durch den Redakteur ausgewählt und im FE angezeigt werden können. In der Auswahl soll der Name und der Ort erscheinen.

Konfiguration in RST-CE

Wie bei RST-CE üblich, muss eine Konfigurationsdatei für die Anzeige im BE sowie ein Template für die FE-Ausgabe erstellt werden. Die Quelltexte sollen nur das Vorgehen verdeutlichen und wie angegeben ist eine Auslagerung der API-Abfragen in separate Dateien zu empfehlen. Mehr zu den Abfragen bei MetaModels Referenz und API oder dem Vortrag von Ingolf Steinhardt zur CK23

 1<?php
 2// rsce_mm_poi_single_config.php
 3/**
 4 * Auswahl eines POI in RST-CE.
 5 *
 6 * Hinweis: Die Ermittlung der Options sollte man in eine Helper-Klasse auslagern - siehe Vortrag CK23 - bzw.
 7 * per options_callback (https://docs.contao.org/dev/reference/widgets/select/) holen.
 8 */
 9
10use Contao\System;
11
12// POI-Liste.
13$options = [];
14
15// Name der MetaModel Tabelle.
16$modelName = 'mm_poi';
17// ID des Filters "BE POI Einzelansicht: Veröffentlicht".
18$filterId = 12;
19
20// Item ermitteln.
21$container        = System::getContainer();
22$factory          = $container->get('metamodels.factory');
23$model            = $factory->getMetaModel($modelName);
24$filter           = $model->getEmptyFilter();
25$filterFactory    = $container->get('metamodels.filter_setting_factory');
26$filterCollection = $filterFactory->createCollection($filterId);
27$items = $model->findByFilter($filter);
28
29if ($items->getCount()) {
30    foreach ($items as $item) {
31        $options[$item->get('id')] = \sprintf('%s - %s', $item->get('name'), $item->get('city'));
32    }
33}
34
35return [
36    'label'           => ['POI Einzelanzeige', 'POI Einzelanzeige'],
37    'types'           => ['content'],
38    'fields'          => [
39        'poi' => [
40            'label'     => ['POI-Auswahl', 'Wählen Sie ein POI aus, welches angezeigt werden soll.'],
41            'inputType' => 'select',
42            'options'   => $options,
43            'eval'      => [
44                'chosen'             => true,
45                'mandatory'          => true,
46                'includeBlankOption' => true,
47                'tl_class'           => 'w50',
48            ],
49        ],
50    ],
51];

Filter in MM

Für die Filterung nach der ausgewählten POI-Id kann man eine Filterregel „Eigenes SQL“ anlegen und dort über den übergebenen Parameter aus $filterUrl entsprechend filtern.

1-- Filterregel in Filter 11
2SELECT id FROM {{table}}
3WHERE id = {{param::filter?name=poi}}

Weiterhin könnte man z. B. in dem SQL oder in einer weiteren Filterregel nach dem Veröffentlichungsstatus filtern.

Ausgabetemplate in HTML5

Für die Ausgabe muss noch ein entsprechendes Template angelegt werden - hier ist die Namenskonvention von RST-CE zu beachten.

 1<?php
 2// rsce_mm_poi_single.html5
 3
 4/**
 5 * Ausgabe eines POI - Auswahl mit RST-CE.
 6 *
 7 * Hinweis: Die Ermittlung der Items sollte man in eine Helper-Klasse auslagern - siehe Vortrag CK23.
 8 */
 9
10// Check POI-Id.
11if (!$this->poi) {
12    return;
13}
14
15// Name der MetaModel Tabelle.
16$modelName = 'mm_poi';
17// ID des Filters "FE POI Einzelansicht: POI-Auswahl + Veröffentlicht".
18$filterId = 11;
19// Filterwert POI-Id.
20$filterUrl = ['poi' => (int) $this->poi];
21// ID der Render-Einstellungen "FE Detailansicht - POI Einzelansicht ".
22$renderId = 20;
23
24// Item ermitteln.
25$factory          = $this->getContainer()->get('metamodels.factory');
26$model            = $factory->getMetaModel($modelName);
27$filter           = $model->getEmptyFilter();
28$filterFactory    = $this->getContainer()->get('metamodels.filter_setting_factory');
29$filterCollection = $filterFactory->createCollection($filterId);
30$filterCollection->addRules($filter, $filterUrl);
31$items = $model->findByFilter($filter);
32
33// Item rendern.
34$renderFactory = $this->getContainer()->get('metamodels.render_setting_factory');
35$arrItems      = $items->parseAll('html5', $renderFactory->createCollection($model, $renderId));
36?>
37<?php if (count($arrItems)): ?>
38    <div class="layout_full">
39        <?php foreach ($arrItems as $arrItem): ?>
40            <div class="poi_item">
41                <h2><?= $arrItem['text']['name'] ?></h2>
42                <p><?= $arrItem['text']['city'] ?></p>
43                <?= $arrItem['html5']['image'] ?>
44                <?php if($arrItem['actions']['jumpTo']['href']): ?>
45                    <p><a href="<?= $arrItem['actions']['jumpTo']['href'] ?>" title="Details">Details</a></p>
46                <?php endif; ?>
47            </div>
48        <?php endforeach; ?>
49    </div>
50<?php else : ?>
51    <p class="info">Kein POI ausgewählt!</p>
52<?php endif; ?>

Ausgabe in Twig

Für die Ausgabe in Twig muss man die auszugebenden Daten an das Template übergeben - eine Abfrage im Template wie bei HTML5 ist in Twig nicht möglich.

Die Daten für Twig werden in einer TwigFunktion geholt und bereit gestellt:

 1<?php
 2// src/Twig/AppExtension.php
 3namespace App\Twig;
 4
 5use MetaModels\Filter\Setting\FilterSettingFactory;
 6use MetaModels\IFactory;
 7use MetaModels\IMetaModel;
 8use MetaModels\Render\Setting\RenderSettingFactory;
 9use Twig\Extension\AbstractExtension;
10use Twig\TwigFunction;
11
12class AppExtension extends AbstractExtension
13{
14    public function __construct(
15        private readonly IFactory $factory,
16        private readonly FilterSettingFactory $filterFactory,
17        private readonly RenderSettingFactory $renderFactory,
18    ) {
19    }
20
21    public function getFunctions(): array
22    {
23        return [
24            new TwigFunction('getPoiById', [$this, 'getPoiById']),
25        ];
26    }
27
28    public function getPoiById(int $id): array
29    {
30        // Name der MetaModel Tabelle.
31        $modelName = 'mm_poi';
32        // ID des Filters "FE POI Einzelansicht: POI-Auswahl + Veröffentlicht".
33        $filterId = 11;
34        // Filterwert POI-Id.
35        $filterUrl = ['poi' => $id];
36        // ID der Render-Einstellungen "FE Detailansicht - POI Einzelansicht ".
37        $renderId = 20;
38
39        // Item ermitteln.
40        $model            = $this->factory->getMetaModel($modelName);
41        $filter           = $model->getEmptyFilter();
42        $filterCollection = $this->filterFactory->createCollection($filterId);
43        $filterCollection->addRules($filter, $filterUrl);
44
45        // Items rendern.
46        return $model->findByFilter($filter)->parseAll(
47            'html5',
48            $this->renderFactory->createCollection($model, $renderId)
49        );
50    }
51}

Registrierung in der service.yml:

 1# config/services.yml
 2services:
 3  _defaults:
 4    autoconfigure: true
 5
 6  App\Twig\AppExtension:
 7    arguments:
 8      $factory: '@metamodels.factory'
 9      $filterFactory: '@metamodels.filter_setting_factory'
10      $renderFactory: '@metamodels.render_setting_factory'

Mehr Informationen zum Thema „Registrierung von Services“.

Ausgabe im Twig-Template:

 1{{ rsce_mm_poi_single.html.twig }}
 2{% set pois = getPoiById(poi) %}
 3<div{% if id %} id={{ id }}{% endif %}{% if class %} class="{{ class }}"{% endif %}>
 4    {% if pois|length > 0 %}
 5        <div class="layout_full">
 6            {% for poi in pois %}
 7                <div class="poi_item">
 8                    <h2>{{ poi.html5.name|raw }}</h2>
 9                    <p>{{ poi.html5.city|raw }}</p>
10                    {{ ... }}
11                </div>
12            {% endfor %}
13        </div>
14    {% else %}
15        <p class="info">Kein POI ausgewählt!</p>
16    {% endif %}
17</div>

Auswahl und Anzeige mit eigenem Content-Element

Möchte man die Funktionalität mit „Contao-Boardmitteln“ statt mit einer Erweiterung implementieren, so kann man ein eigenes Inhaltselement erstellen.

In dem Beispiel soll eine Liste von MM-Datensätzen als Produkte auswählbar sein und auf der Webseite dargestellt werden. Die Reihenfolge der Ausgabe soll individuell einstellbar sein.

Contentelement und Callback

Zunächst wird eine DCA-Konfiguration und die Übersetzungen angelegt. Für die individuelle Reihenfolge wird der inputType als checkboxWizard definiert. Nach dem Anlegen der DCA-Definition muss eine Migration der Datenbank erfolgen.

 1<?php
 2// contao/dca/tl_content.php
 3use Doctrine\DBAL\Platforms\MySQLPlatform;
 4
 5$GLOBALS['TL_DCA']['tl_content']['palettes']['mm_products'] = '
 6    {type_legend},type,headline;
 7    {mm_products_legend},mm_products;
 8    {protected_legend:hide},protected;
 9    {expert_legend:hide},guests,cssID;
10    {invisible_legend:hide},invisible,start,stop;';
11
12$GLOBALS['TL_DCA']['tl_content']['fields']['mm_products'] = [
13    'label'            => &$GLOBALS['TL_LANG']['tl_content']['mm_products'],
14    'inputType'        => 'checkboxWizard',
15    //'options_callback' => See attribute config in MmProductsCallbackListener
16    'eval'             => [
17        'mandatory' => true,
18        'multiple'  => true,
19        'tl_class'  => 'w50',
20    ],
21    'sql'              => [
22        'type'    => 'blob',
23        'length'  => MySQLPlatform::LENGTH_LIMIT_BLOB,
24        'notnull' => false,
25    ],
26];
1<?php
2// contao/languages/en/tl_content.php
3
4// CTE
5$GLOBALS['TL_LANG']['CTE']['mm_products'] = ['CE Product selection', 'CE Product selection for MM products'];
6// Legends
7$GLOBALS['TL_LANG']['tl_content']['mm_products_legend'] = 'Product selection';
8// Fields
9$GLOBALS['TL_LANG']['tl_content']['mm_products'] = ['Product selection', 'Select several products.'];

Für die Generierung der Auswahlliste für das neue ContentElement müssen die Datensätze aus MM ausgelesen werden.

 1<?php
 2// src/EventListener/DataContainer/MmProductsCallbackListener.php
 3namespace App\EventListener\DataContainer;
 4
 5use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
 6use Contao\DataContainer;
 7use MetaModels\Filter\Setting\FilterSettingFactory;
 8use MetaModels\IFactory;
 9use MetaModels\IMetaModel;
10
11use function sprintf;
12
13#[AsCallback(table: 'tl_content', target: 'fields.mm_products.options')]
14class MmProductsCallbackListener
15{
16    public function __construct(
17        private readonly IFactory $factory,
18        private readonly FilterSettingFactory $filterFactory,
19    ) {
20    }
21
22    public function __invoke(DataContainer|null $dc = null): array
23    {
24        // Produkt-Liste.
25        $options = [];
26
27        // Name der MetaModel Tabelle.
28        $modelName = 'mm_products';
29        // ID des Filters "Liste Veröffentlicht".
30        $filterId = 4;
31
32        // Items ermitteln - sortiert nach Name.
33        $model = $this->factory->getMetaModel($modelName);
34        assert($model instanceof IMetaModel);
35        $filter           = $model->getEmptyFilter();
36        $filterCollection = $this->filterFactory->createCollection($filterId);
37        $filterCollection->addRules($filter, []);
38        $items = $model->findByFilter($filter, 'name');
39
40        if ($items->getCount()) {
41            foreach ($items as $item) {
42                $options[$item->get('id')] =
43                    \sprintf('%s - %s [%s]', $item->get('name'), $item->get('measures'), $item->get('articleno'));
44            }
45        }
46
47        return $options;
48    }
49}

Ausgabe in Twig mit eigenem Controller

Im nächsten Schritt wird die Ausgabe der Produkte erstellt. Dazu wird ein Controller benötigt, ein Ausgabetemplate in Twig.

  1<?php
  2// src/Controller/ContentElement/MmProductsElement.php
  3namespace App\Controller\ContentElement;
  4
  5use Contao\BackendTemplate;
  6use Contao\ContentModel;
  7use Contao\CoreBundle\Controller\ContentElement\AbstractContentElementController;
  8use Contao\CoreBundle\Routing\ScopeMatcher;
  9use Contao\CoreBundle\ServiceAnnotation\ContentElement;
 10use Contao\CoreBundle\Twig\FragmentTemplate;
 11use Contao\PageModel;
 12use Contao\StringUtil;
 13use MetaModels\Filter\Setting\FilterSettingFactory;
 14use MetaModels\IFactory;
 15use MetaModels\IMetaModel;
 16use MetaModels\Render\Setting\RenderSettingFactory;
 17use Symfony\Component\HttpFoundation\Request;
 18use Symfony\Component\HttpFoundation\RequestStack;
 19use Symfony\Component\HttpFoundation\Response;
 20
 21use function implode;
 22use function is_array;
 23
 24/**
 25 * @ContentElement("mm_products",
 26 *   category="texts",
 27 *   template="ce_mm_products",
 28 * )
 29 */
 30class MmProductsElement extends AbstractContentElementController
 31{
 32    public function __construct(
 33        private readonly IFactory $factory,
 34        private readonly FilterSettingFactory $filterFactory,
 35        private readonly RenderSettingFactory $renderFactory,
 36        private readonly ScopeMatcher $scopeMatcher,
 37        private readonly RequestStack $requestStack,
 38    ) {
 39    }
 40
 41    protected function getResponse(FragmentTemplate $template, ContentModel $model, Request $request): Response
 42    {
 43        $arrHeadline = StringUtil::deserialize($model->headline, true);
 44        $headline    = is_array($arrHeadline) ? $arrHeadline['value'] ?? '' : $arrHeadline;
 45        $template->set('headline', $headline);
 46        $template->set('hl', $arrHeadline['unit'] ?? 'h2');
 47
 48        $productsList = StringUtil::deserialize($model->mm_products, true);
 49
 50        if ($this->isBackend()) {
 51            $template = new BackendTemplate('be_wildcard');
 52            $template->title    = $headline;
 53            $template->wildcard = 'Produkte: ' . implode(', ', $productsList);
 54
 55            return $template->getResponse();
 56        }
 57
 58        $arrCssId = StringUtil::deserialize($model->cssID, true);
 59        $template->set('id', $arrCssId[0] ?? '');
 60        $template->set('class', $arrCssId[1] ?? '');
 61
 62        $template->set('products', $this->getProductsByIds($productsList));
 63
 64        // ID des AnfrageFormulars in DE.
 65        $template->set('pageAlias', PageModel::findById(5)->alias);
 66
 67        return $template->getResponse();
 68    }
 69
 70    protected function getProductsByIds(array $ids): array
 71    {
 72        // Name der MetaModel Tabelle.
 73        $modelName = 'mm_products';
 74        // ID des Filters "Liste Veröffentlicht + Produkt-Ids".
 75        $filterId = 5;
 76        // Filterwert products.
 77        $filterUrl = ['products' => $ids];
 78        // ID der Render-Einstellungen "Produkt-Liste".
 79        $renderId = 4;
 80
 81        // Items ermitteln.
 82        $model = $this->factory->getMetaModel($modelName);
 83        assert($model instanceof IMetaModel);
 84        $filter           = $model->getEmptyFilter();
 85        $filterCollection = $this->filterFactory->createCollection($filterId);
 86        $filterCollection->addRules($filter, $filterUrl);
 87
 88        // Items rendern.
 89        return $model->findByFilter($filter)->parseAll(
 90            'html5',
 91            $this->renderFactory->createCollection($model, $renderId)
 92        );
 93    }
 94
 95    public function isBackend(): bool
 96    {
 97        if ($request = $this->requestStack->getCurrentRequest()) {
 98            return $this->scopeMatcher->isBackendRequest($request);
 99        }
100
101        return false;
102    }
103}
 1{# templates/orion/ce_mm_products.html.twig #}
 2{% if headline %}
 3    <{{ hl }}>{{ headline }}</{{ hl }}>
 4{% endif %}
 5<div{% if id %} id={{ id }}{% endif %}{% if class %} class="{{ class }}"{% endif %}>
 6    {% if products|length > 0 %}
 7    <div class="product__list">
 8        {% for product in products %}
 9            <div class="product">
10                <div class="product__image">
11                    {{ product.html5.list_image|raw }}
12                </div>
13                <div class="product__features">
14                    <div class="product__name"><a href="{{ product.actions.jumpTo.href }}">{{ product.text.name }}</a></div>
15                    {% if product.text.sub_headline %}
16                        <div class="product__subheadline">({{ product.text.sub_headline }})</div>
17                    {% endif %}
18                    {% if product.text.measures %}
19                        <div class="product__measures">{{ product.text.measures }}</div>
20                    {% endif %}
21                </div>
22                {% if product.text.inquiry %}
23                    <div class="inquiry">
24                        <a href="{{ pageAlias }}?articlno={{ product.text.articleno }}&name={{ product.text.name }}" class="inquiry__button">Anfragen</a>
25                    </div>
26                {% endif %}
27            </div>
28        {% endfor %}
29    </div>
30    {% else %}
31        <p class="info">Kein Produkt ausgewählt!</p>
32    {% endif %}
33</div>

Services laden

Damit die Klassen alle geladen werden, gibt es verschiedene Wege - siehe „Registrierung von Services“. Mit einer eigenen services.yml sieht das wie folgt aus:

 1# config/services.yml
 2services:
 3  _defaults:
 4    autoconfigure: true
 5
 6  App\Controller\ContentElement\MmProductsElement:
 7    arguments:
 8      $factory: '@metamodels.factory'
 9      $filterFactory: '@metamodels.filter_setting_factory'
10      $renderFactory: '@metamodels.render_setting_factory'
11      $scopeMatcher: '@contao.routing.scope_matcher'
12      $requestStack: '@request_stack'
13
14  App\EventListener\DataContainer\MmProductsCallbackListener:
15    arguments:
16      $factory: '@metamodels.factory'
17      $filterFactory: '@metamodels.filter_setting_factory'

Ob alles geladen wird, kann per Konsolenaufruf getestet werden - Cache leeren und ggf. „composer install“ ausführen.