Drupal架构与实现分析

不尽知用兵之害者,则不能尽知用兵之利——孙武《孙子兵法》

Drupal也算是架构精良的PHP开源CMS,它最大的优点是可以灵活性高,不仅可以用于实现一般的CMS功能,还可以容易的实现其它应用,如Drupal官方所说,可以实现wiki, blog甚至商城等.它最大的问题是灵活性太高,如果没有根据实际情况进行设计与管理,后果会很糟糕。

先看一下Drupal的总体架构,下图是Drupal官方的信息流图,详细参见:http://drupal.org/getting-started/before/overview。

从图上看起来非常清晰明了,不过这并没有体现出Drupal的架构方式。在我看来Drupal的基本架构就是Drupal核心+模块+模板,所谓的Drupal核心除模块之外的最基础的代码,全部在includes目录下面,另外Drupal有几个核心模块,如System, User, Node等。

Drupal的处理流程如下:

  • 系统初始化
  • 路径解析(路由)
  • 页面内容渲染
  • 页面布局及block渲染
  • 生成页面

1. Hook

Drupal几乎是纯面向过程的架构,这或许让很多习惯OOP的人有点惊讶,这或许也是Drupal变得难以管理(源码,模块)的原因。Hook是Drupal架构实现的关键,模块通过种hook与Drupal进行交互,这种方式被称为基于过程的AOP[1]

大多数情况下,Drupal通过module_invoke_all()来调用指定的hooks, 如下面代码将调用所有MODULE_NAME_cron()这样的函数:

module_invoke_all('cron');

下面是module_invoke_all()的源码,参数通过func_get_args函数获得,返回值是可选的。顺便提一下,在Drupal及其模块中经常用到func_get_args函数。

function module_invoke_all() {
  $args = func_get_args();
  $hook = $args[0];
  unset($args[0]);
  $return = array();
  foreach (module_implements($hook) as $module) {
    $function = $module .'_'. $hook;
    $result = call_user_func_array($function, $args);
    if (isset($result) && is_array($result)) {
      $return = array_merge_recursive($return, $result);
    }
    else if (isset($result)) {
      $return[] = $result;
    }
  }

  return $return;
}

module_invoke_all()并不是唯一的调用hook的方式,另一个比较常用的是drupal_alter,hook_menu_alter()、hook_mail_alter等就是通过该函数实现的。drupal_alter()与module_invoke_all()的主要差别是drupal_alter()是通过传递参数引用给模块,让模块可以个性数据。

drupal_alter('menu', $callbacks);
drupal_alter('form', $data, $form_id);

drupal_alter()的源码片段:

function drupal_alter($type, &$data) {
  //省略
  foreach (module_implements($type .'_alter') as $module) {
    $function = $module .'_'. $type .'_alter';
    call_user_func_array($function, $args);
  }
}

除了drupal_alter()与module_invoke_all()之外,模块也可以自定义调用hook的逻辑,就像hook_nodeapi的实现一样,如下面的代码所示。但它们都需要调用module_implements()来得知有哪些模块实现了该hook。

function node_invoke_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  $return = array();
  foreach (module_implements('nodeapi') as $name) {
    $function = $name .'_nodeapi';
    $result = $function($node, $op, $a3, $a4);
    if (isset($result) && is_array($result)) {
      $return = array_merge($return, $result);
    }
    else if (isset($result)) {
      $return[] = $result;
    }
  }
  return $return;
}

2. 模块

模块主要用于扩展系统功能,一般都会实现一个或多个hook,如hook_menu是最常见的,但这并不是必须的。是简单的模块只需要包含MODULE_NAME.module与MODULE_NAME.info即可,MODULE_NAME.install用于处理模块安装及卸载相关的逻辑。

所有模块信息都保存在system表中,system同时也保存模板信息。system.status为0表示模块未启用,system.weight有时候很重要,比如模块A和B同时实现hook_x(), 但A必须在B之后执行,则可以通过weight来控制。weight值越大越迟加载,因为模块的加载顺序如下:

SELECT name, filename, throttle FROM {system} WHERE type = 'module' AND status = 1 ORDER BY weight ASC, filename ASC

throttle用于控制模块是否被加载,不过几乎不用它。

3. Form

Drupal定义了一套很详细的API用于定义表单及相关处理逻辑,包括元素默认值、验证、显示结构及顺序等,并提供了默认的显示结构与样式(虽然这很不实用)。

第一个表单对应一个函数,也称为form_id, Drupal通过drupal_get_form($form_id)来获得该表单的定义并渲染HTML。如下面代码所示:

function user_login(&$form_state) {
  global $user;

  // If we are already logged on, go to the user page instead.
  if ($user->uid) {
    drupal_goto('user/'. $user->uid);
  }

  // Display login form:
  $form['name'] = array('#type' => 'textfield',
    '#title' => t('Username'),
    '#size' => 60,
    '#maxlength' => USERNAME_MAX_LENGTH,
    '#required' => TRUE,
  );

  $form['name']['#description'] = t('Enter your @s username.', array('@s' => variable_get('site_name', 'Drupal')));
  $form['pass'] = array('#type' => 'password',
    '#title' => t('Password'),
    '#description' => t('Enter the password that accompanies your username.'),
    '#required' => TRUE,
  );
  $form['#validate'] = user_login_default_validators();
  $form['submit'] = array('#type' => 'submit', '#value' => t('Log in'), '#weight' => 2);

  return $form;
}

这是用户登录的表单,form_id就是user_login, 这里有一个参数$form_state,但这并不是定义表单所必须的,通过drupal_get_form(‘user_login’,$form_state)获得该表单并在页面显示。默认情况下表单的action就是当前页面,但可以过来#action来指定其它路径,但#action指向的页面必须调用drupal_get_form($form_id),如drupal_get_form(‘user_login’,$form_state),否则表单将无法被处理,除非不使用Drupal的方式而是手动来处理表单内容。

第一个表单在渲染之前,Drupal都会自动加上一个form_build_id。

<input type="hidden" name="form_build_id" id="form-cb48b9e32a4a25f8b1e483153f2c7b06" value="form-cb48b9e32a4a25f8b1e483153f2c7b06" />

form_build_id的主要作用是用于表单缓存,很多时候表单缓存中包含一些跟踪上下文信息,所以如果表单的form_build_id被个性或超时就不能被正确处理。默认情况下,Drupal的缓存是保存在数据库中,表单的缓存则保存在cache_form表中。

4. Menu

Drupal的Menu非常重要,它用于定义用户可以访问的路径,模块通过实现hook_menu()来新的路径,每个路径对应一个页面。hook_menu()返回一个定义了menu的数组,如下所示:

 $items['node/%node'] = array(
    'title' => 'View',
    'page callback' => 'node_page_view',
    'page arguments' => array(1),
    'access callback' => 'node_access',
    'access arguments' => array('view', 1),
    'type' => MENU_CALLBACK,
  );

该数组定义了路径的页面回调函数,访问权限等。

这些信息并不是第次请求都会查找实现了hook_menu的函数,而是缓存在menu_router表中。路径的层次关系等信息刚是缓存在cache_menu表中。

详细文档请参考:http://drupal.org/node/102338

5. 模板

类似于hook_menu,hook_theme用于定义模板,模板的定义信息叫做theme registry, 一般都是保存在缓存中,所以每次更新都要更新缓存。

theme()是Drupal模板系统的关键函数,用于渲染HTML。Drupal的模板并非都是模板文件,还可以是函数,一般以theme_开头。

详细文档请参考:http://drupal.org/documentation/theme

6. 内容与用户

CMS当然离不开内容与内容管理,Drupal中内容的关键概念是节点,但它并不是在Drupal核心中实现,而是在Node核心模块中实现。所有内容都被看作节点,节点有不同类型,不同类型在总体结构上是一致的。主要的三个数据库关系表分别是node, node_revisions, node_type。另外涉及单个节点权限管理的表是node_access。

用户及用户管理部分主要有三个主要定义:用户(user), 用户组(user roles)及相关权限(permission)。

7. 系统变量

Drupal的系统变量以键值对的形式保存在variable表中,也可以在settings.php中配置。variable_get()、variable_set()、variable_del()分别用于获取、设置及删除系统变量。

8. 缓存

Drupal的默认缓存保存在数据库中,缓存表在cache_开头。如果需要以其它方式保存缓存数据,如保存到memcache中,只需要实现cache_set(), cache_get(), cache_clear_all()函数,并将cache_inc系统变量设置为自定义缓存处理的PHP文件路径即可。

9. 总结

Hook系统让Drupal具有良好的扩展性,同时也容易让调试与维护变得很困难。我认为如果不是用Drupal做一个像个人博客一样的简单网站,就应该知道:

1. Drupal虽然能做很多东西,但最好只用来做CMS相关的事,不用的东西彻底砍掉,免得浪费资源。
2. 仔细分析设计模块与模板,特别是hook的使用一定要慎重,配置要统一且简单,不然调试将变得异常困难。
3. 不要依赖社区的模块,包括Drupal安装包自带的,大多模块考虑太多的通用性,而不是性能。
4. 手动打补丁,不要使用自动更新。
5. 必要时修改Drupal内核。

10. 参考

1. Programming language trade-off: http://www.garfieldtech.com/blog/language-tradeoffs
2. Architectural priorities: http://www.garfieldtech.com/blog/architectural-priorities
3. The Drupal Overview: http://drupal.org/getting-started/before/overview
4. Form API Reference: http://api.drupal.org/api/drupal/developer!topics!forms_api_reference.html/6

Posted in PHP | Tagged , | 4 Comments

Tools for testing REST API

1. CURL

curl是一个很强大很实用的工具:

curl -v -X POST -H "Content-Type: application/json" -H "Accept: application/json" -d '{"id":1, "method":"message.query", "jsonrpc":"2.0", "params": {"user_id":9} }' http://fwso.cn/services/json-rpc

2. REST Client

WizTools.org RESTClient是一个用Java写的工具, Swing界面,使用比较方便:

3. WFetch

Windows下可以使用微软的WFetch, 功能比较齐全,但感觉不是很好用。

Posted in Notes | Tagged , , | Leave a comment

PHP in_array()与array_search的实现

今天看PHP源码的时候发现in_array与array_search是通过同一个函数php_search_array来实现的,只是最后一个参数不同.

in_array的实现:

PHP_FUNCTION(in_array)
{
    php_search_array(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0);
}

array_search的实现:

PHP_FUNCTION(array_search)
{
    php_search_array(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1);
}

php_search_array的代码:

static void php_search_array(INTERNAL_FUNCTION_PARAMETERS, int behavior) /* {{{ */
{
    zval *value,                /* value to check for */
         *array,                /* array to check in */
         **entry,               /* pointer to array entry */
          res;                  /* comparison result */
    HashPosition pos;           /* hash iterator */
    zend_bool strict = 0;       /* strict comparison or not */
    ulong num_key;
    uint str_key_len;
    char *string_key;
    int (*is_equal_func)(zval *, zval *, zval * TSRMLS_DC) = is_equal_function;

    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "za|b", &value, &array, &strict) == FAILURE) {
        return;
    }

    if (strict) {
        is_equal_func = is_identical_function;
    }

    zend_hash_internal_pointer_reset_ex(Z_ARRVAL_P(array), &pos);
    while (zend_hash_get_current_data_ex(Z_ARRVAL_P(array), (void **)&entry, &pos) == SUCCESS) {
        is_equal_func(&res, value, *entry TSRMLS_CC);
        if (Z_LVAL(res)) {
            if (behavior == 0) {
                RETURN_TRUE;
            } else {
                /* Return current key */
                switch (zend_hash_get_current_key_ex(Z_ARRVAL_P(array), &string_key, &str_key_len, &num_key, 0, &pos)) {
                    case HASH_KEY_IS_STRING:
                        RETURN_STRINGL(string_key, str_key_len - 1, 1);
                        break;
                    case HASH_KEY_IS_LONG:
                        RETURN_LONG(num_key);
                        break;
break;
                }
            }
        }
        zend_hash_move_forward_ex(Z_ARRVAL_P(array), &pos);
    }

    RETURN_FALSE;
}

behavior参数为1时为search,0为判断元素是否存在.
默认情况下用is_equal_function函数来判断元素值是否相同,如果是严格比较,则使用is_identical_function函数。

in_array与array_search在PHP中调用时的参数都是( mixed $needle , array $haystack [, bool $strict = false ] ), 所以
zend_parse_parameters的type_spec参数的值是”za|b”, z表示任意类型, a表示必须是array, |表示之后的参数为可选, b是boolean。

Posted in Notes, PHP | Tagged , | Leave a comment

查找数组中的唯一元素

问题

例如给定一个数组有101个元素,其实50个元素出现再次,只有一个仅出现一次,写一个函数找出该唯一的元素,元素均为整型。

1. 最差的方法

前提是元素都大于0.

int findOdd(int *input, int len) {
	int i, j, z, *buff, blen = (int) (len / 2 + 1);

	buff = (int *) malloc(sizeof(int) * blen);

	for (i = 0; i < len; i++) {
		z = -1;
		for(j = 0; j < blen; j++) {
			if (buff[j] == 0) {
				if (z == -1) {
					z = j;
				}
				continue;
			}
			if (buff[j] == input[i]) {
				buff[j] = 0;
				z = -1;
				break;
			}
		}

		if (z >= 0) {
			buff[z] = input[i];
		}

	}

	j = 0;

	for (i = 0; i < blen; i++) {
		if (buff[i] != 0) {
			j = buff[i];
			break;
		}
	}

	free(buff);
	return j;
}

2. 好点的方法

使用stdlib的快速排序.

int compare (const void * a, const void * b) {
	return ( *(int*)a - *(int*)b );
}

int findOdd2(int *input, int len) {
	int i = 0;

	qsort(input, len, sizeof(int), compare);	

	while (i < len) {
		if (input[i] != input[i+1]) {
			break;
		}
		i = i + 2;
	}
	return input[i];
}

3. 最佳的算法

不知道,请您指点。

Posted in Notes | Tagged , | Leave a comment

数码照片EXIF信息读取(PHP)

EXIF(Exchangeable image file format)是数码相机用于记录图片属性及拍摄数据的标准,最初由日本电子工业发展协会制定。这些信息是可以修改的,因此这些数据只有参考价值。在Windows下查看图片属性可以看到并修改这些信息,下图是在Picasa中看到的部分信息:

Exif扩展

PHP的Exif扩展用于处理这些信息,在编译PHP时–enable-exif即可。该简单比较简单,一共只有5个函数(准确地说只有4个,因为read_exif_data只是exif_read_data的别名),本文主要使用exif_read_data()与exif_thumbnail().

string exif_thumbnail ( string $filename [, int &$width [, int &$height [, int &$imagetype ]]] )

exif_thumbnail读取图片的缩略图,后面三个可选参数分别用于返回缩略图的宽、高及图片类型,图片类型是整型,如2代表JPEG格式(参考exif_imagetype),GD扩展提供了image_type_to_mime_type()函数可以方便地返回对应的mime类型。

array exif_read_data ( string $filename [, string $sections = NULL [, bool $arrays = false [, bool $thumbnail = false ]]] )

exif_read_data用于读取所有EXIF信息,返回的信息分为多个部分,如FILE, IFD0, EXIF等,详细解析参考文档。

顺便提一下,Imagick的getImageProperties()也可以读取部分exif信息。

读取信息

下面示例读取并列出所有信息。

$img = 'yansuo_p1.jpg';

//Read exif meta information
$info = exif_read_data($img, NULL, true, false);

//Read thumbnail data
$thumb = exif_thumbnail($img, $width, $height, $type);

//Get mime type: require GD2 extension
$mimeType = image_type_to_mime_type($type);
$data = base64_encode($thumb);

echo 'Thumbnail: ', $width , 'x', $height, ', Type:',
	$type, ';', $mimeType , "<br />\n";
echo '<img alt="" src="data:', $type, ';base64,', $data, '" />';

foreach ($info as $key=>$sinfo) {
	echo '<h3>', $key, '</h3>';
	if (is_array($sinfo)) {
		echo '<table border="1" cellspacing="0" borderColor="#CCC">';
		foreach ($sinfo as $skey=>$svalue) {
			echo '<tr><td>', $skey, '</td><td>', $svalue, '</td></tr>';
		}
		echo '</table>';
	}
}

测试图片:http://lib.fwso.cn/exif/yansuo_p1.jpg
示例: http://lib.fwso.cn/exif/exif_read_data.php

注意到数据分为了几个部分(组), FILE是图片文件的基本信息.

表1, FILE
FileName yansuo_p1.jpg
FileDateTime 1321330357
FileSize 5439972
FileType 2
MimeType image/jpeg
SectionsFound ANY_TAG, IFD0, THUMBNAIL, EXIF, INTEROP, MAKERNOTE

COMPUTED是经过exif扩展处理的一些数据,IsColor没有找到相关资料,应该是否彩色的意思;ByteOrderMotorola是字节顺序,0为低字节序(little-endian), 1为高字节序(big-endian);CCDWidth表示相机感光器大小,该值跟计算方法有点关系,Canon 600D的感光器大小为22.3mm,这里显示为22,Picasa为23;ApertureFNumber,及后面几项不用解释了。其实这些我只关心Width, Height, CCDWidth, ApertureFNumber.

表2, COMPUTED
html width=”5184″ height=”3456″
Height 3456
Width 5184
IsColor 1
ByteOrderMotorola 0
CCDWidth 22mm
ApertureFNumber f/6.3
UserComment
UserCommentEncoding UNDEFINED
Thumbnail.FileType 2
Thumbnail.MimeType image/jpeg

IFD0表示主要图相的属性,对应的IFD1就是thumbnail。IFD0中我关心的数据只是Make和Model.

表3, IFD0
Make Canon
Model Canon EOS 600D
Orientation 1
XResolution 72/1
YResolution 72/1
ResolutionUnit 2
DateTime 2011:11:05 14:38:23
Artist
YCbCrPositioning 2
Copyright
Exif_IFD_Pointer 348

EXIF IFD部分数据比较多,只列出了部分,其中ExposureTime是曝光时间, FNumber是光圈大小, ExposureProgram是曝光程序(0表示未定义, 1表示手动曝光, 2表示程序自动曝光, 3表示光圈优先自动曝光,4表示快门优先自动曝光…), ISOSpeedRatings为ISO感光度,ExifVersion为EXIF标准的版本, Flash为闪光方式(16关闭,没有闪光,参考),FocalLength为焦距(这里是20mm, Canon 600D的实际焦距是20×1.6=32mm),

表4, EXIF IFD
ExposureTime 1/800
FNumber 63/10
ExposureProgram 3
ISOSpeedRatings 100
UndefinedTag:0×8830 2
UndefinedTag:0×8832 100
ExifVersion 0230
DateTimeOriginal 2011:11:05 14:38:23
DateTimeDigitized 2011:11:05 14:38:23
ComponentsConfiguration 
ShutterSpeedValue 630784/65536
ApertureValue 352256/65536
ExposureBiasValue 0/1
MeteringMode 5
Flash 16
FocalLength 20/1
FocalPlaneXResolution 5184000/905
FocalPlaneYResolution 3456000/595
FocalPlaneResolutionUnit 2
CustomRendered 0
ExposureMode 0
WhiteBalance 0
SceneCaptureType 0
UndefinedTag:0xA430
UndefinedTag:0xA431 074053015010
UndefinedTag:0xA432 Array
UndefinedTag:0xA434 EF-S18-55mm f/3.5-5.6 IS II
UndefinedTag:0xA435 0000042eb8

还注意到一些数据属性为UndefinedTag:0x****, 这些属性都是非标准属性,是由相机生产商自定义的一些数据。特别是MakerNote的属性都是根据不能品牌型号而定的。

一个简单示例:http://lib.fwso.cn/exif/index.php

参考

1. EXIF tags

Posted in PHP | Tagged , , | Leave a comment