Nowoczesny węzeł drogowy ze smugami świateł na wyznaczonych pasach jako metafora tras URL

URL-e w Drupalu 11.4: zamiast sklejać stringi, użyj API core

Drupal 8 ukazał się w 2015 roku, a mimo to w kodzie wciąż można znaleźć URL-e ręcznie składane ze stringów zamiast generowanych za pomocą narzędzi dostępnych w core. Takie skróty nadal powodują problemy w Drupalu 11.4.

Link zbudowany jako '/node/' . $id . '?source=' . $source może działać poprawnie, dopóki w serwisie nie pojawi się alias, prefiks językowy albo wartość parametru zawierająca &. Drupal ma już API obsługujące te przypadki. Pokazujemy, kiedy używać Url::fromRoute(), Url::fromUri() i Url::fromUserInput() oraz jak renderować linki bez utraty metadanych cache.

Przykłady dotyczą Drupala 11.4 i zostały zweryfikowane na wersji core 11.4.9. Fragmenty kodu są przeznaczone do użycia we własnych modułach i wymagają działającej instalacji Drupala.

W tym artykule:

Które API do URL-i wybrać?

Zacznij od tego, jakie informacje o adresie docelowym są już dostępne w kodzie. Jeżeli znana jest nazwa trasy, przekaż ją Drupalowi zamiast ręcznie odtwarzać jej aktualną ścieżkę. Jeżeli encja została już wczytana, można skorzystać z jej szablonu linku.

Co masz do dyspozycji Czego użyć Przykład
Trasę Drupala i jej parametry Url::fromRoute() entity.node.canonical z parametrem node
Wczytaną encję $entity->toUrl() Kanoniczna strona węzła
Zewnętrzne URI Url::fromUri() https://example.com/docs
Lokalny zasób bez trasy Url::fromUri() ze schematem base: base:robots.txt
Względną ścieżkę wpisaną przez redaktora Url::fromUserInput() /contact, ?page=2 lub #details
Tekst linku i obiekt Url Link::fromTextAndUrl()->toRenderable() Link w tablicy renderowania

Obiekt URL opisuje adres docelowy. Samo jego utworzenie nie potwierdza jednak, że adres jest poprawny ani że użytkownik ma dostęp do wskazanego zasobu. Ma to szczególne znaczenie w przypadku linków do stron administracyjnych oraz adresów tworzonych na podstawie danych pochodzących od użytkowników.

Generowanie linków na podstawie tras i encji

Rozważ ręcznie zbudowaną ścieżkę:

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

Taki zapis uzależnia kod od konkretnej struktury ścieżki i omija mechanizm generowania URL-i w Drupalu. Zastąp go nazwą trasy i jej parametrami:

use Drupal\Core\Url;

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

Drupal może wtedy zastosować mechanizmy przetwarzania generowanych ścieżek, w tym skonfigurowane aliasy i prefiksy językowe. Wynik zależy od konfiguracji serwisu. Nie zakładaj, że węzeł (node) 42 zawsze będzie dostępny pod adresem /node/42 ani że każdy język będzie miał własny prefiks.

Jeżeli węzeł został już wczytany, pobierz jego kanoniczny URL:

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

W przypadku innych typów encji sprawdź, jakie szablony linków są dla nich dostępne. Nie każda encja ma stronę kanoniczną. Generowanie URL-a nie zastępuje również sprawdzenia, czy encja istnieje i czy użytkownik może ją zobaczyć.

W Twig link do znanej trasy można utworzyć funkcją path():

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

Przykład zakłada, że szablon otrzymuje węzeł, a komponent prawidłowo uwzględnia wymagania dotyczące dostępu i cache. Gdy potrzebny jest bezwzględny URL trasy, użyj funkcji Twig url(). Żadna z tych funkcji nie nadaje użytkownikowi uprawnień do strony docelowej.

Zewnętrzne URI i ścieżki wpisane przez użytkownika

Url::fromUri() stosuj wtedy, gdy masz URI, a nie nazwę trasy:

use Drupal\Core\Url;

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

Schemat base: pozwala utworzyć adres względem ścieżki bazowej instalacji Drupala. Jest przeznaczony dla zasobów, które nie są obsługiwane przez routing Drupala. Nie sprawdza, czy plik rzeczywiście istnieje.

Nie przekazuj /node/42 bezpośrednio do fromUri() jako bezwzględnego URI: zwykła ścieżka względna nie ma schematu. Dla względnej ścieżki wpisanej przez redaktora użyj fromUserInput():

use Drupal\Core\Url;

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

Metoda wymaga wartości zaczynającej się od /, ? albo #. Gdy istnieje pasująca trasa, może wykorzystać routing i przetwarzanie ścieżek Drupala. Przyjmuje jednak również lokalne ścieżki, które nie odpowiadają żadnej trasie. Metoda ta nie służy do wykrywania niedziałających linków. Nieprawidłowe dane należy obsłużyć podczas walidacji formularza, zamiast wyświetlać użytkownikowi komunikat o wyjątku.

Zewnętrzne adresy wpisywane przez użytkowników muszą przejść walidację zgodną z przeznaczeniem pola. Pole przeznaczone na adresy internetowe powinno mieć jasno określone dozwolone schematy, zwykle http i https. Mechanizm przekierowań może dodatkowo wymagać listy dozwolonych hostów. fromUri() tworzy obiekt URL, ale nie zastępuje walidacji przekierowań, kontroli żądań do zewnętrznych serwerów ani reguł biznesowych.

Parametry zapytania, fragmenty i opcje językowe

Doklejanie '?q=' . $value powoduje problemy, gdy wartość zawiera spacje, ampersandy lub inne znaki zastrzeżone. Przekaż niezakodowane wartości w opcji query i pozostaw ich kodowanie Drupalowi:

use Drupal\Core\Url;

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

Parametry w tym przykładzie pokazują jedynie sposób budowania adresu. Samo dodanie tych parametrów nie sprawi, że strona węzła zacznie obsługiwać wyszukiwanie lub paginację. Fragment zadziała tylko wtedy, gdy na stronie docelowej istnieje element HTML o odpowiednim ID.

Nie koduj wartości parametrów samodzielnie przed przekazaniem i nie umieszczaj # na początku opcji fragment. UrlHelper::buildQuery() przydaje się, gdy osobna integracja rzeczywiście potrzebuje wyłącznie ciągu parametrów zapytania. Przy budowaniu URL-a w Drupalu użyj opcji query zamiast samodzielnie składać ciąg parametrów.

Opcja language przyjmuje obiekt języka, a nie jego kod przekazany jako string. W klasie serwisu ze wstrzykniętym menedżerem języków może to wyglądać następująco:

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

Przykład zakłada wczytany $node oraz serwis language_manager wstrzyknięty jako $this->languageManager, o typie Drupal\Core\Language\LanguageManagerInterface. Wskazanie języka dla URL-a nie tworzy brakującego tłumaczenia treści. Zanim założysz, że URL będzie miał konkretną postać, sprawdź również konfigurację negocjacji języka i aliasów.

Dla linków w wiadomościach e-mail lub kanałach RSS absolute => TRUE pozwala uniknąć adresu względnego. W zadaniach uruchamianych z wiersza poleceń trzeba również zapewnić właściwy kontekst URI serwisu. Samo ustawienie opcji absolute nie określi właściwej domeny produkcyjnej.

Renderowanie linków i sprawdzanie dostępu

Jeżeli link ma być wyświetlany na stronie Drupala, pozostaw jego generowanie systemowi renderowania zamiast ręcznie składać znacznik <a>:

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

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

Zwróć $build z funkcji odpowiedzialnej za zbudowanie danego fragmentu strony. W klasie obiektowej użyj wstrzykniętego serwisu tłumaczeń lub odpowiedniego traitu, zamiast dodawać kolejne globalne wywołania serwisów. W tym proceduralnym przykładzie funkcja t() pozwala zachować krótszy zapis.

Równoważna tablica renderowania wygląda tak:

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

Element linku odpowiada za generowanie odnośnika i przekazywanie związanych z nim metadanych. Nie sprawdza jednak automatycznie dostępu do trasy, gdy od tego ma zależeć widoczność linku. Jeśli link ma być widoczny wyłącznie dla użytkowników z dostępem do strony docelowej, dodaj wynik tej kontroli:

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

Wartość TRUE przekazana jako drugi argument sprawia, że metoda zwraca obiekt wyniku wraz z informacjami o cache, a nie tylko wartość logiczną. Przekazanie tego obiektu do #access pozwala Drupalowi uwzględnić wynik podczas przechowywania wyrenderowanej treści w pamięci podręcznej.

Takie sprawdzenie dostępu ma znaczenie dla URL-i powiązanych z trasami. Dla URI niepowiązanego z trasą Url::access() zwraca dostęp jako dozwolony. Nie oznacza to jednak sprawdzenia uprawnień w zewnętrznym systemie. Niezależnie od widoczności linku trasa docelowa nadal musi samodzielnie egzekwować kontrolę dostępu podczas obsługi żądania.

Metadane cache, gdy potrzebny jest string

Niektóre integracje wymagają zwykłego ciągu znaków z adresem. Url::toString() generuje taki ciąg, ale przy domyślnym wywołaniu zwracany string nie zawiera metadanych cache. Jeżeli taki string trafi do treści buforowanej przez Drupala, trzeba osobno zebrać i przekazać związane z nim metadane cache:

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) zwraca obiekt GeneratedUrl. W przykładzie adres jest wyświetlany jako zwykły tekst, a metadane zebrane podczas jego generowania są przekazywane do tablicy renderowania. W bardziej złożonym komponencie należy połączyć te metadane z istniejącymi informacjami o cache, zamiast je zastępować.

Ten kod uwzględnia tylko metadane związane z generowaniem URL-a, a nie wszystkie zależności komponentu. Jeżeli wynik zależy również od etykiety encji, kontroli dostępu lub konfiguracji, trzeba osobno uwzględnić związane z nimi metadane cache. Dla zwykłego klikalnego odnośnika prostszym wyborem pozostaje toRenderable().

Nie myl Url::toString() z Link::toString(). Pierwsza metoda generuje URL, druga kod HTML linku. Dokumentacja klasy Link w core zaleca użycie toRenderable() przy renderowaniu HTML, aby zachować informacje o cache w systemie renderowania Drupala.

Co powinien sprawdzić zespół?

Właściwe API eliminuje potrzebę ręcznego składania adresów, ale podczas przeglądu kodu nadal trzeba uwzględnić konfigurację serwisu. Przed scaleniem zmian warto sprawdzić następujące przypadki:

  • Aliasy i języki: węzeł z aliasem, drugi język URL-a i brakujące tłumaczenie.
  • Parametry zapytania: spacje, &, znaki spoza ASCII oraz przypadki pozwalające wykryć podwójne kodowanie.
  • Kontekst instalacji: serwis w podkatalogu i bezwzględne linki generowane z wiersza poleceń.
  • Dostęp i cache: użytkownicy z różnymi uprawnieniami, również po zapisaniu wyniku w pamięci podręcznej i przełączeniu na innego użytkownika.
  • Dane wejściowe: poprawne lokalne odwołania, nieistniejące strony docelowe oraz wartości odrzucane przez reguły dotyczące dozwolonych schematów lub hostów.
  • HTML: etykiety linków zawierające znaki < i & oraz sprawdzenie, czy surowy kod HTML nie omija mechanizmu bezpiecznego kodowania znaków.

Własne reguły walidacji sprawdzaj testami jednostkowymi. Wygenerowane linki, aliasy, kontrolę dostępu i wyrenderowaną treść weryfikuj testami funkcjonalnymi. Nie wpisuj na sztywno jednej oczekiwanej ścieżki, jeśli testowane zachowanie powinno uwzględniać konfigurowalne aliasy.

Zasadę dla zespołu można streścić krótko: używaj tras dla znanych stron Drupala, szablonów linków dla wczytanych encji, URI dla adresów bez trasy oraz tablic renderowania dla linków wyświetlanych na stronie. Konwersję do stringa stosuj wtedy, gdy wymaga tego konkretny interfejs, a nie jako domyślny sposób budowania HTML.

Warto również przeczytać artykuł Łączenie stringów i zmiennych w PHP 8+: przykłady i częste błędy.

Potrzebujesz pomocy z kodem Drupala?

Ścieżki zapisane na sztywno, niespójna kontrola dostępu i linki działające inaczej po zmianie języka to konkretne obszary, które warto objąć przeglądem kodu. Zespół Droptica zajmujący się rozwojem stron na Drupalu może pomóc uporządkować moduły niestandardowe i zweryfikować ich działanie w serwisie. Porozmawiajmy o zakresie przeglądu kodu i zmianach, które warto wprowadzić w projekcie.