V třetím pokračování seriálu o vývoji modulů se nejprve seznámíme s databází, která se vytvoří během instalace Drupalu a následně začneme s vývojem vlastního modulu: hlasování pro článek.

Letmý náhled do databáze

Drupal ke svým účelům pracuje s několika databázovými tabulkami. Tabulky, které existují v každé instalaci Drupalu se vztahují k core-required modulům. Další tabulky se vytvoří aktivací dalších modulů, a to jak základních, které jsou součástí instalace Drupalu (ale jsou po nainstalování neaktivní), tak modulů rozšiřujících (třeba jednoho z našich).

Tip: Referenci k tabulkám core modulů Drupalu naleznete na adrese http://drupal.org/node/22754. Při studiu je třeba opět dávat pozor, zda se díváme do reference ke správné verzi Drupalu.

Mezi nejzajímavější tabulky patří:

  • node – základní informace o každém nodu v systému
  • node_revision – informace o revizích každého nodu
  • node_counter – statistiky k jednotlivým nodům
  • comments – komentáře k jednotlivým nodům
  • files – informace k uploadovaným souborům
  • user_roles – vazba mezi uživatelem a typem uživatelského účtu
  • user – informace o uživatelích
  • role – informace o rolích (typech uživatelského účtu)
  • cache – některé stránky jsou cachovány pro zvýšení výkonu sítě
  • sessions – sessions
  • blocks – nastavení a obsah bloků

S těmito (a dalšími neuvedenými) tabulkami mohou pracovat vaše moduly: mohou z nich získávat data, editovat obsah a měnit jejich strukturu (například přidáním nového sloupce), což však nebývá obvyklé.

Váš modul může přirozeně vytvářet tabulky nové. Ty jsou Drupalem automaticky vytvořeny během instalace (první aktivace) příslušného modulu administrátorem. Popis tabulek se musí nacházet v hooku hook_schema, která musí být definována v souboru nazevmodulu.install a zapisuje se pomocí speciálního API. Reference k tomuto API se nachází zde a my se s ní seznámíme v souvislosti s našim prvním modulem.

Mohlo by být zajímavé nahlédnout, jak jsou vytvořeny tabulky core modulů během instalace Drupalu. Kód, který hledáme, se nachází v souboru system.install ve funkci system_install:

// Create tables.
$modules = array('system', 'filter', 'block', 'user', 'node', 'comment', 'taxonomy');
foreach ($modules as $module) {
    drupal_install_schema($module);
}

Funkce drupal_install_schema, zajišťuje instalaci každého z modulů a nachází se v souboru includes/common.inc.

Náš první modul – hlasování pro článek

Na místo pokračování v popisu vnitřností Drupalu se pustíme rovnou do psaní jednoduchého modulu, přičemž další nezbytné znalosti o systému získáme „zapochodu“. Modul, který hodláme vytvořit, patří v programování pro CMS mezi kategorii „hello world“ programů. I když (narozdíl od „hello world“ programu) je – dokonce bez dalšího upravování – skutečně použitelný. Co tedy naprogramujeme? Naprogramujeme modul pro hodnocení článků čtenáři. Od ostatních podobných jej trochu modifikujeme v tom smyslu, že návštěvník může udělit/neudělit danému článku pouze jeden hlas (bod).

Modul nazveme „Hlasování“ a jako jednoslovní název, který bude prefixem názvu všech funkcí, volíme „hlasovani“. Náš modul se bude skládat z následujících souborů:

  • hlasovani.info – soubor bude obsahovat základní informace, které se zobrazují v administrativní části systému a další informace
  • hlasovani.install – soubor je aktivní během instalace a odinstalace modulu
  • hlasovani.modul – samotný PHP kód modulu
  • hlasovani.css – definice stylů

Takto bude vypadat náš hlasovací modul pro Drupal.

Jako první krok vytvoříme ve složce sites/all/modules nový adresář hlasovani, ve které se budou nacházet všechny (výše zmíněné) soubory našeho modulu. Dále vytvoříme všechny čtyři soubory a necháme je prozatím prázdné. Postupně je budeme vyplňovat kódem.

hlasovani.info – informace o modulu

Soubory .info jsou formátu .ini, což je jednoduchý standard pro PHP konfigurační soubory. Více informací o .ini souborech naleznete například v referenci k PHP.

V našem případě vyplníme ze všech možných položek o nichž hovoří reference Drupalu pouze několik. Každá položka má název (identifikátor) a hodnotu a píše se na samostatný řádek. Název položky a její hodnota jsou odděleny symbolem „=“. Záleží na dobré vůli programátora, které a kolik položek vyplní (a zda vůbec nějaké). My vyplníme položky name, description, package a code.

Položka name nese informaci o názvu modulu:

name = Hlasování

Do položky description, v níž je vyplněn popis modulu, napíšeme:

description = Modul umožňuje čtenářům hlasovat váhou jednoho hlasu pro publikované nody.

V další položce vyplníme informaci o tom, pro jakou verzi Drupalu je náš modul určen:

core = 6.x

V poslední položce vyplníme informaci o tom, do jakého balíčku (položka package) náš modul náleží. Můžeme si vymyslet vlastní název balíčku (například „Pokusné moduly“). Moduly se stejným názvem balíčku jsou v administrační části od sebe graficky odděleny:

package = Pokusné moduly

Na začátek souboru ještě přidáme řádek s prázdným CVS identifikačním tagem (ten budeme přidávat na začátek všech souborů):

; $Id$

Jsme hotovi. Celý soubor hlasovani.info by měl vypadat takto:

; $Id$
name = Hlasování
description = Modul umožňuje čtenářům hlasovat váhou jednoho hlasu pro publikované nody.
core = 6.x
package = Pokusné moduly

Pokud se nyní podíváte do administrátorské sekce Drupalu, do části, kde se aktivují moduly, měli byste vidět váš modul v samostatné záložce „Pokusné moduly“ s popisem, který odpovídá tomu, co jsme vyplnili v souboru hlasovani.info. V administraci se zdá, že by bylo možné modul aktivovat. Můžete to zkusit. Drupal sice vypíše hlášku „The configuration options have been saved.“, ale ve skutečnosti se nic nestane. Přirozeně, zatím jsem žádný modul nenapsali.

V administraci můžeme aktivovat náš modul.

Návrh databáze a soubor hlasovani.install

Než začneme psát kód, měl bych se zmínit o tom, že existují tzv. Coding standards, které jsou součástí oficiální reference k Drupalu. Budeme se snažit těchto doporučení držet, neboť jsou komunitou Drupalu v podstatě vyžadovány (je považováno za neslušné se od nich příliš odchylovat).

Komunita Drupalu je poměrně háklivá na styl zápisu komentářů. Krátce se tedy o tom zmíníme:

Krátké komentáře k funkcím píšeme:

/**
 * Funkce konverguje pole na objekt.
 */
function array2object($array) {

Hook dokumentujeme jednoduše jako:

/**
 * Implementace hook_help().
 */
function blog_help($section) {
  // ...
}

Drupal podporuje styl dokumentace Doxygenu, více informací o tom naleznete opět v referenci. U složitějších funkcí tedy můžeme použít tagy Doxygenu:

/**
 * Verifikuje syntax emailove adresy.
 *
 * Prazdna e-mailova adresa je povolena, viz RFC 2822.
 *
 * @param $mail
 *   e-mailova adresa - string
 * @return
 *   TRUE, pokud je adresa ve spravnem formatu
 */
function valid_email_address($mail) {

Přistupíme k implemetaci souboru hlasovani.install. Obecně je soubor .install aktivní během instalace a odinstalace modulu a je v něm definováno schéma databáze. Reference Drupalu říká, že soubor .install má implementovat tři hooky:

  • hook_schema – zde je definováno databázové schéma
  • hook_install – hook instaluje aktuální databázové schéma voláním funkce drupal_install_schema, může vykonat i další operace, které souvisejí s instalací modulu.
  • hook_uninstall – hook je volán při odinstalaci (nikoli deaktivaci) modulu. Může například odstranit databázové tabulky voláním funkce drupal_unistall_schema (je otázkou, zda je rozumné při odinstalaci modulu vždy po sobě uklidit tímto způsobem), smazat nepotřebné soubory, odstranit globální proměnné apod.

Je nutné striktně rozlišovat mezi instalací/aktivací a deaktivací/odinstalací. Instalace je první aktivace modulu (po tom co buď vůbec nebyl ještě aktivován, nebo byl předtím odinstalován). Aktivace je opětovné přivedení modulu k životu (po tom, co byl deaktivován). Během instalace je volána hook hook_install, jehož prací je většinou modifikace databáze. Hook není volán během aktivace modulu (a nedochází tak k pokusu o vytvoření již existujících tabulek, přepisování souborů apod.).

Pokud je v administrativní části modul deaktivován, není ještě odinstalován (i když je deaktivace nutnou podmínkou k odinstalaci, viz administrace). Teprve při odinstalaci je volán hook hook_uninstall. Ten není volán při deaktivaci modulu a nemůže tak dojít pouhou deaktivací modulu k vymazání obsahu databáze.

Do souboru hlasovani.install napíšeme:

function hlasovani_schema() {
  $schema['hlasovani'] = array(
     // specifikace databázové tabulky
  );
  return $schema;
}

function hlasovani_install() {
  // edituj databázi
  drupal_install_schema('hlasovani');
}

function hlasovani_uninstall() {
  // ukliď po sobě v databázi
  drupal_uninstall_schema('hlasovani');
}

Funkce hlasovani_install a hlasovani_uninstall jsou již hotové. K jejich implementaci nám stačil vždy jeden příklad. Nevyplněna zůstala pouze funkce hlasovani_schema, kde budeme definovat novou tabulku hlasovani, která se během instalace vytvoří a která bude uchovávat informaci o hlasování jednotlivých uživatelů.

Tabulku bude obsahovat vazbu na tabulku node a to sloupcem nid (nid jednoznačně identifikují nod, viz tabulka node ve vaší instalaci Drupalu) a dále vazbu na uživatele sloupcem uid (viz tabulka user ve vaší instalaci Drupalu). Skutečnost, že uživatel hlasoval pro jistý článek do databáze, zaneseme přidáním nového řádku s příslušnými uid a nid.

Vše bude jasnější z tohoto ER diagramu:

Propojení databázových tabulek. Tabulky Node a User jsou pouze naznačeny (výčet sloupců není úplný).

Vytvoření tabulky by odpovídal SQL příkaz:

CREATE TABLE hlasovani (
	uid int NOT NULL default '0',
	nid int NOT NULL default '0',
	PRIMARY KEY (uid, nid));

Stačí nám tato tabulka? Určitě ano. Každý řádek bude uchovávat informaci o tom, že uživatel s jistým id (uid) hlasoval pro jistý node (s nid). Počet hlasů pro jistý node zjistíme jednoduše:

SELECT COUNT(*) FROM hlasovani WHERE nid = xx;

Primární klíč se skládá z kombinace uid a nid. To je vpořádku, protože daný uživatel smí pro daný článek hlasovat maximálně jednou. Databázi bychom mohli navrhnout i jinak, například tak, abychom vždy při generování stránky s nodem nemuseli přepočítávat počet hlasů, ale zde pro jednoduchost ponecháme návrh takto.

Od Drupalu verze 6 se při vytváření tabulek nepoužívá přímo SQL kód, ale vytváří se pole (PHP datový typ array) podle tzv. Schema API. Výhodou tohoto přístupu je, že již není třeba psát SQL příkaz zvlášť pro každou databázi a o vytvoření správného příkazu se postará sám Drupal na základě databáze, kterou právě používá. Drupal například podporuje databáze (mimo obligátních MySQL a PostreSQL) SQLite, Microsoft SQL, Oracle apod.

Samotné Schema API je poměrně jednoduché. Vytváří se pole polí. Na první úrovni jsou jednotlivé tabulky, jako klíč je uveden název tabulky (v našem případě "hlasovani") a obsahem je pole, které již specifikuje danou tabulku. To může mít několik položek (v následujícím výčtu jsou uvedeny všechny, o nichž se zmiňuje oficiální reference):

  • description - popis tabulky
  • fields - jednotlivé sloupce tabulky, každý sloupec je opět definován jako nové pole
  • primary key - pole primárních klíčů
  • unique key - pole unikátních klíčů
  • indexes - pole indexů

Pole fields, definují sloupce. Každý sloupec je opět definován jako vnořené pole, s položkami, které definují daný sloupec tabulky. Klíčem je název sloupce. V následujícím výčtu uvádíme všechny položky o nichž se zmiňuje oficiálná reference Drupalu 6.x):

  • description - popis sloupce
  • type - určuje datový typ, přehled podporovaných datových typů naleznete zde
  • size - určuje velikost datového typu, může nabývat hodnota: "tiny", "small", "medium", "normal", "big"
  • not null - určuje, zda může být sloupec nevyplněn
  • default - jaká má být defaultní hodnota sloupce
  • length - maximální velikost typů "varchar", "text" a "int", pro ostatní typy je ignorováno
  • unsigned - boolean hodnota určuje, zda má být datový typ "int", "float" a "numerics" se znaménkem nebo bez
  • precision - pro datový typ "numerics" určuje maximální počet číslic
  • serialize - boolean hodnota nastavující, zda bude datový typ uložen serializován jako string

Spíše než ze slovního popisu a výčtů, bude vše jasné z následujícího kódu. V něm jsme v Scheme API zapsali tabulku hlasovani (viz. předchozí SQL kód):

$schema['hlasovani'] = array(
	'description' => 'The base table for nodes.',
	'fields' => array(
		'nid' => array(
			'description' => 'The primary identifier 
			                   for a node.',
			'type' => 'int',
			'unsigned' => TRUE,
			'not null' => TRUE,
		),
		'uid' => array(
			'description' => 'aentifier for a node.',
			'type' => 'int',
			'unsigned' => TRUE,
			'not null' => TRUE
		)
	),
	'primary key' => array('nid', 'uid')
);

Soubor hlasovani.install by měl po této úpravě (a dopsaní komentářů) obsahovat kód:

<?php
/**
 * Implementace hook_schema().
 * @return array se tabulkou "hlasovani" definovanou pomoci Scheme API
 */
function hlasovani_schema() {
	$schema['hlasovani'] = array(
		'description' => 'The base table for nodes.',
		'fields' => array(
			'nid' => array(
				'description' => 'The primary identifier for a node.',
				'type' => 'int',
				'unsigned' => TRUE,
				'not null' => TRUE,
			),
			'uid' => array(
				'description' => 'aentifier for a node.',
				'type' => 'int',
				'unsigned' => TRUE,
				'not null' => TRUE
			)
		),
		'primary key' => array('nid', 'uid')
	);
	return $schema;
}

/**
 * Implementace hook_install().
 */
function hlasovani_install() {
  // edituj databázi
  drupal_install_schema('hlasovani');
}

/**
 * Implementace hook_uninstall().
 */
function hlasovani_uninstall() {
  // ukliď po sobě v databázi
  drupal_uninstall_schema('hlasovani');
}
?>

Tím jsme svou práci na souboru hlasovani.install dokončili. Pokud nyní aktivujete modul, měla by se vytvořit prázdná tabulka podle výše uvedeného schématu.

Zde bych si dovolil velice důležitou poznámku, byť jsme se o tom již zmiňovali. Hook hook_install je volán pouze při první aktivaci modulu! Není znovu volán, pokud je modul opětovně aktivován po tom, co byl předtím deaktivován. Je však znovu volán, pokud byl předtím modul odinstalován a nyní je znovu instalován (tj. prvně aktivován).

Deaktivace a odinstalace modulu jsou dvě odlišné věci. Teprve po tom, co je modul deaktivován, může být odinstalován a to ve speciální části (uninstall) administrace modulů. Hook hook_uninstall je volán nikoli při deaktivaci modulu, ale až při odinstalaci modulu.

Databázová abstraktní vrstva Drupalu

Před implementací souboru, ve kterém bude kód našeho modulu, se zmíníme o databázové abstraktní vrstvě Drupalu. Smyslem této vrstvy je (pokud možno) abstrahovat konkrétní SQL server a SQL dotazy použité ve vašem modulu. Referenci naleznete zde.

Nás budou zajímat především následující funkce:

Funkce db_query slouží k vykonání SQL příkazu (SELECT, INSERT, UPDATE a samozřejmě dalších). Vrací buď hodnotu FALSE nebo výsledek SQL dotazu. Funkce db_result, db_fetch_object, db_fetch_array použijeme k přečtení dat, které vrací jako návratovou hodnotu příkaz db_query. Příkaz db_result použijeme, pokud očekáváme jednu konkrétní hodnotu (například počet článků v databázi). Příkaz db_fetc_object přečte při každém volání jeden řádek navrácené tabulky jako objekt. Příkaz db_fetc_object přečte při každém volání jeden řádek navrácené tabulky jako pole (array).

K psaní SQL dotazů je třeba se ještě zmínit o dvou věcech. Název tabulky je z bezpečnostních důvodů (tzv. SQL injection attacks) zabalen mezi symboly {}. A při zápisu hodnot do SQL dotazu je možné (a doporučené) použit syntaxi, která je známa u příkazu print v Cčku (viz reference k funkci db_query):

db_query('INSERT INTO {hlasovani} (nid, uid) 
          VALUES (%d, %d)', $nid, $uid);

Uvedeme si několik příkladů. Vyčtení jedné hodnoty z databáze:

return (int) db_result(db_query('SELECT COUNT(*) FROM {hlasovani} 
	          WHERE nid = %d', $nid));

Ilustrační příklad na vyčítání více řádků z tabulky:

$sql = "SELECT nid FROM {node} ORDER BY nid";
$query = db_query($sql);

while($data = db_fetch_object($query)) {
    $output .= ' '.strval($data->nid);
}

Tyto znalosti nám při implementaci modulu budou stačit.

Závěr

Nemalou část modulu jsme již implementovali. Ze čtyř souborů: hlasovani.info, hlasovani.install, hlasovani.modul, hlasovani.css jsme první dva již implementovali. V posledním díle seriálu implementujeme zbývající dva. Se souborem hlasovani.css nás žádné problémy nečekají, ale v souboru hlasovani.modul bude obsažena prakticky veškerá aplikační logika.

12 Příspěvků v diskuzi

  1. tu NanaNA:

    v pristim dile cely modul dokoncime do plne funkcni podoby. Tim skonci prvni (4 dilny) miniserial.

    V nasledujicich miniserialech (zase po 3-4 dilech) budeme vylepsovat stavajici modul… a pak treba napiseme zase dalsi :-).

    Honza

  2. Dobry den,

    k clanku bych mel dve vyhrady:

    1) Meli bychom lidi ucit Drupal coding standards. Neindentace kodu v prikladech je alarmujici.

    2) Myslim, ze zasadou v Drupalu by melo byt psani kodu v anglictine. I v CR. Komentovat priklady je mozne v cestine, ale kod by mel byt anglicky. Opet – coding standards.

  3. Dobry den, tim „neindentace“ myslite chybejici odsazeni? To je spise moje chyba, jakozto vkladatele clanku do redakcniho systemu, nez autora. Vsechny zdrojaky tohoto serialu budou nalezite odsazeny :-)

  4. 1) Jde o „elemenet“ Id, ktery by se mel vyskytovat v kazdem souboru. Priznam se, ze jsem to vynechal.

    2) psani kodu v cestine podle meho nazoru proti coding standards neni

  5. Zdravím, kdy se chystá poslední díl seriálu? :) Velmi akutně řeším výrobu jednoho modulku a nemůžu se dočkat posledního dílu seriálu, abych se doučil zbylou práci :)
    Díky moc!

  6. Myslim, ze v casti „Databázová abstraktní vrstva Drupalu“ a druhom odstavci mate chybu v poslednej vete „Příkaz db_fetc_object přečte při každém volání jeden řádek navrácené tabulky jako pole (array).“, by malo namiesto „db_fetc_object“ byť „db_fetch_array“.

Odpovědět