WordPress 前台基础架构分析之四:总览

核心函数基本构成

WordPress核心文件包含了绝大部分WordPress流行函数,均位于wp-includes/文件夹下。

  • functions.php # main API
  • options.php # Options API
  • plugin.php # Plugin API
  • User.php # User API
  • Post.php # Post API
  • Taxnonmy.php # Taxnonmy API
  • formatting.php # Formatting Functions
  • pluggable.php # 可覆盖的API
  • ..,

这些众多的文件定义了众多API,常见的有:

  • Plugin API
  • Widgets API
  • Shortcode API
  • HTTP API # 发送HTTP请求
  • Settings API
  • Options API
  • Dashboard Widgets API
  • Rewrite API

再看Loop时的全局变量

前文已经说到,WordPress的博客功能其实就是再做三件事:

  1. 加载必要的文件
  2. 根据条件查询数据库,然后保存到全局对象或者全局对象中。
  3. 根据查询后的结果,选择加载合适的模板。

Loop时设置的关键的全局变量有:

  • Post Dtata : $post
  • Author Data : $autordata
  • User Data : $current_user
  • Environment Data: 环境变量
    • 客户端:$is_IE,$is_iphone,$is_mobile,…
    • 服务端: $is_apache,$is_IIS

WordPress定义的Template Tag 会根据全局变量的不同,给出相应的值(或者默认值)。

因此,应该优先调用Template Tag而非使用全局对象或者变量。

保存插件的数据到数据库

大多数WordPress插件都需要获取管理员或用户输入的一些信息,并保存在会话中,以便在过滤器函数(filter)、动作函数(action)和模板函数(template)中使用。若需要在下次会话中继续使用这些信息,就必须将它们保存到WordPress数据库中。以下是将插件数据保存到数据库的几种方法:

  1. 使用WordPress的”选项”机制。
  2. 使用文章元数据(又名自定义域)。
  3. 使用自定义分类。
  4. 使用其他数据表
  5. 创建一个新的,自定义的数据库表。这种方式适合保存那些与个人文章、页面或附件无关的,会随着时间逐渐增长的,并且没有特定名称的数据。关于如何使用,你可以阅读Creating Tables with Plugins以获取更多信息

WordPress选项机制

WordPress有一个”选项”机制,适合储存少量静态的、具有特定名称的数据(通常是网站所有者在创建插件时设置的一些初始化参数,并且以后很少会进行改动) 。
选项的值可以是字符串、数组,甚至是PHP对象(当然,PHP对象在保存时必须能够被序列化或转换成字符串,检索的时候也必须能够被反序列化)。选项的名称必须是字符串,且必须是唯一的,这样才能够确保它们不会和WoredPress或其它插件产生冲突。

对应数据库中数据表{$wpdb->prefix}_options。该表结构类似于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mysql> select * from wp_options limit 25;
+-----------+---------------------------+-----------------------------+----------+
| option_id | option_name | option_value | autoload |
+-----------+---------------------------+-----------------------------+----------+
| 11 | comments_notify | 1 | yes |
| 12 | posts_per_rss | 10 | yes |
| 13 | rss_use_excerpt | 0 | yes |
| 14 | mailserver_url | mail.example.com | yes |
| 15 | mailserver_login | login@example.com | yes |
| 16 | mailserver_pass | password | yes |
| 17 | mailserver_port | 110 | yes |
| 18 | default_category | 1 | yes |
| 19 | default_comment_status | open | yes |
| 20 | default_ping_status | open | yes |
| 21 | default_pingback_flag | 1 | yes |
| 22 | posts_per_page | 10 | yes |
| 23 | date_format | F j, Y | yes |
| 24 | time_format | g:i a | yes |
| 25 | links_updated_date_format | F j, Y g:i a | yes |
+-----------+---------------------------+-----------------------------+----------+

选项机制的几个主要函数:

  • add_option($name,$value,$deprecated,$autoload)
  • get_option($name)
  • update_option($name,$value)
  • delete_option()

通常情况下,最好能够对插件选项的数量进行一下精简。例如,如果有10个不同名称的选项需要保存到数据库中,那么,就可以考虑将这10个数据作为一个数组,并保存到数据库的同一个选项中。

文章元数据

这种方式与第一种方案类似,但是与具体的post相关联,故而适合保存与post相关的数据。

对应数据表{$wpdb->prefix}_postmeta,基本的数据结构类似于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mysql> select * from wp_postmeta limit 15 ;
+---------+---------+-----------------------------+-----------------------------+
| meta_id | post_id | meta_key | meta_value |
+---------+---------+-----------------------------+-----------------------------+
| 1 | 2 | _wp_page_template | default |
| 2 | 5 | _edit_last | 1 |
| 3 | 5 | _edit_lock | 1441571672:1 |
| 4 | 7 | _edit_last | 1 |
| 5 | 7 | _edit_lock | 1441572102:1 |
| 6 | 8 | _edit_last | 1 |
| 7 | 8 | _edit_lock | 1441572197:1 |
| 8 | 10 | _menu_item_type | custom |
| 9 | 10 | _menu_item_menu_item_parent | 0 |
| 10 | 10 | _menu_item_object_id | 10 |
| 11 | 10 | _menu_item_object | custom |
| 12 | 10 | _menu_item_target | |
| 13 | 10 | _menu_item_classes | a:1:{i:0;s:0:"";} |
| 14 | 10 | _menu_item_xfn | |
| 15 | 10 | _menu_item_url | http://localhost/wordpress/ |
+---------+---------+-----------------------------+-----------------------------+

涉及到的一些基本的CRUD函数为:

  • add_post_meta()
  • get_post_meta()
  • update_post_meta()
  • delete_post_meta()

实际上,WordPress2.9之后,引入了register_post_type来创建新的Post Type。配合对元数据的操作函数,可以极大程度上模拟各种实体功能,从而避免创建新的的数据表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//...某插件提供的Post Type注册方法片段

register_post_type(
self::POST_TYPE,
array(
'labels' => array(
'name' => __(sprintf('%ss', ucwords(str_replace("_", " ", self::POST_TYPE)))),
'singular_name' => __(ucwords(str_replace("_", " ", self::POST_TYPE)))
),
'public' => true,
'has_archive' => true,
'description' => __("This is a sample post type meant only to illustrate a preferred structure of plugin development"),
'supports' => array(
'title', 'editor', 'excerpt',
),
)
);

一旦使用了自定义的Post Type来作为Entity,就可以使用WordPress自带的CRUD函数来操作:

  • wp_insert_post() #Create a new post (C).
  • get_post() #Retrieve a post (R).
  • wp_update_post() #Update an existing post (U).
  • wp_delete_post() #Delete a post (D).

当然,搭配到WordPress的钩子上,就更强大了。例如:

  • save_post # 当保存文章时被触发的钩子事件
  • save_post_{post_type} # WP3.7新增钩子,无须校验is_post_type($post_type)

Custom Taxonomy

WordPress默认内置了4种Taxonomies:

  1. Category #往往是写文章之前就预定义好的
  2. Tag #往往拥有多个tag,可在写文章时即时生成
  3. Link Category #往往只在内部使用,可用来categorize links
  4. Post Formats #meta信息,可以用来定制文章的呈现形式。

除此之外,WordPress还允许创建自己的taxonomies。

这种方式适合保存那些需要分门别类存放的数据,如用户信息、评论内容以及用户可编辑的数据等,特别适合于当你想要根据某个类型去查看相关的文章和数据的情况。

使用WordPress内置的其他数据表

WordPress为内置数据表提供了许多便利函数,比如Users数据表:

1
2
3
4
* `wp_create_user()`#Create a new user (C).
* `get_userdata()` #Retrieve a user’s data (R).
* `wp_update_user()`#Update an existing user (U).
* `wp_delete_user()`#Delete a user (D).

建立额外的数据表

除了使用现有的数据库模式之外,还可以添加自己的数据表。如著名的插件WooCommerce就添加了定制的数据表:

1
2
3
4
5
6
7
8
9
10
11
12
+-------------------------------------------------+
| Tables_in_wordpress |
+-------------------------------------------------+
| wp_woocommerce_api_keys |
| wp_woocommerce_attribute_taxonomies |
| wp_woocommerce_downloadable_product_permissions |
| wp_woocommerce_order_itemmeta |
| wp_woocommerce_order_items |
| wp_woocommerce_tax_rate_locations |
| wp_woocommerce_tax_rates |
| wp_woocommerce_termmeta |
+-------------------------------------------------+

create schema

update schema

日常操作

可借助于WPDB类全局类对象$wpdb提供的方法完成。

1
2
3
4
5
6
7
8
$wpdb->insert(
$table_name,
array(
'time'=>current_time('mysql'),
'name'=>$welcome_name,
'text'=>$welcome_text
)
);

Symfony Service Container

SOA(service-oriented architecture)是个比较有意思的想法。但是如何实例化这些服务类?直接在需要使用的地方实例化会产生严重的依赖,造成类与类之间的过度耦合。

更一般的讲,当需要在一个类C中使用另外一个类S对象的时候,OOP的新手常常会在类C中直接实例化类S的对象或者寻找S的类对象(可称之为依赖解析),从而产生了强烈的耦合关系。

IoC(Invention of Control)是一种能更好地解决这类问题的思路:

在C中只针对S声明一个接口对象,类C只负责接口编写逻辑代码,把对S的依赖解析过程从C中分离出去,交给第三方负责。

对比着两种思路,前者是C直接控制了S的实例化,然后再使用S;后者是只声明了SInterface接口对象,然后编写业务逻辑,并不负责S类依赖解析,甚至不管是不是S类(只针对接口编程),换言之,是把对S依赖解析的控制权交给了第三方去完成。

这就是所谓的控制反转。根据IoC原则,我们需要把对Service类依赖解析的控制权及义务交给第三方(即IoC Container)去完成,而具体服务的使用者只是负责服务的使用。

IoC有三种常见实现方案,工厂模式是非常蛋疼的方式,这里只专注于DI。

Dependency Injection

很显然,IoC这个概念本身是要求服务的使用方不要做依赖解析的事儿,因为依赖解析是IoC容器的职责。那么IoC容器是怎么实现依赖解析的?

考虑一个new新建对象的过程:PHP、Java之类的语言,要new一个类对象,最简单的大致都是采用类似于

1
$obj=new 类名(构造器的参数列表);

这种形式,而Python的语法则是:

1
obj=类名(构造器的参数列表)

抛开这些语言本身的语法差异不谈,这类new过程都会而且只会提供两个信息:

  1. 类名
  2. 构造器参数

也就是说,我们新建一个对象,只要一个类名和相应的构造器参数列表就够了。但是类名是一种硬编码,实现了同一个接口的不同实现会有不同的类名,为了屏蔽这种差异、消除过度耦合,我们可以对服务类绑定相关字符串,这样每次我们需要这个这个服务类对象的时候,只要利用这个字符串就可以了。

Symfony提供了DependencyInjection组件来负责依赖注入事宜。

对于一个类,

1
2
3
4
5
6
7
8
9
10
class Mailer{


private $transport;
public function __construct($transport){

$this->transport=$transport;
}

}

可以利用这样的方法进行获得其服务对象:

1
2
3
4
5
6
use Symfony\Component\DependencyInjection\ContainerBuilder;


$sc=new ContainerBuilder();
$sc->register('mailer',"Mailer")
->addArgument('sendmail');

对于已经注册为服务的类,可以用作其他服务的创建参数:

1
2
3
4
5
use Symfony\Component\DependencyInjection\Reference;


$sc->register("newsletter_mgr","NewsLetterMgr")
->addArgument(new Reference('mailer'))

除了构造器注入,还支持setter注入:

1
2
3

$sc->register('newsletter_mgr','NewsLetterMgr')
->addMethodCall('setMailer',array(new Reference('mailer')));

当然Symfony还支持Property injection,不过这可能带来一些问题。

通过容易获取注册的服务对象很简单,利用get()即可:

1
$svr=$sc->get('srv_name');

用配置文件定义依赖注入

很显然,容器的实现原理不局限于PHP这门语言,Java、Python或任意的支持OOP的语言都可以采用类似的思路。把以上DI用PHP代码实现并不是唯一可选方案,我们还可以使用跨语言的配置文件(如XML、YAML)来定义服务。当然,这样一来,我们就需要依赖Symfony/Config组件了。

表达上述DI信息的配置文件格式类似于;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
parameters: 
#...
mailer.transport: sendmail

services:
mailer:
class: Mailer
arguments: ["%mailer.transport%"]
newsletter_mgr:
class: NewsLetterMgr #contruction injection
calls: #setter injection
-[setMailer,["%mailer%"]]
properties: #perperty injection
#...

加载XML配置文件:

1
2
3
4
5
6
7
8
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
use Symfony\Config\FileLocator;


$sc=new ContainerBuilder();
$loader=new XmlFileLoader($sc,new FileLocator(__DIR__));
$Loader->load('services.xml');

加载YAML配置文件:

1
2
3
4
5
6
7
8
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Config\FileLocator;


$sc=new ContainerBuilder();
$loader=new YamlFileLoader($sc,new FileLocator(__DIR__));
$Loader->load('services.yaml');

如果确实想用PHP来创建服务:

1
2
3
4
5
6
7
8
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Config\FileLocator;


$sc=new ContainerBuilder();
$loader=new PhpFileLoader($sc,new FileLocator(__DIR__));
$Loader->load('services.php');

Symfony EventDispather

Event和EventDispatcher

Event是什么?

其实很简单,Event是用于描述“what happens?” 的东西。正因为此,Event对象中通常包含了一些数据,用来表示发生了什么。事实上,事件对象只是在描述发生了什么—准确的说,事件对象提供了一个接口,让别人知道发生了什么。

Symfony/EventDispather 提供了Event基础类来描述“What Happens”,这个基础类非常简单,只是对以下信息进行抽象:

  • Event调度器是谁?(dispatcher属性及getter/setter)
  • Event是否还能传播?(propagationStopped属性及getter/setter)
  • Event叫什么名字?(deprecated:name属性及getter/setter))

对于包含特殊信息的特定事件,可以继承自Event类,再添加自己对该类事件的特殊属性的抽象。比如:

1
2
3
4
5
6
7
8
9
10
11
12
class FilterOrderEvent extends Event{

protected $order;
public function __construct(){
$this->order=$order;
}

public function getOrder(){
return $this->getOrder();
}

}

这样,FilterOrderEvent事件除了包含Event基础类中那些对发生了什么的描述,还增加了对order这类信息的描述(提供了getOrder()接口让其他人获取Order对象)。

再比如,Symfony/Form组件中的FormEvent类,除了基础的Event属性外,还添加了两个额外属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
class FormEvent extends Event
{
private $form;
protected $data;

public function __construct(FormInterface $form, $data)
{
$this->form = $form;
$this->data = $data;
}

//....
}

一个是实现了FormInterface的表单对象$form,代表与当前事件相关的是哪个表单;一个是混合类型的$data参数,用于对特定数据的动态修改,如modelData之类。

很多时候,我们并不仅仅想知道发生了什么,我们还想根据发生了什么做出相应的动作。关于这一点,可以参见Wiki给出的Event定义:

In computing, an event is an action or occurrence detected by the program that may be handled by the program

这段话说明了一点很重要的东西:事件应当可以被检测、处理。通常由事件监听器(event listener)或者说事件处理器(event handle)来完成这一工作。

Symfony/EventDispather组件提供了这类对事件发布、调度、监听的功能。

EventDispather对象的dispatch()方法会根据某一个事件对应的Listeners,按优先级逐一进行调用。EventDispather支持两套风格事件处理程序的绑定。最常用的是addListener(),这是一种快速编码的回调风格,类似于JavaScript中的回调函数。还有一种使用Subscriber对象的风格。

使用具有回调风格的Listener

添加监听器的方法原型为:

1
addListener($eventName,$listener,$priority=0)

这一方法的关键特征是:传递给addListener的第二个参数是一个callable对象。这十分类似于JavaScript的回调函数:

1
2
3
4
5
6
7
8
9
10
11
12

$dispather->addListener(
'foo.action',
array($listener,'onFooAction') //a PHP callable
);

$dispather->addListener(
'bar.action',
function(Event $event){ //a PHP callable
//...
}
);

使用EventSubscriber对象

EventDispather还支持另外一种风格的监听绑定:

1
addSubscriber(EventSubscriberInterface $subscriber);

此方法接受一个实现了EventSubscriberInterface接口的对象作为参数。该接口非常简单:

1
2
3
4
interface EventSubscriberInterface
{
public static function getSubscribedEvents();
}

接口方法getSubscribedEvents()返回一个数组,该数组可以是三种方式中的一种:

  • array(‘eventName’ => ‘methodName’)
  • array(‘eventName’ => array(‘methodName’, $priority))
  • array(‘eventName’ => array(array(‘methodName1’, $priority), array(‘methodName2’))

addSubscriber()方法会自动检测接收到的$subscriber对象并解析出合适的方法名,再通过过addListener()方法完成事件绑定监听。其源码及实现分析如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

public function addSubscriber(EventSubscriberInterface $subscriber)
{
foreach ($subscriber->getSubscribedEvents() as $eventName => $params) {

//$subscripter->getSubscribedEvents 返回的数组类似于:
// array(
// 'eventName'=>"methodName",
// //...
// )
if (is_string($params)) {
$this->addListener($eventName, array($subscriber, $params));
}

//$subscripter->getSubscribedEvents 返回的数组类似于:
// array(
// 'eventName'=>array("methodName",$priority),
// //...
// )
elseif (is_string($params[0])) {
$this->addListener($eventName, array($subscriber, $params[0]), isset($params[1]) ? $params[1] : 0);
}

//$subscripter->getSubscribedEvents 返回的数组类似于:
// array(
// 'eventName'=>array(
// array("methodName1",$priority)
// array("methodName22")
// ),
// //...
// )
else {
foreach ($params as $listener) {
$this->addListener($eventName, array($subscriber, $listener[0]), isset($listener[1]) ? $listener[1] : 0);
}
}
}
}

可以看到,addSubscriber()始终都是把形如array($subscriber,$methodName)这样的callable传递给addListener。显而易见,一个Subscrber应该有如下的类似形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class StoreSubscriber implements EventSubscriberInterface{

public static function getSubscribedEvents(){
return array(
'kernel.repsonse'=>array(
array('onKernelResponsePre',10),
array('onKernelResponseMid',5),
array('onKernelResponsePost',0),
),
'store.order'=>array('onStoreOrder',0)
);
}


public static function onKernelResponsePre{
//...
}

public static function onKernelResponseMid{
//...
}

public static function onKernelResponsePost{
//...
}

public static function onStoreOrder{
//...
}
}

这种十分类似于Java Swing中事件监听接口,比如MouseListener类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
button.addMouseListener(new MouseListener() {

@Override
public void mouseClicked(MouseEvent e) {
System.out.println("mouseClicked");
}

@Override
public void mousePressed(MouseEvent e) {
System.out.println("鼠标被按住");
}


@Override
public void mouseReleased(MouseEvent e) {
System.out.println("鼠标被释放");

}

@Override
public void mouseEntered(MouseEvent e) {

System.out.println("鼠标被进入");
}

@Override
public void mouseExited(MouseEvent e) {
System.out.println("鼠标被退出");
}
});

Symfony中的EventSubscriber对象为一组事件监听器提供了良好的组织形式,使得代码更具有可读性。

Symfony Translation

翻译的过程可以理解为从消息message到译文translation的过程。Symfony/Translation这个组件的工作流程大致可以分为三步:

  1. 创建翻译器
  2. 为翻译器添加资源: sourcetarget的消息映射关系
  3. 根据domainlocale及对应的message给出译文translation

创建翻译器

翻译器的创建非常简单,仅需提供一个默认的locale值:

1
2
3
use Symfony\Component\Translation\Translator;

$translator=new Translator('fr_FR'); #默认locale='fr_FR'

所谓locale,大致可以当成一个指代用户语言和国家/地区的字符串。推荐用
ISO639-1LanguageCode_ISO3166-1Alpha-2CountryCode>
这样的格式来表示。比如:fr_FR

翻译资源的加载

Translation Resources

Translation Resources定义了一组从源(source)到目标target的消息映射关系。这种映射关系可以用不同的形式来表达,比如XML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" datatype="plaintext" original="file.ext">
<body>
<trans-unit id="1">
<source>Symfony is great</source>
<target>J'aime Symfony</target>
</trans-unit>
<trans-unit id="2">
<source>symfony.great</source>
<target>J'aime Symfony</target>
</trans-unit>
</body>
</file>
</xliff>

又如YAML:

1
2
Symfony is great: J'aime Symfony
symfony.great: J'aime Symfony

再如PHP的Array:

1
2
3
4
return array(
'Symfony is great' => 'J\'aime Symfony',
'symfony.great' => 'J\'aime Symfony',
);

当然,还支持JSON等其他形式——只要他们表达了这种映射关系,即可以被当做一个Translation Resources。

消息占位符

上面例子中的%...%这种表达(如%name%)是一种消息占位符。这种占位符机制可以动态映射一些message到translation。

复数形式

借助占位符和管道符可以很好的表达复数形式。

1
'There is one apple|There are %count% apples'

有时候我们需要借助于区间获取更多的控制:

‘{0} There are no apples|{1} There is one apple|]1,19] There are %count% apples|[20,Inf[ There are many apples’

区间表达遵循ISO 311-11的记法规则,可以是两端(可以借助于-Inf和+Inf表达正负无穷)之间的数字,还可以是有限个数字的集合如:

1
{1,2,3}

资源加载的过程:

  1. 设置资源加载器
  2. 利用资源加载器加载资源

Symfony支持不同的资源加载方法,:

  • ArrayLoader
  • CsvFileLoader
  • IcuDatFileLoader
  • PhpFileLoader
  • XliffFileLoader
  • JsonFileLoader
  • YamlFileLoader

所有的加载器都实现了LoaderInterface接口,其load()方法返回一个catalog以用作将来的翻译。

我们还可以设置定义自己的资源类型,比如采用

1
(source)(target)

这种形式,只要我们为之定义加载器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use Symfony\Component\Translation\MessageCatalogue;
use Symfony\Component\Translation\Loader\LoaderInterface;

class MyFormatLoader implements LoaderInterface
{
public function load($resource, $locale, $domain = 'messages')
{
$messages = array();
$lines = file($resource);

foreach ($lines as $line) {
if (preg_match('/\(([^\)]+)\)\(([^\)]+)\)/', $line, $matches)) {
$messages[$matches[1]] = $matches[2];
}
}

$catalogue = new MessageCatalogue($locale);
$catalogue->add($messages, $domain);

return $catalogue;
}

}

加载Translation Resources的示例代码为:

1
2
3
4
$translator->addLoader('xlf',new XliffFileLoader());
$translator->addResource('xlf','message.fr.xlf','fr_FR'); # 默认domain为'messages'
$translator->addResource('xlf','message.fr.xlf','fr_FR','admin');
$translator->addResource('xlf','navigation.fr.xlf','fr_FR','navigation');

翻译过程

翻译实际上是分成两步完成的:

  1. 从translation resources中加载翻译好的message一览表(catalog)
  2. 从catalog中定位message并返回对应的翻译。如果定位不到,则返回原始message。

可以通过调用ITranslator接口提供两个关键的方法trans()或者transChoice()来执行这个过程。

1
2
public function trans($id, array $parameters = array(), $domain = null, $locale = null);
public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null);

如果不提供locale,trans()方法在默认情况下会使用fallback的locale,

1
2
3
4
5
6
7
8
9
$translator->trans('hello, %name%',array('name'=>'Chicago'),'admin','fr_FR');

$translator->transChoice(
'{0} There are no apples|{1} There is one apple|]1,Inf[ There are %count% apples',
10,
array('%count%' => 10),
'messages',
'fr_FR'
);

例子

来自官方文档的一个例子

1
2
3
4
5
6
7
8
9
10
use Symfony\Component\Translation\Translator;
use Symfony\Component\Translation\Loader\ArrayLoader;

$translator = new Translator('fr_FR');
$translator->addLoader('array', new ArrayLoader());
$translator->addResource('array', array(
'Symfony is great!' => 'J\'aime Symfony!',
), 'fr_FR');

var_dump($translator->trans('Symfony is great!'))

在Symfony框架中使用翻译组件

Symfony框架集成了翻译功能,

翻译资源的位置在以下位置寻找(按照以下优先级):

  • app/Resources/translations
  • app/Resources/<bundle name>/translations
  • Path/to/SomeBundle/Resources/translations/

翻译资源文件的命名必须遵循这样的规则:

domain.locale.loader

domain是可选项;Symfony自带的loader包括xlf、php、yml等。例如:FOSUserBundle.zh_CN.ymlvalidators.en.yml等。

设想用户的localefr_FR,当要翻译“Symfony is great” 时,会按照以下顺序寻找:

  1. 尝试寻找fr_FR对应的翻译资源,例如messages.fr_FR.xlf;
  2. 如果第一步没找到,则会寻找fr对应的翻译资源,例如messages.fr.xlf;
  3. 如果还没找到,则使用fallbacks对应的资源。

在Symfony框架集成的Twig模板中使用翻译组件

绝大部分时候,我们都是在在Symfony框架的Twig中使用翻译组件。

Twig提供了tags和filters支持翻译功能:

tags:

1
2
3
4
5
6
7
8
9
{% trans with {'%name%': 'Fabien'} f
rom "app" %}Hello %name%
{% endtrans %}
{% trans with {'%name%': 'Fabien'} from "app" into "fr" %}
Hello %name%
{% endtrans %}
{% transchoice count with {'%name%': 'Fabien'} from "app" %}
{0} %name%, there are no apples|{1} %name%, there is one apple|]1,Inf[ %name%, there are %count% apples
{% endtranschoice %}

filters:

1
2
3
4
{{ message|trans }}
{{ message|transchoice(5) }}
{{ message|trans({'%name%': 'Fabien'}, "app") }}
{{ message|transchoice(5, {'%name%': 'Fabien'}, 'app') }}

Symfony配置

运行环境

如前所述,Symfony自带三种运行环境,prod、dev、和test。
AppKernel会根据当前运行环境自动加载位于app/config/下的对应的配置文件,如

  • config_dev.yml
  • config_prod.yml
  • config_test.yml

这些配置文件的加载规则都非常简单,仅仅只是通过拼凑出config_<environment>.yml这样的配置文件名来实现的:

1
2
3
4
5
# app/AppKernel.php 

public function registerContainerConfiguration(LoaderInterface $loader) {
$loader->load($this->getRootDir().'/config/config_'.$this->getEnvironment().'.yml');
}

配置文件的格式

Symfony支持各类配置格式,默认采用YAML。在YAML中,采用如下约定:

  1. imports: 类似于PHP的include,确保要导入的配置文件被优先加载。
  2. 顶级entry: 为特定的bundle定义配置。

借助于imports可以导入基准的config.yml文件,然后对部分需要修改的部分entry进行覆写操作。

比如:config_dev.yaml结构为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
imports:
- { resource: config.yml }

framework:
router:
resource: "%kernel.root_dir%/config/routing_dev.yml"
strict_requirements: true
profiler: { only_exceptions: false }

web_profiler:
# ...

monolog:
# ...

assetic:
# ...

#swiftmailer:
# delivery_address: me@example.com

事实上,Symfony的配置文件都采用了类似的结构。通过这样的导入-覆写操作,可以保持代码独立、避免重复代码(DRY原则)。

在命令行下可以用

1
app/console config:dump-reference BundleName

导出某一个Bundle默认的配置。

定义和处理配置文件

使用TreeBuilder定义Configuration Vaules的等级结构

TreeBuilder实例是被一个实现了ConfigurationInterface接口的定制的Configuration类返回的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
namespace Acme\DatabaseConfiguration;

use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;

class DatabaseConfiguration implements ConfigurationInterface
{
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder();
$rootNode = $treeBuilder->root('database');

// ... add node definitions to the root of the tree

return $treeBuilder;
}
}

向Tree中增加节点定义

NodeType

  • scalar # 通用类型,包括了booleans,strings,intergers,floats,和null
  • boolean
  • interger
  • float
  • enum # 有限集
  • array
  • variable # 无验证

Numeric Node 约束

提供了min()max()

1
2
3
4
5
6
7
8
9
10
11
12
13
$rootNode
->children()
->integerNode('positive_value')
->min(0)
->end()
->floatNode('big_value')
->max(5E45)
->end()
->integerNode('value_inside_a_range')
->min(-50)->max(50)
->end()
->end()
;

Enum Node

1
2
3
4
5
6
7
$rootNode
->children()
->enumNode('gender')
->values(array('male','female'))
->end()
->end()
;

Array Node

用以描述更深层次的等级结构,rootNode也算是这样一种数组结构:

1
2
3
4
5
6
7
8
9
10
11
12
$rootNode
->children()
->arrayNode('connection')
->children()
->scalarNode('driver')->end()
->scalarNode('host')->end()
->scalarNode('username')->end()
->scalarNode('password')->end()
->end()
->end()
->end()
;

在定义arrayNode的children之前,可以使用以下选项:

  • useAttributeAsKey()
  • requireAtLeastOneElememt()
  • addDefaultsIfNotSet()

默认值和必填值

对于所有的node types,都可以设置默认值,并当一个节点有某些特定值的时候替换alues。

  • defaultValue()
  • isRequired()
  • cannotBeEmpty()
  • default*() #(null,true,false) defaultValue()的shortcut
  • treat*Like() #(null,true,false) 当值是*的时候进行替换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

$rootNode
->children()

->arrayNode('connection')
->children()

->scalarNode('driver')
->isRequired()
->cannotBeEmpty()
->end()

->scalarNode('host')
->defaultVaule('localhost')
->end()

->scalarNode('username')->end()
->scalarNode('password')->end()

->booleanNode()
->defaultFalse()
->end()

->end()
->end()


->arrayNode('settings')
->addDefaultsIfNotSet()
->children()
->scalarNode('name')
->isRequired()
->cannotBeEmpty()
->defaultValue('value')
->end()
->end()
->end()

->end()
;

Documenting the option

info()方法可以用来生成文档。config:dump-reference

appending sections

如果有一个复杂的配置,Tree可能变得非常大,你或许想要把它分割成多个部分。你可以把一个section作为独立的节点append到main tree中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public function getConfigTreeBuilder(){

$TreeBuilder=new TreeBuilder;
$rootNode=$treeBuilder->root('database');


$rootNode
->children()
->arrayNode('connection')
->children()
->scalarNode('driver')
->isRequired()
->cannotBeEmpty()
->end()
->scalarNode('host')
->defaultValue('localhost')
->end()
->scalarNode('username')->end()
->scalarNode('password')->end()
->booleanNode('memory')
->defaultFalse()
->end()
->end()
->append($this->addParametersNode())
->end()
->end()
;
return $treeBuilder;
}

public function addParametersNode(){

$builder = new TreeBuilder();
$node = $builder->root('parameters');

$node
->isRequired()
->requiresAtLeastOneElement()
->useAttributeAsKey('name')
->prototype('array')
->children()
->scalarNode('value')->isRequired()->end()
->end()
->end()
;

return $node;
}

Normalization

标准化过程(normalization process)用于消除那些由于不同格式造成的不同,主要是YAML和XML。

在YAML中,keys中的典型分隔符是_ ,而XML中的是 - ,例如 在YAML中auto_connect 在XML中是 auto-connect
normalization 会把这些都变成auto_connect.

另外一个YAML和XML之间的区别是数组取值的表示方法。

YAML:

1
2
twig:
extensions: ['twig.extension.foo', 'twig.extension.bar']

XML:

1
2
3
4
<twig:config>
<twig:extension>twig.extension.foo</twig:extension>
<twig:extension>twig.extension.bar</twig:extension>
</twig:config>

这种复数的区别可以通过fixXmlConfig来消除:

1
2
3
4
5
6
7
8
$rootNode
->fixXmlConfig('extension')
->children()
->arrayNode('extensions')
->prototype('scalar')->end()
->end()
->end()
;

Symfony 模板系统

Template的逻辑命名和Controller的类似,模板的逻辑名称遵循这样的约定:

BundleName:ControllerName:TemplateName

一般会被映射会这样的物理地址:

  1. app/Resources/{BundleName}/views/ControllerName/TemplateName
  2. {path/to/BundleName/}Resources/views/ControllerName/TemplateName

当第1个地址找不到对应的模板时,会继续查找第2个位置的模板。

Template Services

Symfony模板系统的核心是模板引擎(服务),

从控制器渲染模板:

1
return $this->render('article/index.html.twig');

与直接使用服务是等价的:

1
2
3
4
use Symfony\Component\HttpFoundation\Response;
$engine=$this->container->get('templating');
$content=$engine->render('article/index.html.twig');
$return $response=new Response($content);

Symfony的模板引擎可以在app/config/config.yml中配置:

1
2
3
framework: 
#...
templating: { engines: ['twig']}

Twig模板

Twig模板的语法和Django模板语法非常相似。Twig提供了三种语法:

  1. says sth

    1
    {{ ... }}
  2. does sth

    1
    2
    // does sth
    {% ... %}
  3. comment sth

    1
    2

    {# ... #}

Twig链接:

1
2
3
<a href="{{ path(routeName,context) }} " > home </a>
<img src="{{ assets(images/logo.png) }}"/>
<link href="{{ assets(css/blog.css) }}" rel='stylesheet' type='text/css' />

Twig filters:

1
{{ title|upper }}

Twig模板嵌入

为了代码复用,Twig提供了include:

1
{% include() %}

Twig模板继承与重载:

1
2
3
4
5
6
{% extends 'baseTemplateName' %}

{% block XX %}
{{ parent() }}
{# overwrite here #}
{% endblock %}

一种常用的模板继承是三层方案。

  1. 为整个网站创建基础模板app/Resources/views/base.html.twig
  2. 为某一类特定功能创建模板spec/layout.html.twig(继承自第1层模板)
  3. 为每一个单独的页面创建模板 (继承自第2层模板)

Twig模板嵌入其他控制器

此外,还可以嵌入其他控制器的渲染结果

1
{{ render(Controller("LogicalContrllerName",context)) }}

配合hinclude.js,还可以实现异步加载:

1
2
{{ render_hinclude(controller('...')) }}
{{ render_hinclude(url('...')) }}

Twig Template转义

twig系统自带转义,如需原始输出,可以利用raw 过滤函数

1
{{ article|raw  }}

PHP模板,可以使用

1
<?php echo $view->escape($name)?>

进行转义。

Twig Macro

Twig Macro是非常强大的HTML代码复用手段,它的功能非常类似于C语言的宏:

1
2
3
{% macro input(name, value = "", type = "text", size = 20) %}
<input type="{{ type }}" name="{{ name }}" value="{{ value|e }}" size="{{ size }}" />
{% endmacro %}

Twig Macro可以在其他单独的文件中定义,然后导入到当前的模板文件中:

1
2
3
4
5
6
7
{# 导入整个宏定义文件 #}
{% import "forms.html" as forms %}
{{ forms.input('username') }}

{# 从宏文件中导入某个单独的Macro #}
{% from 'forms.html' import input as input_field %}
{{ input_field('username') }}

Twig Use Statement

Twig的模板继承只支持单继承,Twig还提供了use以帮助我们实现更大程度的代码复用。

use语句告诉Twig去把在某个文件中定义的block块导入到当前模板中。

1
{% use "blocks.html" %}

这一功能类似于对于Macro的import语句,但是use只对block块有效,而且,想use的模板必须满足

  1. 不extends其他模板。
  2. 不定义宏
  3. body为空

和针对Macros的import类似,use也支持了导入一部分代码段的功能,同时还提供别名机制来避免命名冲突:

1
2
3
4
5
6
7
{% extends "base.html" %}

{% use "blocks.html" with sidebar as base_sidebar, title as base_title %}

{% block sidebar %}{% endblock %}
{% block title %}{% endblock %}
{% block content %}{% endblock %}

Template的全局变量

不管是Twig模板,还是纯PHP模板,Symfony都为之提供了一个变量app

  • app.security #deprecated since 2.6
  • app.user
  • app.request
  • app.session
  • app.environment
  • app.debug

也说Python对象、函数参数的传递方式与闭包

关于Python里闭包概念,查了很多中文网站,发现很多人的理解有失偏颇,比如《Python参考手册》第四版 ,比如http://blog.csdn.net/marty_fu/article/details/7679297。——他们认为子函数对与外围变量同名的变量修改不能影响外围。鉴于我不认同他们的观点,自己从头理了下对象的概念。本文第3.1部分,那里将对网上和一些教材的观点进行勘误。

凡下所述,悉皆胡扯,人生百味,聊以自娱耳。

对象可变之禅

起这个小标题可能多少有故弄玄需之嫌,然而Levy(1984)黑客准则第一条就明确提出

Access to Computers - and anything which might teach you something about the way the world works – should be unlimited and total

程序的运作和这个世界的运作方式是统一的。

变,或者不变

在父母长辈看来,对象就是另一半;在Pythoner看来,万物皆对象,对象或止或动,必居二者其一。

在许多编程语言里,比如Python,比如C#,人们把对象不可变叫immutable,例如int类对象、string类对象、etc;可变叫mutable,例如list对象、自定义的Class对象、etc。对象是否是mutable,很重要——To be,or not ,that is a question。

在一般的面向对象的语言里,比如C++,对象必须经过实例化才会存在,实例化的过程是从类型的精神抽象到具体对象的过程,这一过程会分配一段内存来表示对象。
在Python中万物皆对象,类也是对象。问题是,类创建对象,类也是对象,类由什么创建——答案是类由元类创建。关于这一点,具体请参见stackoverflow上e-satis对一篇《What is a metaclass in Python?》的提问的回复

不管怎么说,对象依托内存而存在。对于一个对象,一旦被分配了一段内存,即代表其生命周期开始,这段内存的内容在对象生存期间不可变化的即是immuable对象,反之,则称之为mutable。

对象本无名

对象标识符——或者叫对象的称谓,可以简单当成是程序员为了表示晦涩难记的内存地址的精神创造——在Python的世界里,对象名表示用来指代对象,仅此而已,对象名是彻彻底地的人为的附加到对象上的东西。

一个对象本身并没有名字,只是后来这个世界有了OOP程序员,对象这才有了名字;即使你要称呼他,你也可以按你的意愿叫她女朋友,叫honey,darling,或者妖魔仙佛、洪水猛兽,叫什么的权利在你——但是,这只是你附加给这个对象的,并不影响对象自身。

对象原本并没有名字——注意措辞“原本”两个字,因为现在我们会强加给对象一个名字——理解这点很重要。

名字只是代号,正如同有人有姓名、乳名、昵称、绰号,这些不同的代号可能是同一个人;另一种情况是,尽管我们提倡在给对象命名的啥时候尽量做到“望文生义”,但是没人能阻止我们起个不太合适的名字——正如同如花和秋香从字面意义上来说都是很美的名字,但是,唐伯虎在没见过这两个人的时候是无法仅凭名字就分别出谁是美女的,因为对象名和对象的固有属性无关。

mutable对象——名字只是代号

同一段内存,同一个对象

假定如花的真正闺名叫aList,而秋香则叫bList:

1
2
3
4
5
6
7
#同一段内存,便是同一个对象
aList=[1,2,3,4,5] #首先我们有一个list对象[1,2,3,4,5],嗯,就叫它aList
bList=aList # 然后我再给他起个名字,叫bList
bList.append(6) #在bList追加一个6
print("aList is :",aList)
print("id of aList is :",id(aList))
print("id of bList is :",id(bList))

输出结果类似于:

1
2
3
aList is : [1, 2, 3, 4, 5, 6]
id of aList is : 3068653132
id of bList is : 3068653132

很好理解,bList,aList都是我们对于[1,2,3,4,5]这一同一个列表对象的称呼,我们改变bList和改变aList效果是一样的。对于mutable对象名a,赋值给另一个对象名b,则意味这让b指代a所指代的对象——两个名称对应于同一个对象id。
ps:如果你Class了一Beauty类,qiuXiang和ruHua都是Beauty类对象,并在执行上面类似的操作,会得到类似的结果,因为Class出来的类型也是mutable。

一元操作符、二元操作符、以及属性方法对对象地址的影响

上面是以list类对象的append()方法进行分析的,调用前后,mutable对象的地址未发生变化。于是,我以前想当然认为mutable对象在诸如aList=aList+bList也不会发生变化,但是经过一次偶然的测试发现,事实真不是这样。

1
2
3
4
5
6
7
8
9
10
11
12
#测试不同的修改方式对对象地址造成的影响
cList=["1","2"] #cList是个list对象,下面会用不同的修改方式对其进行测试
dList=["Attention!"] #用于追加到cList后面的list对象
eList=["ATTENTION!"] #用于追加到cList后面的list对象
fStr="Yeah"#用于追加到cList后面的list对象
print("cList is : ",cList,",id is ",id(cList)) #修改之前的cList信息
cList=cList+dList
print("cList is : ",cList,",id is ",id(cList)) #以二元操作修改后的cList信息
cList+=eList
print("cList is : ",cList,",id is ",id(cList)) #以一元操作修改后的cList信息
cList.append(fStr)
print("cList is : ",cList,",id is ",id(cList)) #以对象方法修改后的cList信息

输出结果类似于

1
2
3
4
5
cList is : ['1', '2'] ,id is 3068251724
cList is : ['1', '2', 'Attention!'] ,id is 3068248844
cList is : ['1', '2', 'Attention!', 'ATTENTION!'] ,id is 3068248844
cList is : ['1', '2', 'Attention!', 'ATTENTION!', 'Yeah'] ,id is 3068248844
`

结果分析:一个list,用二元操作符修改,虽然修改前后都叫同一个名字,但是返回的地址变了;用一元操作符是在原地址上进行,返回地址不变,还是同一对象;以append()方法返回的list还是原对象。

imutable对象——去年今日此门中,人面桃花相印红:

immutable对象最大的特点就是不变。但是我刚接触这个的时候很疑惑,因为我发现:

一个叫a_int的int变量确实可以赋值为1,再赋值为2,而且没有语法错误提示!

当时是在C#里被这个问题困惑住的,然后自己实践后分析结果就恍然大悟 。
在Python里,不妨分析这一个例子:

1
2
3
4
5
6
7
8
9
aInt=1#首先我们有一个int对象,嗯,就叫它aInt
bInt=aInt # 然后我再给他起个名字,叫bInt
print("before modified:")
print("\taInt is :",aInt,"id of aInt is :",id(aInt))
print("\tbInt is :",bInt,"id of bInt is :",id(bInt))
bInt=4#修改bInt
print("after modified:")
print("\taInt is :",aInt,"id of aInt is :",id(aInt))
print("\tbInt is :",bInt,"id of bInt is :",id(bInt))

输出类似于:

1
2
3
4
5
6
before modified:
aInt is : 1 id of aInt is : 137396000
bInt is : 1 id of bInt is : 137396000
after modified:
aInt is : 1 id of aInt is : 137396000
bInt is : 4 id of bInt is : 137396048

这一过程中,我首先让bInt=aInt,然后我改变试图bInt指代的对象的值为4,但是bInt指代的是个immutable对象,怎么办?为了最大限度满足我的需求,系统就生成了一个新的int对象,值为4,然后用bInt指代了这个新生成的值为4的int对象。和修改值之前的bInt相比,二者虽然都叫做bInt,但是已经不是指代的同一个对象了(可以看到,二者的id发生了变化)。

同样的环境,同样的名字,只是对象已经不是以前的对象了——去年今日此门中,人面桃花相映红。

这个过程用充满哲学意味的话可以表述为现在的你已然不是昨天的你.《金刚经》通篇都充斥这种思想。

ps:大约是受奶茶5月新出的专辑《亲爱的路人》影响吧,这几句话现在说起来多少有些感慨。多年前,写了一个偈子:佛即非佛,我亦非我,不见真相,不见真我。只言片语,权作掩耳。

值传递,还是引用传递

函数参数传递方式对于C/C++者来说,容不得半点模糊。Python里,这也是个重要的问题。不过经过本文第一部分Python里对象可变之禅的分析,函数参数传递方式非常容易理解。这里仅仅以一小段测试代码简单分析下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def ChangeValue(aInt,bList):
#测试改变参数
aInt+=1
bList.append("Someone remains in yr heart forever")

aInt=1
bList=["someone comes","someone goes"]

print("before calling")
print("\taInt is : ",aInt)
print("\tbList is : ",bList)

ChangeValue(aInt,bList)
print("after calling")
print("\taInt is : ",aInt)
print("\tbList is : ",bList)

输出类似为:

1
2
3
4
5
6
before calling
aInt is : 1
bList is : ['someone comes', 'someone goes']
after calling
aInt is : 1
bList is : ['someone comes', 'someone goes', 'Someone remains in yr heart forever']

  • 对于aInt,由于是不可变变量,在ChangeValue函数内部,会重新生成一个新的aInt;外围变量未收到影响
  • 对于bList,由于是可变变量,在ChangeVaule函数内部,改变以bList名字命名的对象

但是,其中奥妙仅此而已嘛?

函数对外围变量的影响

局部变量对外围变量的影响

上面是通过参数传递给函数的,要是函数直接调用外部变量会有什么后果?很多人说函数对外围同名函数操作没有直接影响,这中理解是不完全正确的。

在函数内部,如果一个对象先以先左值出现,且没有用“.”指定,则系统会试图优先把它解释为在函数内部的局部变量,以相应的语句对其初始化。显然,正常情况下,局部变量不会影响外围变量。

但千万不要以为非左值出现的对象就是右值,就不能改变原对象。因为左值、右值都是针对赋值而言的。要修改一个对象,除了赋值,还可以利用某些非赋值表达式。有两个典型反例就是,

  • 一个对象或许可以通过一元操作符修改自身,
  • 一个对象可以通过调用相关方法修改自身。(参见本文1.2.1的第二部分)

对于一个局部变量,即使有与其同名的外围变量,这二者也毫无关系。不要因为存在了外围同名变量的初值而忽视局部变量的初始化。

总而言之,笼统的说子函数的变量变化不影响外围变量是不负责任的。

假定某个子函数的某一个变量,在函数之外,还存在一个同名的外围变量:

  • 如果它是immutable,不管它是不是以左值出现,都能肯定它和外围变量指代的并不是同一个对象;
  • 而如果这是个mutable变量,则这二者可能是或者可能不是是同一对象——这取决于这个子函数之中,是否有这样一种情况:这个对象以某种方式,修改一个新对象到原来对象名上。
    网上一些技术贴的作者认为:
    如果子函数里那些和外围变量同名的变量的发生变化,结果并不会改变外围变量
    其实这句话是有失偏颇的。测试代码与分析见下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

def ParentFoo():
''''测试子函数对父函数的局部变量的影响父函数有两个对象, aInt:int类对象;aList:list类对象。'''
aInt=1234567
aList=["a","b","c","d"]
print("the ParentFoo Before Calling SubFoo : ")
print("\taInt is ",aInt,",id is ",id(aInt))
print("\taList is ",aList,",id is ",id(aList))

def SubFoo():
' ''子函数,用于测试子函数里局部变量修改对外围变量的影响 '''
print("Calling SubFoo : ")#提示子函数已经开始调用

#Step1:测试对父函数里的aInt的影响
print("\tStep1:测试子函数对aInt的影响")
#下面建立一个局部变量aInt,同时初始化
#如果没有这一步,则后面aInt不能以左值出现!!
aInt=1234567#这个局部变量和外围变量同名并不会造成任何影响
print("\t\taInt is ",aInt,",id is ",id(aInt))
aInt+=1
print("\t\taInt is ",aInt,",id is ",id(aInt))

#Step2:测试对父函数里的aList的影响
print("\tStep2:测试子函数对aInt的影响")
#很多人——很多中文网站贴出来的技术贴的作者认为:
# 如果子函数里那些和外围变量同名的变量的发生变化,结果并不会改变外围变量
#其实这句话是有失偏颇的。
#反例:以非左值出现,同时调用其能修改自身的方法
#这种修改外围变量的操作方式很隐蔽!很变态!很危险!
aList.append("e") #子函数在没有以参数传递的情况下偷偷改变了外围变量!
print("\t\taList is ",aList,",id is ",id(aList))
bList=aList #建立一个局部变量——但实际上此时这两个变量指代的是同一个对象
print("\t\tbList is ",bList,",id is ",id(bList))
bList=bList+["Attention!"] #注意id(bList)变化了!
print("\t\tbList is ",bList,",id is ",id(bList))

SubFoo() #调用子函数
#子函数调用结束后打印父函数里相关对象的信息
print("the ParentFoo After Calling SubtFoo : ")
print("\taInt is ",aInt,",id is ",id(aInt))
print("\taList is ",aList,",id is ",id(aList))

ParentFoo() #调用父函数

输出类似于:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
the ParentFoo Before Calling SubFoo :
aInt is 1234567 ,id is 3069037696
aList is ['a', 'b', 'c', 'd'] ,id is 3068358220
Calling SubFoo :
Step1:测试子函数对aInt的影响
aInt is 1234567 ,id is 3068098400
aInt is 1234568 ,id is 3068054080
Step2:测试子函数对aInt的影响
aList is ['a', 'b', 'c', 'd', 'e'] ,id is 3068358220
bList is ['a', 'b', 'c', 'd', 'e'] ,id is 3068358220
bList is ['a', 'b', 'c', 'd', 'e', 'Attention!'] ,id is 3068363564
the ParentFoo After Calling SubtFoo :
aInt is 1234567 ,id is 3069037696
aList is ['a', 'b', 'c', 'd', 'e'] ,id is 3068358220

闭包

关于闭包,cnbolgs的Vamei老兄的说法很透彻了,我就不狗尾续貂了,但是出于知识完备、日后备忘和便于查找的原因,把关键部分粘贴到这里。本部分定义和例子引自http://www.cnblogs.com/vamei/archive/2012/12/15/2772451.html,略有删改。
一个函数和它的外围变量合在一起,就构成了一个闭包(closure)。在Python中,所谓的闭包是一个包含有外围变量取值的函数对象。外围变量取值被保存在函数对象的closure属性中。
例如:

1
2
3
4
5
6
7
8
9
10
def line_conf():
b = 15
defline(x):
return2*x+b
return line # return a function object

b = 5
my_line = line_conf()
print(my_line.__closure__)
print(my_line.__closure__[0].cell_contents)

输出类似于:

1
2
(<cell at 0xb6df2764: int object at 0x8308000>,)
15

一个实际例子:

1
2
3
4
5
6
7
8
def line_conf(a, b):
defline(x):
return a*x + b #原文误作return ax+b
return line

line1 = line_conf(1, 1)
line2 = line_conf(4, 5)
print(line1(5), line2(5))

输出结果类似于:

1
6 25

如果没有闭包,我们需要每次创建直线函数的时候同时说明a,b,x。实际上,利用闭包,我们创建了泛函。这个函数的一些方面已经确定(比如必须是直线形式),但另一些方面(比如a和b)参数待定。随后,我们根据line_conf传递来的参数,通过闭包的形式,将最终函数确定下来。

(snplou注:更一般的,如果有一类性质相似的动作,其中每一个动作都需要共N个参数,我们可以考虑建立一个泛函——动作配置函数用以表示和生成这一类动作,不妨暂时称之为ActConfig,首先我们需要从动作需要的N个参数里选取K个作为动作配置函数的参数,记作ActConfig(p1,p2,…,pK),而把剩下的N-K个参数放到动作配置函数的子函数Act的参数里,记作Act(pK_1,pK_2,…,pN_K),并子函数Act()里规定对这N 个参量的操作行为,而父函数则返回子函数,这样每次指定p1,p2,…,pK后,都会生成一个新的Act(pK_1,pK_2,…,pN)函数。实际执行某一个具体动作的时候,只需调用Act(pK_1,pK_2,…,pN)即可)

闭包有效的减少了函数所需定义的参数数目。这对于并行运算来说有重要的意义。在并行运算的环境下,我们可以让每台电脑负责一个函数,然后将一台电脑的输出和下一台电脑的输入串联起来。最终,我们像流水线一样工作,从串联的电脑集群一端输入数据,从另一端输出数据。这样的情境最适合只有一个参数输入的函数。闭包就可以实现这一目的。
另外,闭包还能延迟计算。

结论

当你希望修改外围变量,在3.x里,最好显式的用nonlocal声明。尽量不要用本文测试代码里涉及的方法。
而如果你不希望修改外围变量:
尽量不要给子函数直接传递mutable对象参数;
也尽量不要在子函数里直接操作与外围变量同名的mutble对象——即使有,尤其需要注意某些一元操作符和某些特殊的能返回原地址对象的方法,因为这非常可能会在你不注意之中修改外围变量。

WordPress Custom Post Type

WordPress自带了5种Post Type:

  1. Post
  2. Page
  3. Attachment
  4. Revision
  5. Nav Menus

对于一个基本的博客,这些类型足以应付大多数需求。但是当情况复杂后,就需要定制自己的Post Type了。

Custom Post Type

定制的Post Type可能是任何东西,不仅仅是面向公众的内容碎片。比如,可以定制自己的Post Type来跟踪程序中错误。

WordPress提供的Custom Post Type功能使得WP的Post足以模拟任何实体——“The only limitation is your imagination”。

注册定制的Post Type

注册新的Post Type类型可以用以下函数实现:

1
2

register_post_type($post_type,$args_array);

一般都在init时进行注册:

1
2
3
4
5
6
7
8
9
add_action('init',function(){
register_post_type(
'mytesttype',
array(
'labels' => array( 'name' => 'MyTester' ),
'public' => true,
)
);
});

定制Post Type参数

函数register_post_type()的第二个参数提供了众多定制Post Type的选项:

  • public # 前台是否公众可见?默认为false
  • show_ui # 是否创建默认UI?默认为public定义的值
  • publicy_queryable # 前台公共可查?默认为public定义的值
  • exclude_from_search # 从搜索结果中排除?默认为public定义的值
  • show_in_nav_menus # nav menu可见?默认为public定义的值
  • supports # 哪些meta boxes?
    • title
    • editor
    • author
    • thumbnail
    • excerpt
    • comments
    • trackbacks
    • custom-fields
    • page-attributes
    • revision
    • post-formats
  • labels # array,用于各种情况下显示的标签
  • hierarchial # 默认为false
  • has_archive
  • can_export # 可导出?默认为true
  • taxonomies # array
  • menu_position # 默认在评论菜单之后
  • menu_icon # 图标
  • show_in_menu # 是否在admin menu中显示
  • show_in_admin_bar # 是否在admin bar中显示
  • capability_type #
  • capability # an array of custom capabilities ( 改、删、阅、发布)
  • query_var # 查询变量
  • rewrite # 创建permalink

其中,labels 接受一个数组,用于各种情况下,需要显示的标签

  • name # 通用名,常为复数
  • singular_name # 单数形式
  • add_new # Add New submenu item
  • add_new_item # 列表页新建一个Post
  • edit_item
  • new_item
  • view_item
  • all_items
  • menu_name
  • name_admin_bar
  • search_items
  • not_found
  • not_found_in_trash
  • parent_item_colon

Post Type 相关的常用函数

  • get_post_types($args,$output,$operator) # 获取指定条件下的Post Types
  • get_post_type($post) # 获取指定post的Post Type
  • post_type_exists($post_type) # 是否存在指定类型的Post Type
  • add_post_type_support($post_type,$sups) # 增加support
  • remove_post_type_support($post_type,$sups) # 增加support
  • set_post_type($postid,$post_type) # 设置Post Type类型
  • is_post_type_hierarchical('super_duper') # 是否是有等级的