Support Contao 6

- Allow contao/core-bundle and calendar-bundle ^6.0 and Symfony 8
- Return the category columns as RecordLabel under Contao 6, where
  callback strings are encoded as text
- Only undo the double-escaped navigation tooltips where
  BackendUser::navigation() still exists (Contao 5)
- Decode stored entities in the W&K CSV export (Contao 5 stores input
  encoded)
- Use #[Target] for the named Contao logger aliases (Symfony 8.1
  deprecation); document Contao 6 in the README

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jürgen MummertandClaude Fable 5.1 committed 2026-09-27 13:07:17 +02:00
1 parent 8aebfcd28d
commit 27375e8d66
10 files changed
+54 -20

No files matched your search

+9 -3
View File
@@ -1,15 +1,15 @@
# W&K-Sync Bundle
Verbindet **Contao 5.7** mit der Seminarverwaltung **W&K** (Windows-Software ohne Schnittstelle) über CSV-Dateien:
Verbindet **Contao 5.7 und 6** mit der Seminarverwaltung **W&K** (Windows-Software ohne Schnittstelle) über CSV-Dateien:
1. **Import**: Die Redaktion exportiert in W&K die Seminarliste als CSV und lädt sie im Contao-Backend hoch. Vor dem Schreiben zeigt eine **Vorschau**, was sich ändert (neu, geändert, storniert, unverändert, übersprungen, Warnungen). Erst nach Bestätigung werden die Veranstaltungen im Kalender angelegt bzw. aktualisiert.
2. **Online-Anmeldung**: Besucher melden sich über ein Formular des **Formulargenerators** zu einer Veranstaltung an. Das Bundle blendet je nach Veranstaltung Felder ein und aus, prüft Plätze und Anmeldeschluss, speichert die Anmeldung, verschickt die passende Benachrichtigung (Notification Center 2) und leitet auf die Bestätigungsseite weiter.
3. **Posteingang**: Anmeldungen und Kontaktanfragen werden in Ordnern bearbeitet (Neu, Download-Liste, Bereits bearbeitet, Ignoriert/Spam, Exportiert).
4. **Export**: Die Download-Liste wird als CSV für den W&K-Import erzeugt (ISO-8859-1, Semikolon, feste Spalten), die Datensätze werden als exportiert markiert und die freien Plätze der Veranstaltungen reduziert.
- Contao **5.7**, PHP **8.5**, Doctrine DBAL **4**, MySQL 8 oder MariaDB
- Contao **5.7** oder **6.0**, PHP **8.5**, Doctrine DBAL **4**, MySQL 8 oder MariaDB
- Keine Abhängigkeiten außer dem Contao-Core und dem Kalender-Bundle
- **Notification Center 2** wird für Benachrichtigungen empfohlen (Typ „W&K-Anmeldung“), ist aber nicht erforderlich
- **Notification Center 2** wird für Benachrichtigungen empfohlen (Typ „W&K-Anmeldung“), ist aber nicht erforderlich; ohne Notification Center werden Anmeldungen gespeichert und protokolliert, aber keine Mails verschickt (Stand September 2026 unterstützt das Notification Center Contao 6 noch nicht)
- Newsletter-Anmeldung (Double-Opt-in) mit dem Newsletter-Bundle, optional
---
@@ -168,6 +168,12 @@ Alle Unterpunkte von „W&K-Tools“ sind normale Contao-Backend-Module und werd
- Verfügbarkeit wird beim Absenden erneut geprüft (Wettlauf um den letzten Platz).
- Anmeldungen und Anfragen werden nur mit bekannten Spalten gespeichert.
## Contao 5.7 und 6
Ein Stand des Bundles läuft in beiden Versionen. Contao 6 speichert Eingaben unkodiert und kodiert erst bei der Ausgabe, Contao 5 speichert sie kodiert. Die Backend-Oberfläche des Bundles ist komplett in Twig (automatisches Escaping); die wenigen Stellen, die HTML außerhalb von Twig erzeugen (Kategorienliste, Prüfung der Einrichtung, Tooltips der Navigation), kodieren selbst ohne doppeltes Kodieren und geben in Contao 6 ein `RecordLabel` zurück. Der CSV-Export dekodiert gespeicherte Werte, damit unter Contao 5 keine HTML-Entities (z. B. `&#40;` für eine Klammer) in der W&K-Datei landen.
Das Backend-Modul (Import, Posteingang, Exporte) ist ein klassisches `BE_MOD`-Callback-Modul und das Anmeldeformular wird über `Controller::getForm()` gerendert; beides ist in Contao 6 weiterhin unterstützt und nicht als veraltet markiert.
## Entwicklung
```bash
+9 -9
View File
@@ -1,6 +1,6 @@
{
"name": "mummert/wk-sync-bundle",
"description": "W&K-Sync für Contao 5.7: Kursimport aus der Seminarverwaltung W&K (CSV), Online-Anmeldungen mit Posteingang und CSV-Export zurück nach W&K",
"description": "W&K-Sync für Contao 5.7 und 6: Kursimport aus der Seminarverwaltung W&K (CSV), Online-Anmeldungen mit Posteingang und CSV-Export zurück nach W&K",
"type": "contao-bundle",
"license": "MIT",
"authors": [
@@ -13,15 +13,15 @@
"php": "^8.5",
"ext-iconv": "*",
"ext-mbstring": "*",
"contao/core-bundle": "^5.7",
"contao/calendar-bundle": "^5.7",
"contao/core-bundle": "^5.7 || ^6.0",
"contao/calendar-bundle": "^5.7 || ^6.0",
"doctrine/dbal": "^4.0",
"symfony/config": "^7.4",
"symfony/console": "^7.4",
"symfony/dependency-injection": "^7.4",
"symfony/event-dispatcher": "^7.4",
"symfony/http-foundation": "^7.4",
"symfony/http-kernel": "^7.4",
"symfony/config": "^7.4 || ^8.0",
"symfony/console": "^7.4 || ^8.0",
"symfony/dependency-injection": "^7.4 || ^8.0",
"symfony/event-dispatcher": "^7.4 || ^8.0",
"symfony/http-foundation": "^7.4 || ^8.0",
"symfony/http-kernel": "^7.4 || ^8.0",
"twig/twig": "^3.0"
},
"require-dev": {
+2
View File
@@ -16,6 +16,7 @@ use Contao\PageModel;
use Contao\System;
use Mummert\ContaoWkSyncBundle\Sync\CalendarConfig;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -32,6 +33,7 @@ class NewsletterSubscriber
private readonly SimpleTokenParser $tokenParser,
private readonly TranslatorInterface $translator,
private readonly RequestStack $requestStack,
#[Target('contaoErrorLogger')]
private readonly LoggerInterface|null $contaoErrorLogger = null,
) {
}
@@ -6,12 +6,14 @@ namespace Mummert\ContaoWkSyncBundle\Booking;
use Mummert\ContaoWkSyncBundle\EventListener\NotificationCenter\BookingNotificationType;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Terminal42\NotificationCenterBundle\NotificationCenter;
class NotificationCenterNotifier implements BookingNotifierInterface
{
public function __construct(
private readonly NotificationCenter $notificationCenter,
#[Target('contaoErrorLogger')]
private readonly LoggerInterface|null $contaoErrorLogger = null,
) {
}
+5 -2
View File
@@ -5,14 +5,17 @@ declare(strict_types=1);
namespace Mummert\ContaoWkSyncBundle\Booking;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Target;
/**
* Ohne Notification Center werden keine Mails verschickt; der Vorgang wird nur protokolliert.
*/
class NullNotifier implements BookingNotifierInterface
{
public function __construct(private readonly LoggerInterface|null $contaoErrorLogger = null)
{
public function __construct(
#[Target('contaoErrorLogger')]
private readonly LoggerInterface|null $contaoErrorLogger = null,
) {
}
public function notify(int $notificationId, array $tokens): bool
@@ -4,12 +4,16 @@ declare(strict_types=1);
namespace Mummert\ContaoWkSyncBundle\EventListener\DataContainer;
use Contao\CoreBundle\DataContainer\RecordLabel;
use Contao\CoreBundle\DependencyInjection\Attribute\AsCallback;
use Contao\DataContainer;
use Contao\StringUtil;
/**
* Kategorienliste: die W&K-Bezeichnungen (eine pro Zeile) lesbar getrennt anzeigen.
*
* Contao 6 behandelt Spaltenwerte aus dem label_callback als Text; HTML muss dort als RecordLabel
* zurückgegeben werden. Contao 5.7 kennt die Klasse nicht und gibt die Spalten unverändert aus.
*/
#[AsCallback(table: 'tl_wk_category', target: 'list.label.label')]
class CategoryLabelListener
@@ -17,14 +21,14 @@ class CategoryLabelListener
/**
* @param list<string> $args Spaltenwerte in der Reihenfolge von list.label.fields
*
* @return list<string>
* @return list<string>|RecordLabel
*/
public function __invoke(array $row, string $label, DataContainer $dc, array $args): array
public function __invoke(array $row, string $label, DataContainer $dc, array $args): array|RecordLabel
{
$names = array_filter(array_map(trim(...), explode("\n", str_replace("\r", '', (string) ($row['wkNames'] ?? '')))));
$args[2] = implode('<span class="tl_gray"> · </span>', array_map(StringUtil::specialchars(...), $names));
$args[2] = implode('<span class="tl_gray"> · </span>', array_map(static fn (string $name): string => StringUtil::specialchars($name, false, false), $names));
return $args;
return class_exists(RecordLabel::class) ? RecordLabel::fromHtml($args) : $args;
}
}
@@ -20,6 +20,7 @@ use Mummert\ContaoWkSyncBundle\Booking\BookingTokens;
use Mummert\ContaoWkSyncBundle\Booking\NewsletterSubscriber;
use Mummert\ContaoWkSyncBundle\Booking\NotificationRouter;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
@@ -43,7 +44,9 @@ class ProcessFormDataListener
private readonly BookingNotifierInterface $notifier,
private readonly NewsletterSubscriber $newsletter,
private readonly ContentUrlGenerator $urlGenerator,
#[Target('contaoGeneralLogger')]
private readonly LoggerInterface|null $contaoGeneralLogger = null,
#[Target('contaoErrorLogger')]
private readonly LoggerInterface|null $contaoErrorLogger = null,
) {
}
@@ -4,16 +4,18 @@ declare(strict_types=1);
namespace Mummert\ContaoWkSyncBundle\EventListener\Menu;
use Contao\BackendUser;
use Contao\CoreBundle\Event\MenuEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
/**
* Backend-Navigation der Gruppe „W&K-Tools“: Gruppen-Icon einbinden und die Tooltips korrigieren.
*
* Contao escaped die Modul-Beschreibungen für den Tooltip bereits in BackendUser::navigation()
* Contao 5 escaped die Modul-Beschreibungen für den Tooltip bereits in BackendUser::navigation()
* (StringUtil::specialchars) und das Menü-Template escaped das title-Attribut ein zweites Mal, sodass
* aus „W&K“ im Tooltip „W&amp;K“ wird. Der Core-Listener läuft mit Priorität 10; dieser Listener nimmt
* die erste Escapierung für die W&K-Module zurück.
* die erste Escapierung für die W&K-Module zurück. Contao 6 baut die Navigation ohne
* BackendUser::navigation() und escaped nur noch einmal, dort bleibt der Tooltip unverändert.
*/
#[AsEventListener]
class BackendMenuListener
@@ -28,6 +30,10 @@ class BackendMenuListener
$GLOBALS['TL_CSS'][] = 'bundles/contaowksync/wk-sync-menu.css|static';
if (!method_exists(BackendUser::class, 'navigation')) {
return;
}
foreach ($group->getChildren() as $item) {
$title = $item->getLinkAttribute('title');
+6
View File
@@ -4,12 +4,14 @@ declare(strict_types=1);
namespace Mummert\ContaoWkSyncBundle\Export;
use Contao\StringUtil;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Connection;
use Mummert\ContaoWkSyncBundle\Import\SyncFileStorage;
use Mummert\ContaoWkSyncBundle\Inbox\InboxState;
use Mummert\ContaoWkSyncBundle\Sync\SyncLog;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Target;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
@@ -32,6 +34,7 @@ class BookingExporter
private readonly SyncFileStorage $storage,
private readonly SyncLog $log,
private readonly TranslatorInterface $translator,
#[Target('contaoGeneralLogger')]
private readonly LoggerInterface|null $contaoGeneralLogger = null,
) {
}
@@ -132,6 +135,9 @@ class BookingExporter
*/
private function row(array $row, string $wkId): array
{
// Contao 5 speichert Formulareingaben kodiert (z. B. &#40; für eine Klammer), die CSV braucht Klartext
$row = array_map(static fn (mixed $value): mixed => \is_string($value) ? StringUtil::decodeEntities($value) : $value, $row);
$gender = (string) ($row['gender'] ?? '');
$position = (string) ($row['position'] ?? '');
+2
View File
@@ -8,6 +8,7 @@ use Mummert\ContaoWkSyncBundle\Sync\CalendarConfig;
use Mummert\ContaoWkSyncBundle\Sync\CalendarConfigRepository;
use Mummert\ContaoWkSyncBundle\Sync\SyncLog;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Target;
/**
* Ablauf: Datei ablegen und Vorschau berechnen, dann nach Bestätigung anwenden. Die Vorschau wird beim
@@ -22,6 +23,7 @@ class ImportService
private readonly SyncFileStorage $storage,
private readonly SyncLog $log,
private readonly CalendarConfigRepository $calendars,
#[Target('contaoGeneralLogger')]
private readonly LoggerInterface|null $contaoGeneralLogger = null,
) {
}