Drupal nie tylko pozwala na korzystanie z pewnych standardowych hooków omówionych w poprzednich rozdziałach. Ponadto bez żadnego problemu można definiować nowe hooki, które będą implementować inne moduły.

Definicja hooka

Dla przypomnienia - co to jest hook?
Hook to pewna rodzina funkcji drupalowych, które wywoływane są kaskadowo w odpowiedniej kolejności.

Patrząc na konkretny przykład - Drupal dostarcza hook_menu() odpowiedzialny za tworzenie menu i struktury stron oraz hook_menu_alter() umożliwiający bezproblemową edycję elementów dodanych przez inne moduły.

W przypadku, gdy podczas renderowania strony (lub przebudowania cache'u, jeśli jest włączony) Drupal szuka wszystkich funkcji implementujących odpowiedni hook, po czym je wykonuje, umożliwiając w prosty sposób modyfikację poszczególnych elementów.
Informacje na temat hooków przechowywane są w tabeli {cache_bootstrap}.

Jeśli więc zależy nam na tym, aby umożliwić innym funkcjom reagowanie na zdarzenia znajdujące się w naszym module warto stworzyć własne hooki.

W języku naturalnym moglibyśmy to opisać w następujący sposób:

  • moduł wywołujący hook - właśnie wywołuję akcję wyświetlenia użytkownikowi wiadomości, jeśli nie jest autorem przeglądanego node'a; czy któryś z Was [modułów] chciałby coś jeszcze dołożyć?
  • moduł implementujący dostępy - tak, ja bym chciał zwrócić wtedy kod braku dostępu
  • moduł mailowy - a ja chciałbym dorzucić maila do autora przeglądanego node'a, że ktoś próbuje obejrzeć jego supertajną treśc

Moduł implementujący dostepy i moduł mailowy w przełożeniu na api drupala muszą po prostu zaimplementować odpowiednie hooki definiowane w module moduł wywołujący hook.

Typy hooków

Drupal implementuje dwa główne typy hooków oraz jeden podtyp:

  1. hook_menu() - standardowy hook, zazwyczaj zwraca pewną strukturę danych - w przypadku hook_menu() jest to ustrukturyzowana tablica ze zdefiniowanymi elementami drupalowego routera
  2. hooki modyfikujące istniejące elementy
    1. hook_form_alter() - hook, który nie musi niczego zwracać; jego zadaniem jest modyfikacja istniejących elementów, także pierwszy parametr przekazywany do funkcji jest przekazany przez referencję
    2. hook_form_FORM_ID_alter() - hook podobny do powyższego - ma za zadanie jednak nie modyfikację wszystkich elementów, formalnie powinien modyfikować jedynie element o przekazanym ID

Funkcje pomocnicze

Aby poprawnie zaimplementować własne hooki nalezy się zapoznać z następującymi funkcjami pomocniczymi:

  • module_invoke_all($hook) - w przypadku standardowych hooków, gdzie $hook jest nazwą
  • module_invoke($module, $hook) - wywołanie jedynie hooka z modułu $module (bez rozszerzenia .module) o nazwie $hook
  • drupal_alter($type, &$data) - wywołanie hooków typu *_alter

Studium przypadku - opis problemu

Wyobraźmy sobie następującą sytuację - mamy moduł bazujący na api różnych serwisów społecznościowych. Dla każdego node'a chcemy umożliwić użytkownikowi przesłanie dodanej przez niego treści do konkretnego serwisu.
Aby po pierwsze nie tworzyć jednego ogromnego modułu ze wszystkimi funkcjami wysyłającymi dane do portali społecznościowych, po drugie uprościć innym użytkownikom dokładanie swoich elementów warto by było zaimplementować hooki.

Przypuśćmy, że w zakładce node'a wyświetlamy użytkownikowi linki do wszystkich serwisów społecznościowych, jakie mamy udostępnione w systemie. Linki te powinny być zbierane od wszystkich modułów zależnych. W razie kłopotów powinniśmy też dać możliwość zmiany istniejącego już linku. Ponadto jeden z serwisów społecznościowych nie akceptuje treści w formie html, musimy mu wysyłać każde pole z naszego content type'a.

Takie założenia rodzą konieczność zbudowania różnych typów hooków:

  • hook_social_site_list()
  • hook_social_site_list_alter()
  • hook_social_site_list_CONTENT_TYPE_alter()

W module chciałabym pokazać głównie hooki, także wszystkie inne funkcje będą typowo zaślepkowe. Nie zostanie stworzony faktyczny mechanizm wysyłania danych do serwisów społecznościowych.

Implementacja - moduł główny

<?php

/**
 * Implements hook_persmission().
 */
function sendtosocial_permission() {
  return array(
    'send to social' => array(
      'title' => t('Send to social'), 
      'description' => t('Sends node data to social sites.'),
    ),
  );
}

/**
 * Implements hook_menu().
 */
function sendtosocial_menu() {
  $items['node/%node/social'] = array(
    'title' => 'Send to social options',
    'access arguments' => array('send to social'),
    'type' => MENU_LOCAL_TASK,
    'page callback' => 'sendtosocial_connect_page',
    'page arguments' => array(1), // loaded node object %node
  );
  return $items;
}

/**
 * Page with all statistics buttons.
 * Calls for all hook_statistics_connect_links();
 */
function sendtosocial_connect_page($node) {
  $hook = 'social_site_list';
    
  // Call for basic hook.
  foreach (module_implements($hook) as $module) {
    $result['items'][$module] = array(
      'data' => module_invoke($module, $hook),
    );
  }
  /* Call for hook alter */
  drupal_alter($hook, $result, $node);
  /* Call for hook CONTENT TYPE alter. */
  $hook_id_alter = $hook . '_' . $node->type;
  /* $result is a reference */
  drupal_alter($hook_id_alter, $result, $node);

  /* If list is empty return proper message. */
  if(!count($result['items'])) {
    return t('Sorry, there is no social sited you can send data to.');
  }
  /* Otherwise show links. */
  $result['type'] = 'ul';
  $result['title'] = t('Available social sites systems:');
  $result['attributes'] = array();
  return theme('item_list', $result);
}

hook_permission() oczywiście pozwala na zarządzanie uprawnieniami per rola. W hook_menu() natomiast definiujemy stronę, na której pojawią się linki do przesyłania treści pozbierane z innych modułów za pomocą hooków. Także interesuje nas głównie function sendtosocial_connect_page($node).

W pętli musimy zebrać to co zwracają nam wszystkie hooki, aby móc wyświetlić użytkownikom linki do portali, które są zintegrowane z naszym systemem. Każdy hook powinien zwracać odpowiednią wartość, z której budujemy następnie system linków. Po stronie programisty implementującego hook leży zwrócenie poprawnej wartości, formalnie nie musimy więc przy wywoływaniu hooka robić walidacji, natomiast warto dać znać końcowemu użytkownikowi w jakim formacie oczekujemy zwrotu.

W przypadku hook_social_site_links() jest to format zwrócony przez funkcję l(), pojedyncza wartość.

Aby funkcjonalnie pokazać jak implementować hooki można utworzyć również w naszym module plik module_name.api.php. Hooki z tego pliku nie będą wywoływane, to tylko plik poglądowy dla innych programistów. Warto również wspomagać się notacją doxygenową (jak reszta Drupala), gdyż pozwala to na utworzenie prostej i przejrzystej dokumentacji dla developerów (oraz umożliwia różnym IDE podpowiadanie składni).

Implementacja hooków

sendtosocial_superfb.module

<php

/**
 * Implements hook_menu().
 */
function sendtosocial_superfb_menu() {
  $items['node/%node/social/superfb'] = array(
    'title' => 'Send data to superfb',
    'type' => MENU_CALLBACK,
    'access arguments' => array('send to social'),
    'page callback' => 'sendtosocial_superfb_send',
  );
  return $items;
}

/**
 * Implements hook_social_site_list().
 */
function sendtosocial_superfb_social_site_list() {
  $nid = arg(1);
  return l(t('Superfb'), 'node/' . $nid . '/social/superfb', array(''));
}

/**
 * Helper function for testing purposes.
 */
function sendtosocial_superfb_send() {
  drupal_set_message('Data is sending to superfb site...', 'status');
  return 'Please wait a little.';
}

W funkcjach pomocniczych, takich jak sendtosocial_superfb_send() nie implementuję niczego poza wyświetleniem wiadomości dla użytkownika. Wtedy dokładnie widać jaki hook i gdzie zostaje wykonywany.

Implementacją hooka jest oczywiście sendtosocial_superfb_social_site_list(). Buduje on link do połączenia się z serwisem SuperFB.

sendtosocial_weirdsocial.module

<?php

/**
 * Implements hook_menu().
 */
function sendtosocial_weirdsocial_menu() {
  $items['node/%node/social/weirdsocial/article'] = array(
    'title' => 'Send data to weirdsocial article',
    'type' => MENU_CALLBACK,
    'access arguments' => array('send to social'),
    'page callback' => 'sendtosocial_weirdsocial_article_send',
  );
  $items['node/%node/social/weirdsocial/page'] = array(
    'title' => 'Send data to weirdsocial article',
    'type' => MENU_CALLBACK,
    'access arguments' => array('send to social'),
    'page callback' => 'sendtosocial_weirdsocial_page_send',
  );
  return $items;
}

/**
 * Implements hook_social_site_list_CONTENT_TYPE_alter().
 */
function sendtosocial_weirdsocial_social_site_list_article_alter(&$links, $node) {
  $link = l(t('Weirdsocial for article'), 'node/' . $node->nid . '/social/weirdsocial/article', array(''));
  $links['items']['sendtosocial_weirdsocial'] = array(
    'data' => $link,
  );
}

/**
 * Implements hook_social_site_list_CONTENT_TYPE_alter().
 */
function sendtosocial_weirdsocial_social_site_list_page_alter(&$links, $node) {
  $link = l(t('Weirdsocial for simple page'), 'node/' . $node->nid . '/social/weirdsocial/page', array(''));
  $links['items']['sendtosocial_weirdsocial'] = array(
    'data' => $link,
  );
}

/**
 * Helper function for testing purposes.
 */
function sendtosocial_weirdsocial_article_send() {
  drupal_set_message('Data is sending to weirdsocial article site...', 'status');
  return 'Please wait a little.';
}

/**
 * Helper function for testing purposes.
 .
function sendtosocial_weirdsocial_page_send() {
  drupal_set_message('Data is sending to weirdsocial page site...', 'status');
  return 'Please wait a little.';
}

Serwis WeirdSocial natomiast nie przyjmuje kodu HTML, dlatego musimy wywołać hook'a dla każdego content type'a, aby upewnić się, że przesyłamy wszystkie pola. Zasada działania jest bardzo podobna do zwykłych hooków, różnicą jest przesyłanie pierwszego argumentu przez referencję. Z tegoż samego powodu nie musimy nic zwracać.

sendtosocial_superfb_api.module

<?php

/**
 * Implements hook_menu().
 */
function sendtosocial_superfb_api_menu_alter(&$items) {
  $items['node/%node/social/superfb']['page callback'] = 'sendtosocial_superfb_api_send';
}

/**
 * Implements hook_social_site_list_alter().
 */
function sendtosocial_superfb_api_social_site_list_alter(&$links, $node) {
  $link = l(t('Superfb API'), 'node/' . $node->nid . '/social/superfb', array(''));
  $links['items']['sendtosocial_superfb']['data'] = $link;
}

/**
 * Helper function for testing purposes.
 */
function sendtosocial_superfb_api_send() {
  drupal_set_message('Data is sending to superfb site through API...', 'status');
  return 'Please wait a little.';
}

Po kilkudniowym developingu naszej paczki modułów okazało się, że serwis SuperFB nieustannie testuje API. Nie mamy aż tylu specjalistów, aby nadążać za zmianami, skontaktowaliśmy się więc z dostawcą "pośredniego", niezmiennego API. Innymi słowy zrzucamy z barków odpowiedzialność za aktualizacje modułu. Oczywiście można zmodyfikować moduł sendtosocial_superfb, ale mamy nadzieję, że zmiany API są tymczasowe, poza tym dostawca SuperFB Api jest drogi.

Z pomocą przyjdzie nam zwykły hook_alter. W momencie, gdy włączymy moduł zmieni się adres strony odpowiadającej za połączenie z systemem SuperFB (hook_menu_alter()) oraz treść linka menu (hook_social_site_list_alter()). Rozwiązanie ma taką zaletę, że po wyłączeniu modułu nie trzeba wprowadzać żadnych zmian - Drupal skorzysta ze swojego pierwszego połączenia z SuperFB.

Podsumowanie

Drupal dostarcza programistom naprawdę potężnego systemu, dzięki któremu można w prosty i przyjemny sposób rozszerzać pewne funkcjonalności. System hooków jest "przeciwieństwem" dziedziczenia w OOP o tyle, że w dziedziczeniu to potomek wywołuje rodzica (lub też nie), a w przypadku hooków to hook nadrzędny określa co jak i kiedy wywołać. Nieco bardziej przypomina wzorzec Observera, choć i do niego mu daleko.

Abyście w prosty sposób mogli przetestować jak system hooków działa przygotowałam paczkę z plikami do samodzielnej instalacji. Być może komuś się przyda w bardziej dogłębnym zrozumieniu hooków.

Moduły z hook'ami

Dodaj komentarz