Naprogramujte si svůj první blok v Drupalu 8

Tvorba modulů pro Drupal 8 se výrazně liší od toho, nač jste byli zvyklí u sedmičkové řady Drupalu. Naštěstí není až tak složitá, aby to byl nepřekonatelný problém. V tomto článku se koukneme na vytvoření jednoduchého bloku, který bude zobrazovat nejnovější příspěvky z Facebooku a Twitteru v jednom výpisu.

Reklama

Drupal 8 využívá komponenty z PHP frameworku Symfony. Drupal 7 využíval množství hákovacích funkcí. Pokud chcete v Drupalu 8 vytvořit blok, musíte naprogramovat plugin. V Drupalu 7 jste použili hook_block_info() pro vytvoření definice bloku a následně jste definovali funkci generující tělo nového bloku.

To jsou zhruba základní rozdíly. V praxi v podstatě potřebujete vědět, že PHP kód generující blok a jeho obsah musíte umístit do správně pojmenovaného souboru, tak aby je automatické načítání tříd v PHP v Drupalu 8 bylo schopno detekovat.

Dalším rozdílem je definice zápisu základních informací o modulu. V Drupalu 7 jsme používali syntaxi podobnou zápisu souborů ini, v Drupalu 8 se pracuje se soubory ve formátu YAML.

Pro tento článek jsem zvolil ukázku v podobě jednoduchého modulu bez jakéhokoli nastavení, jehož úkolem bude načíst seznam příspěvků ze stránky na Facebooku a seznam příspěvků z účtu na Twitteru. Následně je seřadí dle data a času, vybere zvolený počet nejnovějších a výsledek zobrazí v těle bloku. Modul jsem nazval FbTwDrupal.

Základní informace o modulu – soubor *.info.yml

Tak jako dříve, i v Drupalu 8 musíte někde definovat základní informace o modulu, které pak Drupal 8 zobrazí v administračním přehledu modulů. Připravte si tedy složku /modules/custom/fbtwdrupal. V ní bude náš modul umístěn. Všimněte si, že moduly se v osmičce umisťují rovnou do složky modules, jádro je kompletně bokem ve složce core.

V připravené složce modulu vytvořte soubor fbtwdrupal.info.yml, který je tedy pojmenován stejně jako modul (složka modulu) a bude obsahovat základní informace o modulu.

Jeho obsah použijte následující:

name: FbTwDrupal
type: module
description: Displays latest posts from FB Page and Twitter account in the block
core: 8.x
dependencies:
  - node
package: Other

Nutné je definovat především jméno, typ, popisek a verzi jádra. Následuje výčet závislostí. Pozor, YAML je v podstatě strukturovaný textový dokument. Je důležité dodržovat odsazení mezerami, v opačném případě narazíte na problém. Poslední řádek, tak jako v Drupalu 7 obsahuje informaci o balíčku, sekci, do které modul patří a bude v administraci zobrazen.

Tělo bloku vytvoříte v Drupalu 8 jako plugin

Ve složce modulu si připravte také soubor fbtwdrupal.module, do kterého ale vložte jen začáteční <?php, obsah pro tento soubor žádný nepotřebujeme.

Definice bloku i jeho těla se totiž tvoří speciálním způsobem. V případě bloku půjde o vytvoření pluginu.

Ve složce modulu tedy vytvořte složku src, v ní pak složku Plugin (dbejte na velikost písmenek) a v ní složku Block.

Zde vytvořte soubor odpovídající velikostí písmenek názvu modulu a doplněný o slovo Block. V daném případě tedy FbTwDrupalBlock.php.

Zbytek je již poměrně jednoduchý a stačí tento soubor naplnit několika základními údaji. V první řadě definujte použitý jmenný prostor, ve kterém je opět použito jméno modulu dodržující všude v modulu stejnou velikost písmenek. Dále doplňte zápis use pro použití jmenného prostoru BlockBase.

Následuje komentář, který zde ovšem není jen pro formu, ale je naopak nezbytný. To, co jste dříve definovali pomocí hook_block_info(), to nyní zapíšete do tohoto komentáře.

Tento způsob anotací s definicemi nějakých prvků Drupal 8 převzal ze Symfony.

V anotaci Drupalu 8 sdělujete, že definujete blok s určitým identifikátorem a administračním popiskem. Zde je uveden včetně zápisu pro průchod přes lokalizační rozhraní administrace Drupalu.

Vytvořte si třídu jejíž název odpovídá názvu souboru, který editujete a zároveň rozšiřuje třídu BlockBase. V této třídě připravte veřejnou funkci build(), která pošle na výstup pole definující obsah bloku.

V té nejzákladnější podobě pošlete na výstup pole s klíčem #markup naplněným libovolným HTML. Základ modulu by tedy vypadal následovně:

<?php

namespace Drupal\FbTwDrupal\Plugin\Block;

use Drupal\Core\Block\BlockBase;

/**
 * @Block(
 *   id = "fbtwdrupal_block",
 *   admin_label = @Translation("FbTwDrupal Block")
 * )
 */

class FbTwDrupalBlock extends BlockBase {

  public function build() {
    $build = array(
      '#markup' => '<h2>Ahoj</h2><p>Jak se máte?</p>',
    );
    return $build;
  }
}

Zpracováváme data z Facebookové stránky

Kostru modulu poskytujícího blok máme hotovou, pokud byste modul aktivovali a zapnuli jeho blok, pak vám zobrazí nějaký základní HTML výstup. Zbývá tedy doplnit slíbenou funkcionalitu pro zobrazení seznamu příspěvků z Facebooku a Twitteru. Tato ukázka slouží pro zobrazení dat z firemní či jiné stránky na FB, tahání údajů z osobního profilu by bylo poněkud odlišné.

Pro začátek si musíte na stránce Facebook for Developers vytvořit novou „aplikaci“, která vám poskytne ID aplikace a k ní odpovídající tajný klíč. ID stránky získáte konverzí z její adresy například na webu Find your Facebook ID nebo podle tohoto návodu.

PHP kód v mé ukázce pak na základě ID stránky, ID aplikace a tajného klíče odešle požadavek na server Facebooku, získá výstup ve formátu JSON a jednotlivé příspěvky si uloží do pole s klíči o čase, obsahu, typu (facebook nebo twitter) a odkazem na zdroj.

Jak získat výpis příspěvků z Twitteru

Získání dat z Twitteru si vyžádá o několik řádků kódu navíc. V první řadě si opět svůj web zaregistrujte na stránce Twitter Application Management. Svoje Twitterové jméno bez zavináče na začátku znáte, dále budete potřebovat přístupový token a tajný klíč, které získáte na zmíněné stránce. Stejně tak consumer key a jeho tajný klíč.

PHP kód následně provádí přihlášení pomocí OAuth k serveru Twitteru. Pomocí curl následně přečte data o příspěvcích pro zvolené jméno, zpracuje výstup ve formátu JSON a jednotlivé záznamy si přiřadí do pole $rows, ve kterém jsou z předchozí části data o příspěvcích z Facebooku.

Sestavujeme výpis do bloku

Informace o příspěvcích tedy máme posbírané do pole $rows. Funkcí usort() je seřadíme podle klíče time, tak abychom měli nejnovější příspěvky za začátku pole. Poté funkcí array_slice() pole s příspěvky ořízneme na zvolený počet záznamů, v kódu ponechávám tři nejnovější příspěvky.

Smyčkou foreach() pak projdete zbylé záznamy a za použití preg_match a regulárních výrazů v těle příspěvků vyhledáte odkazy, hashtagy a zmínky jiných uživatelů Twitteru a převedete je na odkazy.

A to je vše. Máte připraveno pole se třemi nejnovějšími záznamy z obou sociálních sítí dohromady a jen si nějakým způsobem sestavíte HTML výstup. V daném případě s využitím přímého zápisu HTML, bez šablonování.

Výsledný kód doplněný o porovnávací funkce, zobrazení uplynulého času a oříznutí textu je zde:

<?php

namespace Drupal\FbTwDrupal\Plugin\Block;

use Drupal\Core\Block\BlockBase;

/**
 * @Block(
 *   id = "fbtwdrupal_block",
 *   admin_label = @Translation("FbTwDrupal Block")
 * )
 */

class FbTwDrupalBlock extends BlockBase {

  // Credit goes to http://stackoverflow.com/a/26098951
  private function trim_text($input, $length, $ellipses = true, $strip_tag = true,$strip_style = true) {
    //strip tags, if desired
    if ($strip_tag) {
        $input = strip_tags($input);
    }
    //strip tags, if desired
    if ($strip_style) {
        $input = preg_replace('/(<[^>]+) style=".*?"/i', '$1',$input);
    }
    if($length=='full')
    {
        $trimmed_text=$input;
    }
    else
    {
        //no need to trim, already shorter than trim length
        if (strlen($input) <= $length) {
        return $input;
        }
        //find last space within length
        $last_space = strrpos(mb_substr($input, 0, $length), ' ');
        $trimmed_text = substr($input, 0, $last_space);
        //add ellipses (...)
        if ($ellipses) {
        $trimmed_text .= '...';
        }
    }
    return $trimmed_text;
  }


  // Credit goes to https://codeforgeek.com/2014/10/time-ago-implementation-php/
  private function get_timeago( $ptime )
  {
    $estimate_time = time() - $ptime;
    if( $estimate_time < 1 )
    {
        return 'less than 1 second ago';
    }
    $condition = array(
      12 * 30 * 24 * 60 * 60  =>  'rokem',
      30 * 24 * 60 * 60       =>  ' měsíci',
      24 * 60 * 60            =>  'd',
      60 * 60                 =>  'h',
      60                      =>  'm',
      1                       =>  's'
    );
    foreach( $condition as $secs => $str )
    {
        $d = $estimate_time / $secs;
        if( $d >= 1 )
        {
            $r = round( $d );
            return 'před ' . $r . '' . $str;
        }
    }
  }

  private function cmp($a, $b) {
    return $b["time"] - $a["time"];
  }

  private function buildBaseString($baseURI, $method, $params) {
    $r = array();
    ksort($params);
    foreach($params as $key=>$value){
        $r[] = "$key=" . rawurlencode($value);
    }
    return $method."&" . rawurlencode($baseURI) . '&' . rawurlencode(implode('&', $r));
  }

  private function buildAuthorizationHeader($oauth) {
    $r = 'Authorization: OAuth ';
    $values = array();
    foreach($oauth as $key=>$value)
        $values[] = "$key=\"" . rawurlencode($value) . "\"";
    $r .= implode(', ', $values);
    return $r;
  }

  public function build() {
	  // Facebook
	  $page_id = '0000000';  // your page id
    $app_id = '000000';  // your app id
    $app_secret = 'xxxxxxxx';  // your app secret
    $url = 'https://graph.facebook.com/'.$page_id.'/posts?access_token='.$app_id.'|'.$app_secret;
		$string = file_get_contents($url);
		$posts = json_decode($string);
		$rows = array();
		foreach ($posts->data as $post){
		  if (!isset($post->story)){
		    $rows[] = array(
		      'time' => strtotime($post->created_time),
		      'content' => $this->trim_text($post->message,100),
		      'type' => 'facebook',
		      'link' => 'http://www.facebook.com/'.$page_id.'/posts/'.str_replace($page_id.'_','',$post->id)
		    );
		  }
		}
		// Twitter
	  $twitter_handle = "xxxxxxx";  // your Twitter account name without @
		$url = "https://api.twitter.com/1.1/statuses/user_timeline.json";
		$oauth_access_token = "111111-22222222";  // your oauth token
		$oauth_access_token_secret = "xxxxxxx";  // your oauth token secret
		$consumer_key = "xxxxxxx";  // your consumer key
		$consumer_secret = "xxxxxx";  // your consumer key

		$oauth = array(
			'oauth_consumer_key' => $consumer_key,
	    'oauth_nonce' => time(),
	    'oauth_signature_method' => 'HMAC-SHA1',
	    'oauth_token' => $oauth_access_token,
	    'oauth_timestamp' => time(),
	    'oauth_version' => '1.0',
	    'screen_name' => $twitter_handle
		);
		$base_info = $this->buildBaseString($url, 'GET', $oauth);
		$composite_key = rawurlencode($consumer_secret) . '&' . rawurlencode($oauth_access_token_secret);
		$oauth_signature = base64_encode(hash_hmac('sha1', $base_info, $composite_key, true));
		$oauth['oauth_signature'] = $oauth_signature;
		$header = array($this->buildAuthorizationHeader($oauth), 'Content-Type: application/json', 'Expect:');

		$options = array(
			CURLOPT_HTTPHEADER => $header,
	    CURLOPT_HEADER => false,
	    CURLOPT_URL => $url . '?screen_name=' . $twitter_handle,
	    CURLOPT_RETURNTRANSFER => true,
	    CURLOPT_SSL_VERIFYPEER => false
		);
		$feed = curl_init();
		curl_setopt_array($feed, $options);
		$json = curl_exec($feed);
		curl_close($feed);
		$posts = json_decode($json, true);

		foreach ($posts as $post){
		  $rows[] = array(
		    'time' => strtotime($post['created_at']),
		    'content' => $post['text'],
		    'type' => 'twitter',
		    'link' => ''
	      );
		}

		// Let's build the output
		$build = '';
		// Sort by datetime
	  usort($rows, array($this, "cmp"));
	  // Display only three rows
		$rows = array_slice($rows,0,3);

		$reg_exUrl = "/(http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?/";
		$reg_exHash = "/#([a-z_0-9]+)/i";
		$reg_exUser = "/@([a-z_0-9]+)/i";
		// make URLs as mentioned on http://www.johnbhartley.com/2013/twitter-feed-with-php-and-json-v1-1/
		foreach ($rows as $key => $row){
  	  if(preg_match($reg_exUrl, $row['content'], $url)) {
       // make the urls hyper links
       $row['content'] = preg_replace($reg_exUrl, "<a href='{$url[0]}'>{$url[0]}</a> ", $row['content']);
      }
      if(preg_match($reg_exHash, $row['content'], $hash)) {
        // make the hash tags hyper links
        $row['content'] = preg_replace($reg_exHash, "<a href='https://twitter.com/search?q={$hash[0]}'>{$hash[0]}</a> ", $row['content']);
        // swap out the # in the URL to make %23
        $row['content'] = str_replace("/search?q=#", "/search?q=%23", $row['content'] );
      }
      if(preg_match($reg_exUser, $row['content'], $user)) {
        $row['content'] = preg_replace("/@([a-z_0-9]+)/i", "<a href='http://twitter.com/$1'>$0</a>", $row['content']);
      }
      $rows[$key]['content'] = $row['content'];
		}

		foreach ($rows as $row){
		  $build .= '<div class="row type-'.$row['type'].'">';
		  $build .= '<div class="icon type-'.$row['type'].'"><span>'.$row['type'].'</span></div>';
		  $build .= '<span class="ago">'.$this->get_timeago($row['time']).'</span>';
		  $build .= '<div class="message">'.$row['content'];
		  if (!empty($row['link'])){
	  	    $build .= ' <a href="'.$row['link'].'" target="_blank">&raquo; &raquo; &raquo;</a>';
		  }
		  $build .= '</div>';
		  $build .= '</div>';
		}

    $build = array(
      '#markup' => $build
    );
    return $build;
  }
}

Kompletní kód modulu najdete na GitHubu. Postupně jej doplním o formulář pro nastavení a třeba i o využití šablony v Twigu. K úpravám postupně vzniknou další články.

Výsledek může po doplnění CSS vypadat třeba takto:

FbTwDrupal

Tip: V mojí nové knize o Drupalu 8 najdete nejen návod pro sestavení bloku, ale také návod pro práci s Twigem, generování stránky a řadu dalších věcí. Zaměřena je ale především na práci s administrací Drupalu, stavbu webu bez programování a tipy na zajímavé moduly.

Reklama

Komentáře

Nebolo by lepsie namiesto file_get_content a manualneho CURLu pouzit kniznicu Guzzle, ktoru ma D8 uz v jadre? Pozor aj na coding standards, nazvy metod v ramci triedy by mali byt pisane camelCase.

Možná. Díky za tip, v další iteraci na Guzzle mrknu.

Tvořím weby. Nabízím poradenství pro Drupal. Jsem na Twitteru.

Přidat komentář