Modern highway interchange with light trails on defined lanes as a metaphor for URL routes

Drupal 11.4 URLs: Core-APIs statt zusammengesetzter Strings

Drupal 8 erschien 2015, und trotzdem enthält Drupal-Code noch immer URLs, die aus Strings zusammengesetzt werden, statt die Werkzeuge aus dem Core zu nutzen. Diese Abkürzungen verursachen auch in Drupal 11.4 Probleme.

Ein Link wie '/node/' . $id . '?source=' . $source kann so lange korrekt aussehen, bis die Website einen Alias, ein Sprachpräfix oder einen Query-Wert mit & verwendet. Für genau diese Fälle bringt Drupal bereits APIs mit. In diesem Artikel zeige ich, wann Sie Url::fromRoute(), Url::fromUri() und Url::fromUserInput() einsetzen und wie Sie Links rendern, ohne Cache-Metadaten zu verlieren.

Die Beispiele beziehen sich auf Drupal 11.4 und wurden mit dem Core-Release Drupal 11.4.9 geprüft. Es handelt sich um Fragmente für eigene Module, die innerhalb einer gebooteten Drupal-Anwendung laufen.

In diesem Artikel:

Welche URL-API sollten Sie verwenden?

Gehen Sie davon aus, welche Informationen Ihr Code über das Ziel hat. Wenn ein Routenname bekannt ist, übergeben Sie diesen Namen an Drupal, statt den aktuellen Pfad der Route nachzubauen. Wenn bereits eine Entität vorliegt, verwenden Sie deren Link-Template.

Was Sie haben Verwenden Beispiel
Eine Drupal-Route und ihre Parameter Url::fromRoute() entity.node.canonical mit dem Parameter node
Eine geladene Entität $entity->toUrl() Die kanonische Seite eines Nodes
Eine externe URI Url::fromUri() https://example.com/docs
Eine bekannte lokale Ressource ohne Route Url::fromUri() mit base: base:robots.txt
Ein relativer Pfad, den ein Redakteur eingegeben hat Url::fromUserInput() /contact, ?page=2 oder #details
Linktext und ein Url-Objekt Link::fromTextAndUrl()->toRenderable() Ein Link in einem Render-Array

Ein URL-Objekt beschreibt ein Ziel. Es genehmigt dieses Ziel nicht und gewährt auch keinen Zugriff darauf. Dieser Unterschied ist wichtig bei Links auf Verwaltungsseiten und bei Werten, die von Nutzern stammen.

Links aus Routen und Entitäten erzeugen

Betrachten Sie diesen manuell zusammengesetzten Pfad:

// Avoid building a known Drupal route as a string.
$path = '/node/' . $node_id;

Er schreibt die Pfadstruktur fest und umgeht die URL-Erzeugung von Drupal. Ersetzen Sie ihn durch den Routennamen und die Parameter:

use Drupal\Core\Url;

// Example: node 42 exists on the site.
$url = Url::fromRoute('entity.node.canonical', ['node' => 42]);

Drupal kann dann die ausgehende Pfadverarbeitung der Website anwenden, einschließlich konfigurierter Aliase und Sprachpräfixe. Der resultierende Pfad hängt von der Konfiguration ab: Gehen Sie nicht davon aus, dass Node 42 immer zu /node/42 wird oder dass immer ein Sprachpräfix vorhanden ist.

Wenn Sie einen Node bereits geladen haben, fragen Sie die Entität nach ihrer kanonischen URL:

// $node is a loaded Drupal\node\NodeInterface instance.
$url = $node->toUrl('canonical');

Prüfen Sie bei anderen Entitätstypen, welche Link-Templates sie bereitstellen. Nicht jede Entität hat eine kanonische Seite, und das Erzeugen einer URL ersetzt nicht die Prüfung, ob eine Entität existiert oder ob der aktuelle Nutzer sie sehen darf.

In Twig lässt sich eine bekannte Route mit path() verlinken:

<a href="{{ path('entity.node.canonical', {'node': node.id}) }}">
  {{ 'Read more'|t }}
</a>

Das setzt voraus, dass das Template einen Node erhält und die umgebende Komponente die Anforderungen an Zugriff und Cachebarkeit erfüllt. Verwenden Sie die Twig-Funktion url(), wenn Sie eine absolute Routen-URL brauchen. Keine der beiden Funktionen macht ein Ziel für einen Nutzer ohne Berechtigung zugänglich.

Externe URIs und von Redakteuren eingegebene Pfade

Url::fromUri() ist die richtige Wahl, wenn Ihr Ziel eine URI und kein Routenname ist:

use Drupal\Core\Url;

$documentation = Url::fromUri('https://example.com/docs');
$email = Url::fromUri('mailto:[email protected]');
$robots = Url::fromUri('base:robots.txt');

Das Schema base: erzeugt eine URL relativ zum Basispfad der Drupal-Installation. Es ist für Ressourcen gedacht, von denen Sie wissen, dass das Routing-System von Drupal sie nicht verarbeitet. Ob die Datei existiert, prüft es nicht.

Übergeben Sie /node/42 nicht direkt an fromUri(), als wäre es eine absolute URI: Ein gewöhnlicher relativer Pfad hat kein Schema. Für eine relative Referenz, die ein Redakteur eingegeben hat, verwenden Sie stattdessen fromUserInput():

use Drupal\Core\Url;

$contact = Url::fromUserInput('/contact');
$next_page = Url::fromUserInput('?page=2');
$section = Url::fromUserInput('#details');

Diese Methode verlangt einen Wert, der mit /, ? oder # beginnt. Sie kann einen lokalen Pfad über das Routing und die Pfadverarbeitung von Drupal auflösen, wenn eine passende Route existiert. Sie akzeptiert aber auch lokale Pfade, zu denen es keine Route gibt. Sie ist kein Werkzeug zur Prüfung defekter Links. Behandeln Sie ungültige Eingaben in der Formularvalidierung, statt eine Exception bis zum Besucher durchzureichen.

Wenden Sie bei externen Zielen, die Nutzer angeben, zuerst die vorgesehenen Regeln des Feldes an und erzeugen Sie erst danach die URL. Ein Feld, das nur Weblinks zulassen soll, sollte die gewünschten Schemata ausdrücklich erlauben, in der Regel http und https. Ein Weiterleitungsprozess braucht unter Umständen zusätzlich eine Allowlist für Hosts. fromUri() ist ein Konstruktor und kein allgemeiner Validator für Weiterleitungen, externe Anfragen oder Geschäftsregeln.

Query-Parameter, Fragmente und Sprachoptionen übergeben

Ein angehängtes '?q=' . $value wird mehrdeutig, sobald der Wert Leerzeichen, kaufmännische Und-Zeichen oder andere reservierte Zeichen enthält. Übergeben Sie unkodierte Werte in der Option query und überlassen Sie die Kodierung Drupal:

use Drupal\Core\Url;

$url = Url::fromRoute('entity.node.canonical', ['node' => 42], [
  'query' => [
    'search' => 'R&D / Europe',
    'page' => 2,
  ],
  'fragment' => 'technical-details',
  'absolute' => TRUE,
]);

Die Query oben zeigt nur, wie die URL aufgebaut wird. Eine Node-Seite unterstützt nicht automatisch Suche oder Paginierung, nur weil diese Parameter vorhanden sind. Ein Fragment braucht außerdem eine passende HTML-Element-ID auf der Zielseite.

Kodieren Sie die Query-Werte nicht vorab und setzen Sie kein # in die Option fragment. UrlHelper::buildQuery() von Drupal ist nützlich, wenn eine separate Integration tatsächlich nur einen Query-String braucht. Für eine Drupal-URL ist die Option query besser, als den String selbst zusammenzusetzen.

Die Option language erwartet ein Sprachobjekt, keinen Sprachcode als String. In einem Service mit injiziertem Language Manager kann das so aussehen:

$language = $this->languageManager->getLanguage('pl');
if ($language === NULL) {
  throw new \InvalidArgumentException('Polish is not configured.');
}
$url = $node->toUrl('canonical', ['language' => $language]);

Das Beispiel setzt voraus, dass $node geladen ist und der Service language_manager als $this->languageManager injiziert wurde, typisiert als Drupal\Core\Language\LanguageManagerInterface. Die Sprachauswahl in der URL legt keine fehlende Übersetzung des Inhalts an. Prüfen Sie außerdem die Spracherkennung und die Aliase der Website, bevor Sie eine bestimmte Ausgabe erwarten.

Bei Links in E-Mails oder Feeds verhindert absolute => TRUE ein relatives Ziel. Stellen Sie bei Kommandozeilen-Jobs sicher, dass die Anwendung den richtigen Site-URI-Kontext hat. Eine absolute URL anzufordern, erzeugt nicht von selbst den gewünschten Produktions-Hostnamen.

Wenn ein Link auf einer Drupal-Seite erscheint, lassen Sie ihn im Render-System, statt ein <a>-Tag zusammenzusetzen:

use Drupal\Core\Link;
use Drupal\Core\Url;

$url = Url::fromRoute('user.login');
$build['login'] = Link::fromTextAndUrl(t('Sign in'), $url)
  ->toRenderable();

Geben Sie $build aus dem passenden Build-Callback zurück. Verwenden Sie in einer objektorientierten Klasse den injizierten Übersetzungsservice oder den Translation-Trait, statt weitere globale Serviceaufrufe einzuführen. Hier hält t() das prozedurale Beispiel kurz.

Das entsprechende Render-Array sieht so aus:

$build['login'] = [
  '#type' => 'link',
  '#title' => t('Sign in'),
  '#url' => $url,
];

Ein Link-Render-Element übernimmt die Erzeugung des Links und die zugehörigen Bubbleable-Metadaten. Die Zugriffsprüfung der Route, die Sie für eine bedingte Anzeige brauchen, führt es jedoch nicht automatisch durch. Wenn der Link nur erscheinen soll, wenn der aktuelle Nutzer auf das Ziel zugreifen darf, ergänzen Sie ein Zugriffsergebnis:

$build['login']['#access'] = $url->access(NULL, TRUE);

Das zweite Argument fordert ein Zugriffsergebnis-Objekt mit seinen Cachebarkeits-Informationen an, statt nur eines booleschen Werts. Wenn Sie dieses Objekt über #access übergeben, kann Drupal das Ergebnis beim Caching der gerenderten Ausgabe berücksichtigen.

Diese Prüfung ist bei URLs mit Route sinnvoll. Für eine URI ohne Route gibt Url::access() ein erlaubtes Ergebnis zurück. Die Berechtigungen eines externen Servers prüft es nicht. Die Zielroute muss ihre eigenen Zugriffsregeln bei jeder Anfrage durchsetzen, unabhängig davon, ob ein Link sichtbar ist.

Cache-Metadaten erhalten, wenn ein String nötig ist

Manche Integrationen brauchen eine URL als einfachen String. Url::toString() liefert ihn, doch der Standard-Rückgabewert enthält keine Cache-Metadaten. Wenn dieser String Teil gecachter Drupal-Ausgabe wird, sammeln Sie die Metadaten und geben Sie sie ausdrücklich weiter:

use Drupal\Core\Url;

$url = Url::fromRoute('entity.node.canonical', ['node' => 42]);
$generated = $url->toString(TRUE);

$build = [
  '#type' => 'container',
  'address' => [
    '#plain_text' => $generated->getGeneratedUrl(),
  ],
];
$generated->applyTo($build);

toString(TRUE) gibt ein GeneratedUrl-Objekt zurück. Das Beispiel zeigt damit eine Adresse als reinen Text an und behält dabei die Metadaten, die bei der URL-Erzeugung gesammelt wurden. Führen Sie sie in einem komplexeren Build mit den vorhandenen Cachebarkeits-Informationen der Komponente zusammen, statt diese zu ersetzen.

Das deckt die Metadaten aus der URL-Erzeugung ab, nicht jede Abhängigkeit der umgebenden Komponente. Hängt die Ausgabe auch von einem Entitätslabel, einem Zugriffsergebnis oder einem Konfigurationsobjekt ab, nehmen Sie diese Abhängigkeiten ebenfalls auf. Für einen normalen klickbaren Link bleibt toRenderable() die einfachere Wahl.

Verwechseln Sie Url::toString() nicht mit Link::toString(). Die erste Methode erzeugt eine URL, die zweite Link-Markup. Die Link-Dokumentation im Core empfiehlt toRenderable() für die HTML-Ausgabe, damit die Cache-Informationen in der Render-Pipeline von Drupal bleiben.

Was sollte Ihr Team testen?

Die richtige API ersetzt das manuelle Zusammensetzen von Strings, doch ein Review muss trotzdem die Konfiguration der Website abdecken. Diese Fälle würde ich vor dem Merge einer Änderung prüfen:

  • Aliase und Sprachen: ein Node mit Pfad-Alias, eine zweite URL-Sprache und eine fehlende Übersetzung.
  • Query-Werte: Leerzeichen, &, Nicht-ASCII-Zeichen und Werte, an denen doppelte Kodierung sichtbar wird.
  • Installationskontext: eine Website in einem Unterverzeichnis und absolute Links aus einem Kommandozeilen-Job.
  • Zugriff und Caching: Nutzer mit unterschiedlichen Berechtigungen, auch bei bereits gefülltem Render-Cache nach einem Nutzerwechsel.
  • Nutzereingaben: gültige lokale Referenzen, ein fehlendes Ziel und Werte, die die Schema- oder Host-Regeln des Feldes ablehnen.
  • Markup: Linkbeschriftungen mit Zeichen wie < oder &, ohne das Escaping durch rohes Markup zu umgehen.

Verwenden Sie Unit-Tests für Ihre eigenen Eingaberegeln und funktionale Tests für erzeugte Links, Aliase, Zugriff und gerenderte Ausgabe. Prüfen Sie nicht auf einen festen Pfad, wenn das getestete Verhalten konfigurierbare Aliase berücksichtigen soll.

Die Regel für das Team ist kurz: Routen für bekannte Drupal-Seiten, Link-Templates für geladene Entitäten, URIs für Ziele ohne Route und Render-Arrays für Links auf Seiten. Die Umwandlung in einen String sollte eine bewusste Grenze sein, nicht der Standardweg, HTML zu erzeugen.

Lesen Sie auch: Strings und Variablen in PHP 8+ verbinden: Beispiele und typische Fehler.

Unterstützung bei eigenem Drupal-Code

Wenn Ihre Module fest codierte Pfade, uneinheitliche Zugriffsprüfungen oder gecachte Links enthalten, die sich je nach Sprache unterschiedlich verhalten, lohnt sich ein gezieltes Code-Review. Das Drupal-Entwicklungsteam von Droptica unterstützt Sie beim Refactoring eigener Module und prüft ihr Verhalten auf Ihrer Website. Kontaktieren Sie uns, um über den Code zu sprechen, den Sie verbessern möchten.