本章将介绍节点和节点类型,我将展示怎样用两种不同方式创建一个节点类型,首先展示用Drupal钩子通过写一个模块来程序化地创建一个节点类型,这种情况在决定一个节点能干和不能干什么的时候有更大程度上的控制权和柔性。然后我将展示怎么样从Drupal管理员接口创建一个节点类型,最后我们将研究Drupal节点访问控制机制。
TIP:开发者经常使用术语node和node type,在Drupal用户接口中,它们分别对应posts和content type,在术语的使用上我们尽量同管理员保存一致。
节点具体是什么
Drupal的开发新手问的首要问题就是:”node是什么?“,一个节点是一个内容片,Drupal为每个内容片指定一个ID号码叫节点ID(代码中简写为$nid),通常每个节点还有个标题,允许管理员查看一个标题的列表。
注意:如果你熟悉对象导向,可以将节点类型想象为一个类,而个体节点想象为一个对象的实例,尽管Drupal不是100%的对象导向,一个好的原因在这里(http://api.drupal.org/api/HEAD/file/development/topics/oop.html)。
有许多不同j节点类别,就是节点类型。一些常用的节点类型是”blog entry“ ”poll“和”forum“。有时术语内容类型(content type)与节点类型(node content)是同义词,尽管一个节点类型是真正的抽象概念并且被认为源自节点,就像图7-1的描述。
Figure 7-1. Node types are derived from a basic node and may add fields.
所有的内容类型作为节点的最美好的地方是它们基于相同下层数据结构。对于开发者,这意味着你能让许多操作程序化地对待所有内容,它能简单地在节点上执行批量操作并且你还能创造行动获得大量自定义内容类型的功能。因为下层节点数据结构和行为,Drupal天生支持检索、创建、编辑和管理内容,这些也一致地呈献给终端用户。创建、编辑和删除节点的表单看起来和感觉上非常相似,成为一个一致的因而更加易用的接口。
节点类型通常通过增加它自己的数据属性来扩展基本节点。节点类型poll存储投票的选项,不管当前投票是否可用,也不管是否允许用户投票;forum节点类型为每个节点载入分类,那样它就知道它适合放在管理员定义的论坛的哪里啦;blog节点例外,不增加任何其他的数据,它们只通过为每个用户建立blogs和为每个blog建立Rss来增加不同的视图到数据。所有节点都有下列存储在node和node_revisions表中的属性:
+ nid:节点的唯一的ID + vid:节点的唯一的版本ID,需要它是因为Drupal能为每个节点存储内容版本 + type:每个节点都有一个节点类型——例如,blog、story、article、image等等 + language:节点的语言,开箱即用这个列是空的,包括语言中立的节点 + title:短于255个字符的字符串,作为节点的标题,除非节点类型声明节点没有标题, 通过将node_type的has_title字段设成0 + uid:作者的用户ID,默认节点有一个单个的作者 + created:节点建立时的Unix时间戳 + changed:节点最后修改时的Unix时间戳,如果你使用节点版本系统, node_revisions表中的timestamp也用相同的值 + comment:一个整数字段,描述节点的评论状态,有3个可能的值: + 0:节点不允许评论 在comment模块禁用是,这是默认值 + 1:当前节点不允许有更多的评论,在用户接口的“Comment settings”表单中 相当于“read only” + 2:评论可见、用户可以添加新的评论,控制评论权限和是否可见是在comment模块中,在 在用户接口的“Comment settings”表单中相当于“read/write” + promote:一个整型字段决定是否在front页面上展示这个节点,有两个值: + 1:提升到front页面,这个节点将提升到你的站点的默认首页,正常页面也展示它 例如http://example.com/?q=node/3,首页这个叫法不恰当,实际应该是 http://example.com/?q=node页面,它包含所有promote为1的节点,这个页面是 默认的首页 + 0:节点不显示在http://example.com/?q=node上 + sticky:当Drupal在一个页面上显示一个节点列表时,默认是先显示标记为sticky的那些节点, 然后是按照建立的时间顺序显示节点,1代表sticky,同一个列表中可以有多个标记 为sticky的节点 + tnid:当一个节点是作为另一个节点的翻译版本的时候,源节点的nid就存储在这,例如 节点3是英语、节点5是与节点3同内容的瑞典语,节点5的tnid字段就是3 + translate:值是1指示翻译需要更新,0表示谁翻译是新的
如果你使用节点版本系统,Drupal将建立内容版本可以跟踪谁最后编辑
不是所有东西都是节点
用户、区块 、评论不是节点,它们有自己特殊的数据结构,有自己的钩子系统来实现自己的目的。节点(通常)有标题和主体内容,表现一个用户的数据结构不需要这些,相反,它需要一个e-mail地址、一个用户名、一个安全存储的密码;区块是一个为一些诸如菜单导航、搜索框、最新评论列表等小片内容的轻量级存储解决方案;评论也不是节点,使它们保持轻量级是正确的,每页上完全可能有100个或更多的评论,如果这些评论中的每个评论在载入时都执行一遍节点钩子系统,那么这将是对性能的巨大打击。 过去,对用户和评论是否应该是节点存在大量争论,并有贡献模块真正实现了这个,警告不要过度关注这个论点,这就像在一个编程集会上大喊“Emacs是最好的”一样。(Emacs和Vi都行,个人习惯,不是核心问题,反而容易引起不必要的争论,要关心真正的核心问题。)
创建一个节点模块
传统上,当你想建立一个新的节点类型是你要写一个节点模块,它承担提供你的节点类型需要的新的感兴趣的东西,我们说“传统上”是因为最近Drupal框架允许你在管理员接口建立新的节点类型并利用贡献模块扩展它们的功能,这比白手起家写代码容易,本章我们将涵盖这两种解决方案。 我要写个模块,让用户增加一个工作帖子到一个站点,一个工作帖子节点应该包括一个标题、一个含有工作细节的主体和一个能输入公司名字的字段。对于标题和主体,我将使用内建的节点title和body,也是Drupal所有节点的标准,我需要为公司名称增加一个新的自定义字段。 开始时在sites/all/modules/custom目录建一个文件夹job_post。
创建.install文件
job post模块的install文件执行所有的setup操作,像定义节点类型、建立组成我们节点类型的字段、处理卸载过程等。
<?php /** * @file * Install file for Job Post module */ /** * Implements hook_install(). * - Add the body field * - Configure the body field * - create the company name field */ function job_post_install() { node_type_rebuild(); $types = node_type_get_types(); // add the body field to the node type node_add_body_field($types['job_post']); // load the instance definition for our content type's body $body_instance = field_info_instance('node', 'body', 'job_post'); // Configure the body field $body_instance['type'] = 'text_summary_or_trimmed'; // save our changes to the body field instance. field_update_instance($body_instance); // create all the fields we are adding to our content type. foreach (_job_post_installed_fields() as $field) { field_create_field($field); } // Create all the instance for our fields. foreach (_job_post_installed_instances() as $instance) { $instance['entity_type'] = 'node'; $instance['bundle'] = 'job_post'; field_create_instance($instance); } } /** * Return a structured array defining the fields create by content type. * For the job post module there is only one additional field - the company name * Other fields could be added by defining them in this function as additional elements * in the array below */ function _job_post_installed_fields() { $t = get_t(); return array( 'job_post_company' => array( 'field_name' => 'job_post_company', 'label' => $t('Company posting the job listing'), 'type' => 'text', ), ); } /** * Return a structured array defining the field instances associated with this content type. */ function _job_post_installed_instances() { $t = get_t(); return array( 'job_post_company' => array( 'field_name' => 'job_post_company', 'type' => 'text', 'label' => $t('Company posting the job listing'), 'widget' => array( 'type' => 'text_textfield', ), 'display' => array( 'example_node_list' => array( 'label' => $t('Company posting the job listing'), 'type' => 'text', ), ), ), ); } /** * Implements hook_uninstall() */ function job_post_uninstall() { // Gather all the example content that might have created while this // module was enabled. $sql = 'SELECT nid FROM {node} n WHERE n.type = :type'; $result = db_query($sql, array(':type' => 'job_post')); $nids = array(); foreach ($result as $row) { $nids[] = $row->nid; } // Delete all the nodes at once node_delete_multiple($nids); // Loop over each of the fields defined by this module and delete // all instance of the field, their data, and the field itself. foreach (array_keys(_job_post_installed_fields()) as $field) { field_delete_field($field); } // Loop over any remaining field instance attached to the job_post // content type (such as the body field) and delete them individually. $instances = field_info_instances('node', 'job_post'); foreach ($instances as $instance_name => $instance) { field_delete_instance($instance); } // Delete our content type node_type_delete('job_post'); // Purge all field infromation field_purge_bacth(1000); }
创建.info文件
让我们还建立一个.info文件放到job_post目录
name = Job Post description = A job posting content type package = Pro Drupal Development core = 7.x files[] = job_post.install files[] = job_post.module
创建.module文件
最后,我们需要模块文件自己,在sites/all/modules/custom/job_posting目录中建一个叫job_post.module的文件,在你完成这个模块后,你可以在Modules页面激活它,开始你应该在文件中写下PHP的tag:
<?php /** * @file * This module provides a node type called job post */
提供关于我们节点类型的信息
现在你准备增加一些钩子到job_post.module,第一个钩子我们想实现的是hook_node_info(),Drupal在它搜寻哪些类型是激活的时执行这个钩子,你需要为你的自定义节点提供某些元数据。
/** * Implements hook_node_info() to provide our job_post type. */ function job_port_node_info() { return array( 'job_post' => array( 'name' => t('Job Post'), 'base' => 'job_post', 'description' => t('Use this content type to post a job.'), 'has_title' => TRUE, 'title_label' => t('Job Title'), 'help' => t('Enter the job title, job description, and the name of the company that posted the job'), ), ); }
单个一个模块可以定义多个节点类型,返回值应该是一个数组。这是可以在node_info()钩子中支持的分解的元数据值: + name:节点类型的人类可读名字,必需 + base:base字符串用于构造符合节点类型的回调(例如,如果base是example_foo,那么example_foo_insert将在 插入这个类型的一个节点时调用)这个值通常但不总是模块名,必需 + description:节点类型的简要描述,必需 + help:当建立一个此类型的节点是显示的帮助信息,可选 + has_title:布尔值,指示这个节点类型是否有标题字段,默认为TRUE,可选 + title_label:标题字段的标签,默认为Title,可选 + locked:布尔值,指示管理员是否可以此类型的机器名,FALSE可以改变(未锁)TRUE不可改变,默认为TRUE,可选
注意:在前面的列表base中提到的内部的name字段,用于构建“Create content”连接的URL,例如,我们用job_post做我们节点类型的内部名(它是我们返回数组的键),那么去建立一个新的job_post,用户将转到http://example.com/?q=node/add_job_post。通常将locked设成FALSE使内部名可修改不是个好主意。内部名存储在node和node_revirsions表的type列。
修改菜单回调
要在“Create content”页面上有一个连接,不需要事先hook_menu(),Drupal自动搜寻新内容类型并将其加到到http://example.com/?q=node/add页面,如图7-2所示,一个到节点提交表单的连接将出现在http://example.com/?q=node/add/job_post中,名称和描述将从你定义的job_post_node_info()中取值。
如果你不希望增加这个字节连接,你能用hook_menu_alter()删除它,例如,下面代码能为所有那些没有“administer nodes”权限的人删除这个页面。
/** * Implements hook_menu_alter() */ function job_post_menu_alter(&$callbacks) { // If the user does not have 'administer nodes' permisssion, // disable the job_post menu item by setting its access callback to FALSE. if (!user_access('administer nodes')) { $callback['node/add/job_post']['access callback'] = FALSE; // Must unset access arguments or Drupal will use user_access() // as a default access callback. unset($callback['node/add/job_post']['access arguments']); } }
用hook_permission()定义权限
典型的模块定义节点类型的权限包括建立此类型节点、修改你建立的节点、修改任何人建立的这个类型节点的能力,这是在hook_permission()中作为 create job_post、edit own job_post、edit any job_post等等定义的,你可以在你的模块中定义这些权限,让我们现在使用hook_permission()建立这些权限:
/** * Implements hook_permission(). */ function job_post_permission() { return array( 'create job_post' => array( 'title' => t('Create a job post'), 'description' => t('Create a job post'), ), 'edit own job post' => array( 'title' => t('Edit own job post'), 'description' => t('Edit own job post'), ), 'edit any job post' => array( 'title' => t('Edit any job post'), 'description' => t('Edit any job post'), ), 'delete own job post' => array( 'title' => t('Delete own job post'), 'description' => t('Delete own job post'), ), 'delete any job post' => array( 'title' => t('Delete any job post'), 'description' => t('Delete any job post'), ), ); }
现在,如果你导航到People点击Permission选项卡,你定义的新的权限就在那里并准备分配给用户角色。
用hook_node_access()限制节点类型的访问
你在hook_permission()中定义了权限,但是它们是怎么实施的呢?节点模块能使用hook_node_access()来限制访问它们定义的节点类型。超级用户(ID为1)允许通过任何访问检查,这种情况下钩子不调用。如果没有为你的节点类型定义钩子,那么所有的访问检查将失败,那么将只有有“administer nodes”权限的超级用户才可以建立、编辑、删除这个类型的内容。
/** * Implements hook_node_access(). */ function job_post_node_access($op, $node, $account) { $is_author = $account->uid == $node->uid; switch($op) { case 'create': // Allow if user's role has 'creat joke' permission. if (user_access('create job', $account)) { return NODE_ACCESS_ALLOW; } case 'update': // Allow if user's role has 'edit own joke' permission and user is // the author; or if the user's role has 'edit any joke' permission. if (user_access('edit own job', $account) && $is_author || user_access('edit any job', $account)) { return NODE_ACCESS_ALLOW; } case 'delete': // Allow if user's role has 'delete own joke' permission and user is // the author; or if the user's role has 'delete any joke' permission. if (user_access('delete own job', $account) && $is_author || user_access('delete any job', $account)) { return NODE_ACCESS_ALLOW; } } }
上面的函数允许用户去建立一个job post,如果他的角色中含有“create job post”权限,如果角色含有“edit own job post”权限且是此帖子的作者或者他们有“edit any job post”权限,他们还能更新一个job post;如果他们有“delete own job post”权限则可以删除自己的帖子,如果有“delete any job post”权限,则可以删除任意job post类型的节点。
我们传给hook_node_access()的参数$op的另一个值是view,允许你去控制谁可以看到这个节点,作为警告,hook_node_access()只是为单独的节点视图页面调用,hook_node_access()将不能阻止某些人从强制节点视图查看这个节点,比如一个多节点列表页面。你能通过创造性地使用其它钩子和操作$node->teaser值来直接克服这些,但是这只是一个小技巧,最好解决方案是使用hook_node_grants(),我们过会再说。
为我们的节点类型自定义表单
到此为止,我们为我们的新节点类型定义了元数据定义了访问权限,下一步,你需要建立节点表单以使用户能输入一个job。你可以通过实现hook_form()来做到这些,Drupal提供一个包含标题、主体、和其它任何你定义的可选字段的标准节点表单,对于Job post内容类型,标准表单相当合适,那么我们用它来渲染add/edit表单。
/** * Implement hook_form() with the standard default form. */ function job_post_form($node, $form_state) { return node_content_form($node, $form_state); }
注意:如果你不熟悉form API,请看第11章。
作为站点管理员,如果你激活你的模块,你能通过浏览Add content -> Job Post来看见刚建立的表单(图7-3)
当你使用一个节点表单而不是一个通用表单工作时,node模块处理校验和保存所有它知道的节点表单的默认字段(诸如标题、主体字段),也提供钩子给开发者来校验和保存你的自定义字段,下步我们将说明这些。
用hook_validate()校验字段
当你的节点类型的一个节点被提交,你的模块将通过hook_validate()调用,因此,当用户提交表单来建立一个job post,函数job_post_validate()使你能校验在你自定义字段的输入,你能在提交后用form_set_value()来改变数据,错误由form_set_error()来设置,见下面:
/** * Implements hook_validate(). */ function job_post_validate($node) { // Enforce a minimum character count of 2 on company names. if (isset($node->job_post_company) && strlen($node->job_post_company['und'][0]['value']) < 2) { form_set_error('job_post_company', t('The company name is too short. It must be alteast 2 char.'), $limit_validation_error = NULL); } }
注意你已经在hook_node_info()中定义了主题最少字符数,Drupal将自动校验,尽管如此,punchline字段是一个你增加的扩展字段,那么你需要响应它的校验和载入、存储。
我将赞助人的信息拆分进独立的主题函数中,这个主题函数可以轻易被覆写,这是对过度工作的管理员的一种礼貌态度,因为他可能使用你的模块,但是他不想定制输出的外观和感觉。为激活这个特性,我将建立一个hook_theme()函数,它将定义模块怎样处理新的赞助人字段,在hook_thmem()函数中,我将定义分配给赞助人字段的变量和模板文件,此文件将用来定义怎样将赞助人信息作为节点的一部分渲染。
/** * Implements hook_theme(). */ function job_post_theme() { // define the variables and template associated with the sponsor field // The sponsor will contain the name of the sponsor anr the sponsor_id // will be used to create a unique CSS ID return array( 'sponsor' => array( 'variables' => array('sponsor' => NULL, 'sponsor_id' => NULL), 'template' => 'sponsor', ), ); }流程的最后一步是建立为赞助商信息建立模板文件,在hook_theme()函数中我分配一个值sponsor给template属性--然后在我的模块目录建立一个sponsor.tpl.php文件,文件内容入下:
<?php /** * @file * Default theme implementation for rendering job post sponsor infomation * * Available variables: * - $sponsor_id: the node ID associated with the job posting * - $sponsor: the name of the job post sponsor */ ?> <div id="sponsor-<?php print $sponsor_id; ?>" class="sponsor"> <div class="sponsor-title"> <h2>Sponsored by</h2> </div> <div class="sponsored-by-message"> Thie job posting was sponsored by: <?php print $sponsor; ?> </div> </div>你需要去清理主题注册缓存来使Drupal看起了是你主题钩子的样子,你可以是我弄个devel.module或浏览Modules页面来清理缓存。你现在拥有了全功能的job post条目和展示系统,进到以前的某些jobpost试试有什么东西,你能看到你的帖子有了些格式,如同图7-4和7-5。
用hook_node_xxxxx()来操作非我们类型的节点
前面的钩子只是基于模块实现的hook_node_info()的base键才调用,当Drupal看到一个blog节点类型,blog_load()就被调用。如果你想去增加某些信息到每个节点,不管它是什么类型,该如何?到目前为止,我们学的钩子都不能胜任这项工作,为此,我们需要一个特别的强大的钩子集合。 node_xxxx钩子为模块创造一个机会,使之在任意一个节点的的生命周期对不同操作做出反应,node_xxxx钩子通常由node.module只在node-type-specific回调调用之后才调用,这里是主要的node_xxxx钩子列表: + hook_node_insert($node):响应节点的建立。 + hook_node_load($node,$type):起作用在一个节点从数据库载入的时候。$node是键化数组,$type是载入的类型的键化数组。 + hook_node_update($node):响应一个节点的更新 + hook_node_delete($node):响应一个节点的删除 + hook_node_view($node,$view_mode):起作用在节点开始渲染的时候,$view_mode是显示模式,例如full活teaser + hook_node_prepare($node):起作用在节点刚要显示在add/edit表单时 + hook_node_presave($node):起作用在节点开始插入或更新 + hook_node_access($node, $op, $account):控制节点的访问,$op是将要执行的操作类型(如insert、update、view、delete) $account是执行这些操作的用户的账号 + hook_node_grants_alter(&$grants, $account, $op):在试图查看、编辑或删除一个节点时,改变用户访问授权 在显示一个节点页面如http://example.com/?q=node/3时,钩子的触发顺序如图7-6.节点如何存储
在数据库中节点是分开存储的,node表包含大多数描述节点的元数据。node_revisions表包含节点的主体和摘要,连同版本信息。并且你可以在job_post.module看到,在节点载入时,其它节点可以自由地向节点中追加数据而且可以将任何想要的数据存储在自己的表中。 如图7-7,一个节点对象包含大多数公共属性,注意有fieldAPI建立的用于存储job post公司的表是与主要节点表分开的,依赖于什么模块被激活,你的Drupal节点对象可能包含或多或少的属性。通过管理员界面来创建节点类型
尽管使用模块来创建一个节点类型的方式提供了更多的控制与效能,但是它太复杂了,难道就没有一个比编程创建节点类型更好的方式了吗,Drupal为你提供了核心的自定义节点类型功能。 通过管理接口你能增加一个节点类型(就像你的模块job_post一样),在Structure->Content types,如果你的模块job_post激活了,你要确保节点类型有个不同的名字,避免命名空间冲突。在job_post.module例子中,你需要三个字段:job title、job description(节点主体)和发布帖子的公司名字name。在job_post.module中,你不得不手工增加body字段和name of company字段,使用核心自定义节点类型功能,通过用户接口你能排除所有编程任务,Drupal核心处理一切建表任务及插入、更新、删除、访问控制和查看节点。限制节点访问
有几种方式来限制对节点的访问。你已经看到怎样用hook_access()来控制访问节点及使用hook_permission()来定义权限,但是Drupal用node_access表提供了一个更加丰富的访问控制集,还有两个访问钩子: hook_node_grants(),hook_access_records()。 当Drupal最初安装时,一个单个记录写进了node_access表,作用为关闭Drupal节点访问机制,只有当一个使用节点访问机制的模块激活时,Drupal这部分才开始生效,在modules/node/node.module中的函数node_access_rebuild()保持对哪个节点访问模块激活的跟踪,如果这些模块禁用,那么这个函数恢复默认记录,表7-2: Table 7-2. The Default Record for the node_access Table --------------------------------------------------------------------------- nid gid realm grant_view grant_update grant_delete --------------------------------------------------------------------------- 0 0 all 1 0 0 --------------------------------------------------------------------------- 通常,一个节点访问模块被使用(就是修改node_acccess表的那个),Drupal将允许访问节点,除非节点访问模块将一个记录插入node_access表,来定义怎样对待访问控制。定义节点授权
节点上的操作有3个基本权限:view、update和delete,当这些操作中的某个将要发生,提供节点类型的模块首先通过它的hook_access()的实现说话,如果模块没有为访问允许提供位置(就是那个地方为NULL,而不是TRUE或FALSE),Drupal询问所有对节点访问感兴趣的模块来回应是否操作必须被允许的问题,它们靠响应hook_node_grants()来做这些,hook_node_grants()带有一个当前用户每个领域的授权ID列表。什么是领域(Realm)
Realm是一个独特的字符串,它允许多个节点访问模块去共享node_access表,例如,acl.module是一个贡献模块,利用访问控制列表来控制节点访问,它的realm是acl。另外的贡献模块还有taxonom_access.module,它基于taxonomy分类来约束对节点的访问,它是用term_access作为realm。那么,realm就是一些在node_access表中指示你的模块空间的东西,它有点像命名空间,当你的模块返回授权ID,你要为你的模块定义realm。什么是授权ID
一个授权ID是一个标示符,为一个给定的领域(realm)提供关于节点访问权限信息,例如一个节点访问模块forum_access.module,它通过用户规则来管理到forum节点的访问——可以使用用户规则ID作为授权ID,一个通过美国邮政编码来管理节点访问的模块可以使用邮政编码作为授权ID。在每一种情况中,它都得确定一些用户的东西:他是否被分配了这个用户规则或者他的邮编是否是12345?或者用户是否在访问控制列表中,或者用户是否已经订阅了一年以上? 尽管每个授权ID对节点访问模块(这些模块从包含这个授权ID的领域来获得授权ID)来说意味着特别的东西,这只不过表现为node_access表中有一行包含这个授权ID来许可访问,带有访问类型,此类型有grant_view、grant_update、grant_delete列的值是不是1来决定。 当一个节点保存时,授权ID就被插入node_access表。每一个实现hook_node_access_records()的模块都传递一个节点对象。模块预计测试一下节点并简单返回(如果它不为节点处理访问)或者为node_access表的插入返回一个授权数组。下面是node_access_example.module的一个例子,在这个模块中,hook_node_access_records()检视节点是否是私有——如果是,那么授权设置成只能查看,第二个授权检视用户是否为作者——如果是,那么设置所有授权(查看、更新、删除)。function hook_node_access_records($node) { // We only care about the node if it has been marked private. If not, it is // treated just like any other node and we completely ignore it. if ($node->private) { $grants = array(); $grants[] = array( 'realm' => 'example', 'gid' => 1, 'grant_view' => 1, 'grant_update' => 0, 'grant_delete' => 0, 'priority' => 0, ); // For the example_author array, the GID is equivalent to a UID, which // means there are many many groups of just 1 user $grants[] = array( 'realm' = 'example_author', 'gid' => $node->uid, 'grant_view' => 1, 'grant_update' => 1, 'grant_dalete' => 1, 'priority' => 0, ); return $grants; } }
节点处理流程
当一个操作要在一个节点上执行,Drupal按照下面的流程轮廓顺序执行,如图7-8:小结
读完本章,你可以: + 理解什么是节点什么是节点类型 + 写建立节点类型的模块 + 理解怎样钩入节点的建立、保存、载入等等 + 理解怎样访问节点的判定