7 tip

7 trików w tworzeniu skórki Drupala, o których mogłeś nigdy nie słyszeć

Tworzenie skórek w Drupalu nie jest aż tak proste, jak mogłoby się wydawać. Dlatego jest tu przestrzeń, aby ułatwić sobie pracę. Często jednak w tym celu sięgamy po metody na skróty, ale te nie zawsze przynoszą pożądane rezultaty lub też nie są zgodne z filozofią Drupala i ogólnie przyjętymi dobrymi praktykami.

Zapewne wielu z Was wie, że podobnie jak w każdym innym CMS-ie czy frameworku, na którym pracujemy, tak i w Drupalu możemy osiągnać te same cele, obierając odmienne drogi do ich dotarcia. W toku zdobywanego przeze mnie doświadczenia wiele razy widziałem częste błędy popełniane przez użytkowników Drupala, zdążyłem prześledzić niejeden kod tworzony zarówno przez początkujących jak i doświadczonych Drupal developerów i wiem, jak mocno różnią się pomiędzy nimi stosowane podejścia. W ramach niniejszego artykułu chciałbym przybliżyć 7 wybranych przeze mnie sztuczek (lub po prostu dróg na skróty), które ułatwiają pracę przy tworzeniu skórek, a o których wielu programistów, z którymi przyszło mi pracować nigdy nie słyszało.

Ze względu na to, że z każdym wydaniem podwersji w Drupalu zachodzą różne istotne zmiany, chciałbym na wstępie podkreślić omawiane tu przykłady są zgodne z wersją core 8.9.x. Wszystkie prezentowane tricki, choć mogą być niekiedy kontrowersyjne w użyciu, to są one w pełni zgodne ze sztuką i mogą być stosowane w praktyce, w realnych projektach.

1. Użycie kluczu #context w tablicach renderowania

Jednym z powszechnym problemów przy tworzeniu szablonów, jest możliwość przekazywania danych pomiędzy nimi. Najczęsciej wymaga to od programistów wykonywania złożonych operacji np. przy procesowaniu szablonów. Problem narastał jeszcze bardziej, kiedy pewne hooki zostały już zapisane w pamięci podręcznej, a przekazywanie danych nadal było potrzebne. Pomysłów na rozwiązanie problemu może pojawić się wiele, ale wraz z wersją Drupala 8.8 sprawę rozwiązuje klucz #context.

Załóżmy, że mamy zaimplementowany hook_preprocess_node, w którym dodajemy jakiś dodatkowy element wykorzystujący dedykowany szablon, ale chcielibyśmy w nim nadal mieć dostęp do obiektu node. Z pomocą przychodzi właśnie klucz #context.

function my_module_preprocess_node(array &$variables) {
  $node = $variables['node'];

  $variables['my_custom_element'] = [
    '#theme' => 'my_custom_theme',
    '#title' => t('My custom theme title'),
    '#context' => [
      'entity' => $node,
    ],
  ];
}

Dzięki niemu staje się możliwe przekazanie dowolnych danych z jednej tablicy renderowania, do innej. Można w niej przemycić cokolwiek, np. instancję obiektu, który będzie nam potrzebna w dalszym przetwarzaniu:

function my_module_preprocess_my_custom_theme(array &$variables) {
  $node = $variables['#context']['entity'];
  // Do whatever you needed with the node entity in your custom theme...
}

Warto wspomnieć, że ta zmienna jest dostępna także w hook_theme_suggestion_alter, co może dodatkowo ułatwić tworzenie sugestii nazw szablonów zależnych od dodatkowego kontekstu.

2. Obsługa cache w szablonach twig

Coraz bardziej wymagające projekty graficzne serwisów internetowych, a także optymalizacja stron np. pod kątem ilości zwracanego kodu HTML, wymusza na nas pewne skrótowe rozwiązania, np. wyciąganie wartości pola bezpośrednio w szablonie encji (np. typu node). Takie rozwiązania, jak np.

{{ content.field_my_reference_field.0 }}

w połączeniu z wyświetlaniem jedynie pojedynczych pól zamiast zmiennej content, mogą mieć swoje zgubne skutki w postaci problemów z pamięcią podręczną. W tablicach renderowania poza finalnym kodem HTML, znajdują się także klucze odpowiedzialne za przechowywanie takich metadanych pamięci podręcznej: tag, context czy max-age. Jeśli chcecie mieć zawsze pewność, że te metadane zostaną poprawnie pozbierane przez serwis renderer, wystarczy dodać w szablonie jedną magiczną linię:

{% set rendered = content|render %}

Na przykładzie szablonu noda będzie to zmienna content, ale może to być jakaś dowolna "główna" zmienna waszego szablonu. Przepuszczenie takiej głownej tablicy renderowania przez filtry render pozwala na poprawne zebranie wszystkich metadanych pamięci podręcznej, bez konieczności wyświetlania zawartości całej zmiennej content. Jednocześnie takie rozwiązanie nie ma żadnego negatywnego wpływu na wydajność wykonywania kodu, o ile używamy mechanizmów pamięci podręcznej.

3. Theme suggestion bez użycia hook_theme_suggestion_

Tak, tak, brzmi to jak coś zupełnie szalonego, a jednak jest to rowiązanie dostępne "out of the box". Wystarczy w dowolnej tablicy renderowania (np. w hook_preprocess_HOOK) dodać do wartości kryjących się w kluczach #theme lub theme_hook_original dodatkową sugestię (zachowując konwencję nazewnictwa szablonów). W efekcie tak ustawiona wartość zmiennej

function my_module_preprocess_paragraph(array &$variables) {
  $paragraph = $variables['paragraph'];

  if ($paragraph->bundle() === 'my_awesome_bundle') {
    $variables['theme_hook_original'] = 'paragraph__my_custom_theme_suggestion';
  }
}

powoduje zarejestrowanie sugestii dla danego szablonu bazowego (paragraph), pozwalającej na utworzenie pliku szablonu o nazwie: paragraph-my-custom-theme-suggestion.html.twig

To rozwiązanie ograniczyłbym mimo wszystko do wyjątkowych przypadków. Do ogólnego zastosowania zalecałbym jednak użytkowanie dedykowanego w tym celu hooku.

4. Bezpieczny zamiennik filtra |raw

Filtr ten na ogół uznawany jest za potencjalnie niebezpieczny w użyciu (zwiększa możliwość powodzenia ataków XSS) i nawet stosowany w pojedynczych przypadkach, ale powinno się go unikać jak to tylko możliwe. Praktycznie zawsze namawiam każdego, kto go stosuje do poszukania innego, lepszego sposobu, zwłaszcza że jeden z nich jest tak banalnie prosty w użyciu, co zaraz zobaczycie na przykładzie.

Założmy, że posiadamy pole, które finalnie ma wyświetlić obrazek, ale z jakiegoś powodu w naszym konkretnym przypadku chcielibyśmy pominać cały wygenerowany po drodze kod HTML. Najłatwiej byłoby to zrobić w ten sposób:

{{ content.field_my_image_field|render|striptags('<img>')|trim }}

Doświadczeni programiści Drupala wiedzą, że takie rozwiązanie zamiast wyświetlić obrazek na stronie, wyświetli nam całość jako zakodowany do encji HTMLowych czysty tekst. Owszem, można dodać filtr raw, ale można to zrobić inaczej z wykorzystaniem Render API Drupala, używając klucza #markup w taki sposób:

{{ {'#markup': content.field_my_image_field|render|striptags('<img>')|trim} }}

Sposób może nieco mniej czytelny niż z użyciem filtra, ale za to bezpieczny. Kod HTML będzie tutaj nadal filtrowany i przepuszczone zostaną tylko dozwolone bezpieczne tagi.

5. Odwołanie się do pliku szablonu z innej skórki/modułu

Jeżeli w waszych plikach szablonów jeszcze nie wykorzystujecie takich mechanizmów Twig jak: include, embed czy extend to jest to moment, w którym sugeruję, by regułę DRY wziąć sobie bardziej do serca w każdym aspekcie programowania, czy jak w naszym wypadku ogólnie webdewelopmentu. Skórki to miejsca, które najbardziej cierpią z powodu zduplikowanego kodu, zarówno w plikach szablonów, jak i w arkuszach styli.

Wiele razy spotkałem się z tym, że skopiowany został cały plik szablonu np. ze skórki dziedziczonej, czy z modułu tylko po to by np. dodać jakiś otaczający go kod HTML lub zmodyfikować pojedynczy blok szablonu. Taki sam efekt można osiągnać odnosząc się do macierzystego pliku z wykorzystaniem aliasów @module_name @theme_name.
Założmy, że mamy plik szablonu (card) w naszym przykładowym module o nazwie custom_card

<div class="card">
  {% block card_header %}
    <div class="card__header">{{ header }}</div>
  {% endblock %}
  
  {% block card_content %}
    <div class="card__content">{{ content }}</div>  
  {% endblock %}
  
  {% block card__footer %}
    <div class="card__footer">{{ footer }}</div>
  {% endblock %}
</div>

A teraz w naszej skórce potrzebowalibyśmy dodać w bloku header jeszcze jakieś dodatkowe elementy, np. ikonę. To, co spotyka się najczęściej, to skopiowanie całego kodu szablonu, ale można to zrobić prościej i bez konieczności powtarzania kodu:

{% extends '@custom_card/card.html.twig' %}

{% block header %}
    <div class="card__header">
      <div class="icon">{{ icon }}</div>
      {{ header }}
    </div>
{% endblock %}

6. Przeładowanie widoku z użyciem InvokeCommand

Jak wiadomo moduł Views daje możliwość tworzenia widoków używających technologii AJAX. Wykorzystywany jest on do dynamicznego przełączania się między poszczególnymi stronami widoku, ale także do wyświetlania rezultatów reakcji na zmiany wartości z udostępnionego formularza. Co jednak zrobić w przypadku, gdy zawartość widoku musi zostać odświeżona w związku z wykonaniem jakiejś akcji poza nim, np. po wysłaniu jakiegoś żądania do kontrolera z innego bloku? Czy też przesłania formularza znajdującego się na tej samej stronie? Otóż z pomocą przychodzą dwie bardzo wygodne w wykorzystaniu funkcjonalności dostępne w core Drupala.

Pierwsza z nich do domyślnie zarejestrowane na widoku zdarzenie RefreshView, które powoduje odświeżenie widoku z wykorzystaniem AJAX (polecam wywołać nawet w konsoli przeglądarki na waszym przykładowym widoku i zaobserwować efekt).

Drugi mechanizm to InvokeCommand, który pozwala w odpowiedzi typu AJAX, zwrócić wywołanie metody jQuery, wraz z zadanymi parametrami na elemencie DOM. W połączeniu daje to taki przykładowy kod:

class MyCustomController extends ControllerBase {

  /**
   * Perform the update and refresh the view.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   Ajax response.
   */
  public function doUpdateAndRefresh(): AjaxResponse {
    // Perform some sort of logic needed for your controller.
    $this->doUpdateOperations();

    // Refresh the view.
    $response = new AjaxResponse();
    $response->addCommand(new InvokeCommand('#my-view-block', 'trigger', ['RefreshView']));

    return $response;
  }

}

W moim realnym przykładzie wewnątrz wygenerowanego widoku znajdowały się formularze do zatwierdzania użytkowników. W odpowiedzi na zapis takiego formularza potrzebowałem wyświetlać już zaktualizowaną listę. Jak widać w zasadzie jedna dodatkowa linia wykonała całą potrzebną magię bez żadnego dodatkowego nakładu pracy.

7. Wykorzystanie Drupal.debounce

Na sam koniec zostawiam coś, co nie tyle jest jakąś sztuczką, ile dobrą praktyką. Do rdzenia Drupala dołączona jest cała biblioteka Underscore.js, jednakże wielu programistów w ogóle z niej nie korzysta, a zawiera ona szereg przydatnych do codziennego wykorzystania funkcjonalności. Jednym z najczęstszych przypadków gdzie możliwości tej biblioteki nie są wykorzystywane, jest tzw. deboucing, czyli zapobieganie wielokrotnemu wykonaniu kodu w reakcji na dane zdarzenie - najczęściej jest to zmiana szerokości okna przeglądarki. Wielokrotnie spotykałem się z kodem który przypominał:

function onResize(callback) {
  var timer;
  return function (event) {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(callback,100, event);
  };
}

$(window).resize(onResize(function(e) {
  // Resize implementation goes here.
}));

Oczywiście jest to przykładowa i najprostsza implementacja, a taki kod wykona się poprawnie. Pytanie: tylko po co wymyślać koło od nowa? Odpowiedź: po części, żeby w rdzeniu Drupala poprawić zgodność z Drupalem, a także żeby zwiększyć wygodę pracy deweloperom - znajduje się tam bowiem port debouncera z biblioteki underscore. Wystarczy dodać do swojej biblioteki zależność:

- core/drupal.debounce

a w kodzie JS dodać następującą implementację:

$(window).resize(Drupal.debounce(function () {
  // Resize implementation goes here.
}, 100));

Prawda, że prościej? I co najważniejsze wykorzystujemy coś, co już raz zostało dobrze napisane.

Podsumowanie

Chociaż różnych ciekawych podejść do pracy ze skórką, z którymi w naszej agencji drupalowej się spotkałem można wymieniać dużo, to trzeba podkreślić, że wiele rzeczy przychodzi wraz doświadczeniem. Zwłaszcza początkujący developerzy już od samego początku powinni podążać za filozofią Drupala i starać się rozumieć, dlaczego wykonują daną rzecz w określony sposób. W dalszym etapie pozwala to łatwiej dostrzeć jakie możliwości kryje za sobą Drupal i jak je wykorzystać, by zwiększyć komfort i wydajność pracy, nie tracąc tym samym na czystości kodu.