Drupal 10 läuft aus. Wir haben das genutzt, um Altlasten loszuwerden.
Am 9. Dezember 2026 endet der Support für Drupal 10. Statt nur den Core zu bumpen, haben wir in einem Bestandsprojekt erst mal alles rausgeworfen, was seit Jahren nichts mehr tut — und dabei gelernt, dass „toter Code" manchmal keiner ist.
Am 9. Dezember 2026 endet der Support für Drupal 10. Wir betreuen ein Drupal-Projekt, das seit vielen Jahren läuft und entsprechend viel angesammelt hat. Statt einfach nur den Core zu bumpen, haben wir den Anlass genommen, alles rauszuwerfen, was seit Jahren nichts mehr tut. Hier steht, was das war, was wir dabei falsch eingeschätzt haben und was noch offen ist.
Ausgangslage
Upgrade Status hat uns ein überschaubares Bild gezeigt: die eigenen Module waren sauber, aber ein paar Contrib-Pakete erlaubten Drupal 11 nicht, ein Theme benutzte eine Funktion, die in 11 entfernt ist, und ein Stapel eigener Migrate-Plugins stand auf „Check manually“. Composer konnte kein Drupal-11-Set auflösen. Nichts davon war dramatisch. Aber nichts davon war auch mal eben erledigt.
Contrib: drei Blocker, drei verschiedene Antworten
Ein Modul war seit Juli deinstalliert, stand aber noch in der composer.json. Rauswerfen, fertig. Webform brauchte nur ein Minor-Update. json_ld_schema brauchte einen Major-Sprung, den ich erst für eine echte Portierung gehalten hab — am Ende waren es drei : bool Return-Types auf isApplicable(). Lektion: Erst den Diff lesen, dann Aufwand schätzen. Nicht andersrum.
Der spannendere Fall war context_active_trail. Das Modul ist verwaist und wird Drupal 11 nicht mehr erreichen. Wir haben es in ein eigenes Modul portiert. Beim Review des Ports fanden sich fünf Sachen: ein Cache-Tag, der nie invalidiert wurde (die Invalidierung zeigte auf den falschen Tag, Trail-Änderungen blieben stale), ein fehlendes Config-Schema, ein fehlender Render-Cache-Flush, das alte Contrib-Modul noch im Composer — und eine Proxy-Klasse, die ich für toten Code gehalten hab.
War sie nicht. menu.active_trail ist ein Lazy Service, und wer dessen Klasse überschreibt, braucht den Proxy. drush updb hat das dann gezeigt. Beim Neu-Generieren mit dem Core-Script kam sogar etwas Besseres raus als das Contrib-Original: korrekte Argument-Übergabe plus die Destructable- und CacheCollector-Interfaces, die das alte Modul weggelassen hatte — ohne die wird der Trail-Cache nie zurückgeschrieben. Der Fehler des Originals hat also die ganze Zeit mitgelaufen.
Eigener Code: eine ganze Migration weg
Das Projekt hatte noch den kompletten Migrations-Stack aus dem Umzug von Drupal 7 auf 8 — Plugins, Config, eigene Module, Composer-Pakete. Seit Jahren nicht angefasst. Die entscheidende Frage war nicht „brauchen wir das noch?“, sondern „kann das überhaupt noch laufen?“. drush migrate:status hat sie beantwortet: die Quell-Datenbank existiert seit Jahren nicht mehr. Code, der nicht mehr laufen kann, ist keine Sicherheit, sondern nur Gewicht. Also weg.
Dazu ein Stapel leerer Module aus derselben Zeit — Hüllen ohne Inhalt, die beim Umzug mal gebraucht wurden und dann liegen blieben. Vor dem Deinstallieren haben wir über Drupals eigene Uninstall-API geprüft, dass dabei keine Config gelöscht wird. Erst dann raus. Klassischer Spätsommerputz. Und Url::toRenderArray() im Theme durch die heutige Variante ersetzt.
Core-Patches: einer bleibt, einer geht
Zwei Patches auf dem Core. Einer (Views distinct) wird von einer View tatsächlich gebraucht und passt sauber auf 11.x — bleibt. Der andere stellte sich als Twig-Debug-Output-Fix heraus, den das Theme nie benutzt hat, und er passt nicht auf 11.x. Den würden wir lieber droppen als neu rollen. Patches, die niemand mehr zuordnen kann, sind auch Altlasten.
Wie wir gearbeitet haben
Die Arbeitsteilung war jedes Mal dieselbe: Ich hab gesucht und gefunden — welches Modul tot ist, welcher Patch niemandem mehr gehört, wo der Cache-Tag falsch zeigt. Dann hab ich es einem Coding-Agent übergeben. Nicht, weil der es besser kann, sondern weil die Arbeit danach immer gleich ist: Modul entfernen oder ersetzen, Tests laufen lassen, prüfen, ob JSON-LD, Endpunkte und Seiten noch das Erwartete liefern, committen, nächstes. Das ist stumpf und fehleranfällig, wenn man es zwanzigmal von Hand macht — und ein Agent macht es zwanzigmal gleich. Der Nebeneffekt war, dass die ganze Aufräumaktion in einen Bruchteil der Zeit gepasst hat, die ich sonst dafür eingeplant hätte.
Was das nicht ersetzt: das Finden und das Entscheiden. Die Proxy-Klasse hat auch der Agent erst für tot erklärt und dann zurückgenommen. Prüfen bleibt der Job.
Stand jetzt
Upgrade Status meldet null Blocker. Alle Contrib-Pakete und aller eigener Code sind Drupal-11-fähig. Was noch fehlt, ist der Core-Bump selbst — und da wartet noch eine Hürde, die nichts mit Code zu tun hat: Composers policy.advisories.block weigert sich, eine Core-Version mit vergangenem Advisory zu laden, und der Bump muss den Core neu auflösen. Das werden wir bewusst entscheiden müssen, nicht umgehen. Dann Constraints hoch (drupal/core-* auf ^11, PHP auf ^8.3), dann Endpunkte, Webforms, JSON-LD und Breadcrumbs verifizieren.
Zwei lose Enden notieren wir uns: alte migrate_map_*-Tabellen liegen vermutlich noch in der Datenbank, und ein Commit hat eine Theme-Änderung mit einem „skip frontend“-Flag durchgewunken, was wir nochmal prüfen wollen.
Was wir anders machen: Bei jedem großen Upgrade zuerst die Frage stellen, was seit dem letzten eigentlich gestorben ist — bevor wir anfangen, es mitzuschleppen.