中文自動完成在 DRUPAL 8 中得到改善

不知大家有沒有察覺,在新的 DRUPAL 8.2.4 版本以後中,在標籤 (TAG) 輸入中文,不再返回奇怪的結果。 不必要的 HTTP REQUEST 也減少了。

DRUPAL 7 / DRUPAL 8.2.4 以前:
返回一堆不相幹的結果

(感謝 @jungle 提供 GIF)

D8.2.4 以後:

(再次感謝 @jungle 提供 GIF)

這受益於瀏覽器的 CompositionEvent API,監察 IME 輸入與輸出。
(可以參考 VUE 開發者的文章:
http://blog.evanyou.me/2014/01/03/composition-event/)

再沒有理由不用上新版 DRUPAL 8。至於 DRUPAL 7,要 BACKPORT PATCH 很容易,新年有空的朋友,可以展示一下你的功力: https://www.drupal.org/node/2823589

(特別感謝 @jungle 的 REVIEW,JS PATCH 在 DRUPAL CORE 要一天內完成及順利進到 CORE,不常出現)

Drupal: 增加子項目 type 支援到 theme_item_list()

D8 Issue (COMMITED):
http://drupal.org/node/1785310

 

D7 可自已增加到 theme 中:
http://api.drupal.org/api/drupal/includes!theme.inc/function/theme_item_list/7


// .. 省略
      if (count($children) > 0) {
        // Render nested list.
        $data .= theme_item_list(array('items' => $children, 'title' => NULL, 'type' => $type, 'attributes' => $attributes));
      }
// .. 省略

更改為


// .. 省略
      if (count($children) > 0) {
        // Render nested list.
        $data .= theme_item_list(array('items' => $children, 'title' => NULL, 'type' => (isset($item['type']) ? $item['type'] : $type), 'attributes' => $attributes));
      }
// .. 省略

如果不會使用 theme_item_list(),可看看這裏 EXAMPLE

修改 Drupal Views SQL 沒難度![hook_views_query_alter]

Drupal Views 讓你可透過界面設定所需條件,輕鬆抓取特定資料。不過開發者創意無限,Views 架構沒可能完全滿足您的要求。在此時,可以考慮使用 hook_views_query_alter() 修改/增強 Views 預設不能滿足的條件。

引用 Drupal Taiwan 論壇上一問題作例子:

做出一個列表,當使用者登入後,可以看到同一組別的成員

對於 Views,這類列表本是很簡單,但要實現時卻發現問題:Views 只能過濾登入使用者的 ID (User: Current)。要以登入使用者的角色或其他因素似乎有點難度。

在主題中,我提出了使用 Contextual 的方案,問題的確能解決,但你的 URL 將變成:my-group-member/{使用者的組別}。也不太完美。(另外,也提及了 Views PHP,估計也能達成效果,但筆者想更深入地看看萬能的方法)

不能確認原發問者需求,為此教程,筆者定義組員列表規格

  • 列表中只能看到同一組別的成員 (此教學中,我們將要列出 K Team 成員)
  • URL:example.com/my-group-member
  • 透過 Profile 增加一個 Team (field_team) 選擇欄位,包括選項:K Team | Drupal Team

 

修改 Views Query

在此略過寫模組的基本需要。我們來建立一個 View,然後如下圖設置。(為了說的更清晰,筆者去除了多餘的設定)
我們增加了 User: Team (= Drupal Team)。這並不是我們想要的,但沒關係,因為即將下來透過程式碼進行修改。

 

此時,如果你有打開 SQL 預覽,可以對比增加 Filter Criteria 前後的 SQL 變化。沒錯,會寫程式的你應看懂 Views 是在做什麼..
([admin/structure/views/settings] -> Live preview settings ->  Show the SQL query)


Views 將一個資料庫表關連了,並設定了 WHERE 過濾條件。所以,我們最終要改變的將是 WHERE 條件中的 IN('Drupal Team') 為當前使用者的 Team

hook_views_query_alter() 真正登場了

*** 下面程式碼包含了詳細註釋,由上至下細看***
(dpm 是 Drupal Devel 模組的 Debug Function)


/**
 * hook_views_query_alter
 * 模組名_views_query_alter
 */
function alterquery_views_query_alter(&$view, &$query) {
  // 看看有什麼東西
  dpm($view, 'Views 的所有資訊');
  dpm($query, 'Views SQL 查詢');

  if (
    // Views 的 Machine Name
    $view->name == 'my_group_member' 
    // Views 頁面 Machine Name (收藏在設定頁面的 Advanced 裏)
    && $view->current_display == 'page' 
  )
  {
    if(!isset($user)) { // $user 是保存當前使用者資訊的變數。在這裏我們檢查有否載入
      $user = user_load($GLOBALS['user']->uid);
    }
    dpm($user, '當前使用者資訊');

    /**
     * $user_team = $user->field_team['und'][0]['value'];
     * 上面這行較直接,但之前談過,由於 Drupal 是支援多語言,這不是最好的方法。
     * 看:https://notabluescreen.com/field_get_items
     */
    $team = field_get_items('user', $user, 'field_team');
    $user_team = $team[0]['value'];
    dpm($team, '使用者組別資訊');


    /**
     * [K-1]我們需要修改 field_data_field_team.field_team_value 這一項
     */
    foreach ($query->where['1']['conditions'] as $key => $value) {
      if ($query->where['1']['conditions'][$key]['field'] == 'field_data_field_team.field_team_value') {
         $query->where['1']['conditions'][$key]['value'][0] = $user_team;
         break;
      }
    }

    // 看看有沒有正確修改
    dpm($query, '修改後的Views SQL 查詢');
  }
}

清除 Drupal Caches,重新整理網頁,應有其效果了!

( 到此為止,要是看不懂,猜想還沒達到 PHP 基本水平。好好再學學 PHP 。 )

 

上面 [K-1] 處的 我們使用了 FOREACH 語句。只要你掌握到所需的資料表,你還可以取消 Filter Criteria: User: Team (= Drupal Team),及將該處程式碼替代為:


// 增加上面關聯的資料表
    $query->add_table('field_data_field_team');
    
    // 增加 WHERE 條件 (不難看出吧!跟上面 [K-1] 的程式碼是對應的)
    $query->add_where(0, 'field_data_field_team.field_team_value', array($user_team), 'in');

 

除了註釋,實際只有十行左右。附上測試模組。

增減 Drupal HTML HEAD 的資訊 (Meta Tags & LINKs)

Drupal 頁面的 HTML <head> 有一堆 META TAGs 及 LINKs。這都是經 html.tpl.php 的 $head 輸出 ( $head = drupal_get_html_head() )。您可經由 hook_html_head_alter 修改其內容,比如要刪除 Drupal 的版權及版本宣告:


// 以 Bartik Theme 為例
function bartik_html_head_alter(&$head_elements){
  // dpm($head_elements); // 看看內裏有什麼怪東西

  // 找到了 'system_meta_generator' 這個不想要的傢伙
  unset($head_elements['system_meta_generator']);

  // 想增加 X-UA-Compatible,令 IE 永遠用最新的 Rendering Engine 或 Chrome Frame
  $head_elements['x_ua_Compatible'] = array(
    '#type' => 'html_tag', // 這不是必要的
    '#tag' => 'meta',
    '#attributes' => array(
      'http-equiv' => "X-UA-Compatible",
      'content' => "IE=edge,chrome=1",
    ),
  );
}

**
如果沒有效果:

一、你見鬼了;
二、忘記在 template.php 增減 FUNCTIONS 後重建 CACHES

特殊情況: 要是真的見鬼了,你可以手動修改 html.tpl.php,也不要在此留言帶給我衰氣 🙂
**

此外,也可以在 theme_preprocess_html() 或模組中利用 drupal_add_html_headdrupal_add_html_head_link 增加你想要的東西。

Drupal Fields: field_attach_update() 更新欄位資料

之前透過 node_save() 的方式來儲存新的 NODE 內容,在 Drupal 7 中同樣可以使用,不過如果要更新,方法稍有不同:


$node2 = node_load($nid); // 如果更新 TITLE,需要提供 Node 的 vid
$node2->title = 'new title';
$node2= node_submit($node2);
node_save($node2);

如果不是更新標題,可以用以下方法,但會有很多 PHP NOTICE 錯誤:


$node3 = new stdClass();
$node3->nid = $nid;
$node3->type = "article";
$node3->body[LANGUAGE_NONE][0]['value'] = 'new value';
node_object_prepare($node3); 
$node3 = node_submit($node3);
node_save($node3);

這個排除了 PHP 錯誤提示:


$node4 = node_load($nid);
$node4->body[LANGUAGE_NONE][0]['value'] = 'new value';
node_object_prepare($node4); 
$node4 = node_submit($node4); 
node_save($node4);

不過在 Drupal 7 中,幾乎都是欄位 (Fields),我們可以考慮使用 Field Attach API:


$node5 = new stdClass();
$node5->nid = $nid;          
$node5->type = 'article';
// $node5->status = 1;
$node5->body[LANGUAGE_NONE][0]['value'] = 'new value';
field_attach_presave('node', $node5);
field_attach_update('node', $node5);

兩者分別:

  • node_save() 除了你要更新的欄位外,還有一堆其他更新,例如:NODE 的時間
  • 效能、效能:以執行 1000 次算,Field Attach API 約快兩倍以上

field_get_items() 取得欄位的內容資料

進入 Drupal 7,都被 Fields 包圍著,要想取得一個半個欄位的資料要怎麼做呢?

大家最直接的想法會不會是看看 $node object 中有什麼呢?


debug($node,'',1);

但留意,若果兩者在不同的語言下發佈,是會有差異的:

[und] = "Undetermined" = "LANGUAGE_NONE" (不確定的意思)

 

為了解決上述問題或在複雜的欄位中取得資料,可以用 Fields API 中的 field_get_items()


$items = field_get_items('node', $node, 'field_image');
debug($items,'',1);

是否簡潔很多!

 

大家可能還對 Drupal 中的 Entity / Fields 概念很陌生 (ME TOO!!),簡單說一說:


$items = field_get_items('node', $node, 'field_image');

"node" 是 Entity Type,D7 中的 NODE,COMMENT,USER 都是 Entity Type

"$node" 怎麼來的呢? node.tpl.php 版型中當然不用理會,在其他地方可以使用 node_load, user_load …取得

"field_image" 是欄位的 Machine Name

** 值得注意這一 Function 還有一個 $langcode = NULL 的參數,可以讓你取得指定語言的內容

好的,給另一例子,假若我想取得某個回應的內容:


$obj = comment_load(1);
$items = field_get_items('comment', $obj, 'comment_body');
debug($items,'',1);

Drupal API: drupal_render_cache_by_query [基於 DB Query,緩存你的查詢結果]

在網站中,經常有一些資料是相同的,並不必要每一次刷新都重建,若果這在一個大的 DB 資料庫,或一個複雜的 SQL 查詢中,很消耗資源。將查詢結果進行緩存是一個極好的解決方法。

Drupal 7 新增了 API drupal_render_cache_by_query() 來嘗試解決以上問題,主要用作緩存相同的 DB Query 的結果,為 DB Server 減輕查詢負擔。也就是說,當 DB Query 有改變,這會重新產生,否則這會直接由 DB 取得預早的緩存結果。

在 Drupal Core 中也只有在 Forum Block 中使用過一次。由於太新了,這唯一的一次實例也出錯了,以下是修正的版本示範:


function forum_block_view($delta = '') {
  $query = db_select('forum_index', 'f')
    ->fields('f')
    ->addTag('node_access');
  switch ($delta) {
    case 'active':
      $title = t('Active forum topics');
      $query
        ->orderBy('f.last_comment_timestamp', 'DESC')
        ->range(0, variable_get('forum_block_num_active', '5'));
      break;

    case 'new':
      $title = t('New forum topics');
      $query
        ->orderBy('f.created', 'DESC')
        ->range(0, variable_get('forum_block_num_new', '5'));
      break;
  }

  $block['subject'] = $title;
  // Cache based on the altered query. Enables us to cache with node access enabled.
  $block['content'] = drupal_render_cache_by_query($query, 'forum_block_view', CACHE_TEMPORARY, DRUPAL_CACHE_PER_ROLE);
  $block['content']['#access'] = user_access('access content');
  return $block;
}

上面,我們很清楚可見:

  • $query – db_select() Object (沒有執行 $query->execute())
  • forum_block_view – 這是一個 callback,將會呼叫 forum_block_view_pre_render 進行後續處理,並將處理好的數據作為緩存資料
  • CACHE_TEMPORARY – 緩存的時間性設定,可參考 cache_set() $expire
  • DRUPAL_CACHE_PER_ROLE – 這項是緩存的方式,比如:
    DRUPAL_CACHE_PER_ROLE (以角色區分)
    DRUPAL_CACHE_PER_PAGE (以每一頁面區分)。
    也可以兩者同時區分:"DRUPAL_CACHE_PER_PAGE | DRUPAL_CACHE_PER_ROLE" (既以頁面又以角色區分)。
    還有一個是 DRUPAL_CACHE_PER_USER。選擇那一個要按實際需求,過份濫用也有可能會造成 DB 壓力。

多善用 🙂

Drupal 中文化字串 functions [t(), format_plural(), JavaScript]

在 Drupal 當中,包含一些處理文字界面的 FUNCTIONs,其實這不只為本地化語言而製造的,在英文中也起了防止 XSS 的作用。

最簡單的: t()


t($string, array $args = array(), array $options = array())

例如:


t('I am Kay.L.')

t("I am !name.", array('!name' => 'Kay.L'));

好了,在 Drupal 中我們常見 !variable / @variable / %variable,這是什麼?其實它們用作格式化我們傳入的內容,甚至有起 XSS 作用。

  • !variable: 原封不動傳遞給 Drupal
  • @variable: 透過 Drupal 內置 check_plain() Function 將 HTML 轉換 (== PHP htmlspecialchars)
  • %variable: 經過 drupal_placeholder() 處理,跟 @variable 差不多,不同的是前後增加了一個 HTML tag: <em>你的內容</em>

複數字串呢? Drupal 有 format_plural()


format_plural($count, '只有 1 個', '這有 @count 個');

聰明的你應該也理解到,在這裏 $count == @count

一字多譯又怎麼辦?

在 Drupal 7 引入 context 概念嘗試解決這問題

比如 "order",

  • 整理排序時,譯為 "次序"
  • 電子商務時,譯為 "訂單"

t('order', array(), array('context' => 'sorting'));

t('order', array(), array('context' => 'ecart'));

我們在不同的模組中,作不同的定義,以示區別。在 PO 翻譯中格式為:


msgctxt "sorting"
msgid "order"
msgstr "次序"

msgctxt "ecart"
msgid "order"
msgstr "訂單"

在 Javascript 當中,也有一樣的 Functions

例如:


Drupal.t('I am Kay.L.')

Drupal.t('I am !name.', {'!name' : 'Kay.L'})

var count = '10';
Drupal.formatPlural(count, '只有 1 個', '這有 @count 個');

var count = '10';

var args = {};
args['!apple'] = '蘋果';

Drupal.formatPlural(count, '只有 1 個!apple', '這有 @count 個!apple', args);

像上面只傳入一個變量,可以更直接,你應該知道的:


var count = '10';
Drupal.formatPlural(count, '只有 1 個!apple', '這有 @count 個!apple', {'!apple' : '蘋果'});

打開你瀏覽器的 Console (Firebug console, Chrome console..etc) 立刻試試吧。

(值得留意,Drupal 中幾乎 100% 字串都經過 t() 處理,但在寫程式碼時,有一些不必自我增加的,比如 hook_menu 中的 title / description,這預設就必經過 t() 處理。)

整合舊網站資料至 Drupal (利用 node_save() 建立 Node)

要從舊有的網站,轉移至 Drupal,是否很頭痛?? 怎麼 Drupal 建立一個 NODE 會改動這麼多資料表啊!! 該怎麼好呢? 請永遠緊記 Drupal API 是你的朋友!

透過編寫程式建立 Drupal Node

我們的編寫步驟及方向流程:

  1. 如果你還未會建立 Drupal 模組或要從外部執行,可使用 bootstrap 將它們連結起來。
  2. 從其他程式的資料庫或任何途徑取得你想要的資料 (PHP 你不會? 那請關閉此網頁)
  3. 透過 Drupal API node_save() 儲存到 Drupal 資料庫中
  4. 上 Youtube 看一會短片,讓程式自己運行

簡單示範程式碼:



// 打開 Drupal index.php,將前面兩行複製一下就可以
require_once './includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

$node = new stdClass(); // 建立 Node Object.

module_load_include('inc', 'node', 'node.pages');

/**
 * 自動準備好一些該有的參數
 * 因此在下面只需填上你要修改的內容便可
 * 如缺乏這一步,你需要獨一編寫。
 */
node_object_prepare($node); 

/**
 * 好戲現在開始
 * 從其他資料庫或程式中取得數據,然後再寫入
 * 使用 print_r($node) 或 Devel dsm($node) 了解有什麼可用
 */
 
$node->type = 'story';
$node->title = "Node 標題";
$node->body = "Node 內容";

$new_node = node_submit($node); // 準備儲存前的一些資料
node_save($new_node); // 最重要是這一步 !! 

就是這麼簡單。

透過現有模組

當然,如果你真是不會 PHP,而又沒有好好聽我上面所說而關閉此頁面的話,可以考慮使用現有的 Drupal 模組來達成。

到 Drupal.org 搜尋 csv import, node import, X improt 就有一大堆,我也不知那個好,跟我分享哦 🙂