Słowem wstępu: wpis tłumaczy działanie Better Updatera po łebkach. Skupia się na najważniejszych elementach i podejrzewam, że wymaga podstawowej wiedzy z tworzenia własnych modułów, aby w pełni go zrozumieć.

Geneza powstania

Standardowo zaleca się instalowanie wszystkich modułów w katalogu sites/all/modules, jakkolwiek Drupal dopuszcza umieszczanie ich również w podfolderach czy też w folderach odpowiednich dla danej witryny w przypadku multi-site installation. Niestety Drupal 7 z modułem update (pozwalającym również na zdalne zaciąganie i rozpakowywanie plików bezpośrednio na serwerze) "zapomniał" o takiej możliwości. Ze względów przyzwyczajeniowych, ilościowych i zwyczajnie estetycznych był więc dla mnie nie do używania. Do czasu.

Hackować core?

Najprostszym sposobem byłaby pewnie "lekka" zmiana w samym corze systemu, jedna linijka i po sprawie. W razie potrzeby podmieniamy.
Czemu w corze? Drupal ma na stałe wpisaną ścieżkę, w której ma instalować moduły, bez względu na to, czego mógłby zażyczyć sobie user.
Takie rozwiązanie jednak niesie ze sobą kilka istotnych niebezpieczeństw. Po pierwsze jednak to core, a hackowanie core'a, jeśli da się coś obejść jest grzechem niemal śmiertelnym. Po drugie i tak wszystkie zmiany znikną z pierwszym updatem Drupala. Ponadto takich rzeczy się nie robi, bo nie.
Patch załatwiałby tutaj bardziej sprawę, ale zmiana patcha w zależności od tego gdzie chcę instalować zdecydowanie nie odpowiada moim wymaganiom.

Własny moduł drupalowy

Tak więc zrodziła się idea napisania własnego pełnoprawnego modułu, wykorzystującego możliwości systemu. Na nieszczęście dokumentacji jak na lekarstwo, nie odbyło się więc bez grzebania w kodzie domyślnych modułów, efekt natomiast spełnił moje oczekiwania. Czy spełni oczekiwania innych - okaże się.

Wgląd w kod

betterupdater.info

name = Better Updater
description = Handles directory for install new modules.
package = Administration
version = 1.0
core = 7.x

dependencies[] = update

files[] = betterupdater.updater.inc

Plik jest dość mocno standardowy, jedyne co może być mniej standardowe to linijka files[] = betterupdater.updater.inc odpowiadająca za autoładowanie klasy z pliku betterupdater.updater.inc

betterupdater.module

Pozwolę sobie skrócić nieco moduł, który do długich nie należy, ale można go obejrzeć dzięki linkowi na dole.

<?php
/**
 * Implements hook_updater_info_alter().
 */
function betterupdater_updater_info_alter(&$updaters) {
  $updaters['module']['class'] = 'BetterupdaterModuleUpdater';
}

// [...]

/**
 * Implements hook_form_FORM_ID_alter() for update_manager_install_form().
 * 
 */
function betterupdater_form_update_manager_install_form_alter(&$form, &$form_state, $form_id) {
  if (update_manager_local_transfers_allowed()) {
    $options_first = _betterupdater_get_directories();
    $selected = (
      isset($form_state['values']['first']) ?
      $form_state['values']['first'] : 'sites'
    );
    $form['dir'] = array(
      '#title' => t("Choose a directory"),
      '#prefix' => '
', '#suffix' => '
', '#type' => 'fieldset', ); $form['dir']['first'] = array( '#type' => 'select', '#size' => 1, '#options' => _betterupdater_get_directories($selected), '#title' => t("Choose where to install new module"), '#description' => t('If left empty or "Choose" option selected module will be installed in sites/all/modules directory'), '#default value' => $selected, '#ajax' => array( 'callback' => '_betterupdater_update_form', 'wrapper' => 'dirs', 'method' => 'replace', 'effect' => 'fade', ), ); $form['#validate'][] = 'betterupdater_form_update_validate'; } } // [...] ?>

Element, który domyślnie instaluje moduły jest klasą zdefiniowaną w dwóch modułach w corze - system i updater. Na szczęście dano użytkownikom dwa sympatyczne hooki, z czego jeden jest użyty powyżej. Są to hook_updater_info_alter(&$updaters) i hook_updater_info(). Oba pozwalają mieszać nieco przy uaktualnianiu i instalowaniu modułów i skórek.
hook_updater_info() odpadł z pewnej bardzo prostej przyczyny - Drupal i tak będzie próbował zainstalować moduł w swoim domyślnym katalogu, ponieważ wywoła wszystkie zgłoszone klasy.

Tak więc poniższy kod tylko i wyłącznie nadpisuje domyślną klasę instalującą moją własną klasą. Dokładnie to, o co mi chodziło.

<?php
function betterupdater_updater_info_alter(&$updaters) {
  $updaters['module']['class'] = 'BetterupdaterModuleUpdater';
}

Oczywiście aby użytkownik mógł wybrać odpowiedni moduł muszę dostarczyć mu odpowiedni interfejs. Jest to wykonywane za pomocą drugiego prezentowanego hooka. Dzięki temu formularzowi zapisuję wybraną przez użytkownika wartość w drupalowej zmiennej do późniejszego użycia.

betterupdater.updater.inc

Czyli klasa obsługująca instalowanie modułów.

class BetterupdaterModuleUpdater extends ModuleUpdater {

  public function getInstallDirectory() {
    if ($relative_path = drupal_get_path('module', $this->name)) {
      $relative_path = dirname($relative_path);
    }
    else {
      $relative_path = variable_get('betterupdater_module_path', 'sites/all/modules');
    }
    return drupal_realpath($relative_path);
  }

  public function postInstallTasks() {
    variable_del('betterupdater_module_path');
    system_rebuild_module_data();
    return array(
      l(t('Install another module'), 'admin/modules/install'),
      l(t('Enable newly added modules'), 'admin/modules'),
      l(t('Administration pages'), 'admin'),
    );
  }

}

Jak widać klasa dziedziczy ze standardowego ModuleUpdatera. Dzięki temu byłam zmuszona nadpisać tylko i wyłącznie dwie metody.

getInstallDirectory() jak sama nazwa wskazuje wybiera sobie odpowiedni katalog na podstawie wartości wpisanych przez użytkownika wcześniej. W postInstallTask() najważniejszą funkcją jest system_rebuild_module_data(), uaktualniająca listę modułów. Bez niej Drupal czasem nie zauważał, że nowy element został dodany.

A tymczasem na drupal.org...

Strona mojego projektu (jeszcze w wersji sandbox). Oczywiście można zobaczyć również kod poszczególnych plików.

Na sam koniec oznajmiam, że produkt jest już zgłoszony jako kandydat do pełnego projektu. Przeszedł wstępne poprawki i oczekuje na reakcję git adminów ;) Może to potrwać kilka tygodni, muszę więc uzbroić się w cierpliwość.

Za to moim marzeniem jest wprowadzenie podobnej funkcjonalności do core'a Drupala 8.

Dodaj komentarz