2009年3月23日星期一

前段控制器是邪恶的吗?

所谓前端控制器(FrontController),是指一个请求运行的公共起点,并且在这里决定下一步执行什么。多数PHP框架里都实现了它。统一进行权限限制,会话管理等等公共操作,并且进而通过一个类似路由的装置,把请求委派给一个具体的命令对象来执行。实现方式上,前端控制器通常是以一个名为index.php的文件为载体,通过重写规则把请求都转发到这个文件上,如CakePHP在Apache上的设置:








RewriteEngine On


RewriteCond %{REQUEST_FILENAME} !-d


RewriteCond %{REQUEST_FILENAME} !-f


RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]








不过老话说,物极必反,随着前端控制器使用的泛滥,越来越多的人开始质疑PHP开发是否有必要使用前端控制器。这里以PHP之父Rasmus Lerdorf的影响最大,早些时候,他在Simple is Hard里陈述了如果想开发出有扩展性的Web应用,必须保证架构是Share-nothing Architecture:





Share-nothing Architecture


* Like HTTP, each request is distinct


* Shared data is pushed down to the data-store layer


* Avoid front controllers








Rasmus Lerdorf的看法:


在Rasmus Lerdorf看来,想要让一个网站具有良好的扩展性其实是非常简单的,只需将网站应用分成若干个独立小程序,前端透过API提供服务,后端是应用程序引擎,这样做自然会有扩展性。因为应用的每一个部分,都有不同等级的使用方式,需要有不同的扩充程度,需要不同的机制来处理。以开发雅虎电子邮件而言,是要开发一个地址服务程序、一个读信服务、一个送信服务,而送信程序完全和读信程序无关。以雅虎的规模而言,需要让这些工作完全分离,才有扩充性。这里的关键是你必须建立分离、模块化的独立端点,而不是全部放在同一个大篮子里。大多数现今开发框架,都使用了前端控制器,每一次浏览器发出请求时,就会调用这个前端控制器,再由前端控制器来分辨,使用者想要执行哪一支程序。这样做,一点意义都没有。在浏览器层次,程序完全能知道用户想要做什么事情,例如使用者只是要读信,程序就不用再把需求送到服务器,让服务器判断使用者要读信还是送信。将这类决策工作拉出浏览器,由服务器处理,就会浪费大量服务器资源,来处理那些对用户没有实际功用的工作。







我的看法:
我绝对相信Rasmus Lerdorf对于系统可扩展性的论述。但是我觉得他对于前端控制器合理性的判断太过武断。诚然,在浏览器的层面就完成了所有路由的工作有很好的可扩展性,但是在不同的请求里都进行权限限制,会话管理等等公共操作也变得重复了。而且,Rasmus Lerdorf对于前段控制器的论述是建立在一个假设条件上的:网站仅有一个前端控制器,并且仅运行在一台服务器上,所有的请求都要经由这台服务器再进行分发。如果真是这样,那么前端控制器必然会引起单点瓶颈,但事实上我们在实际使用前端控制器的时候,完全可以把代码拷贝到若干台服务器之上,请求均匀的分发到这些服务器上,此时的前端控制器只是程序逻辑上的单点,在物理架构上,是不存在瓶颈的,对扩展性没有影响。而且,系统里是可以有多个前端控制器的,比如说一个网站里可以有文章系统前端控制器,论坛系统前端控制器,用户系统前端控制器,这样压力自然就得以分散。当然,一不留神还是会遭遇一些前端控制的使用障碍,比如说路由无限扩张的问题:通常,使用前端控制器的时候,需要通过路由设定来映射URL和Action的关系,随着系统的膨胀,路由设定的内容会越来越多,每当一个请求过来的时候,都要从头解析路由设定以找出符合本次URL映射的Action,如果路由设定内容很多的话,这无疑是个巨大的负担,想要规避这个问题也没有太好的方法,只能是尽可能精简的使用自定义路由的功能。

总结:
牢记Rasmus Lerdorf的教诲是有意义的,但前端控制器并不邪恶,在了解它的优缺点后,合理的使用,既能保持程序的美感,又不丧失架构的扩展性。

2009年3月20日星期五

在企业里,逻辑本身常常让人困惑。

有这样一家公司,它已发展十年,从早期的一年营业额400万元,到今天可能一天的营业额就超过这个数字。去年,它首次跨过了盈亏平衡线,并且依然保持着“全球最大的中文网上书店”的称号。
实事求是地讲,这样的成绩并不算难看。于是,在最近我们杂志发表了一篇名为《当当:错过黄金十年?》的文章后,遭到了该公司的强烈抗议,一些主流门户网站也在压力下撤掉了稿件。
媒体是苛刻的评判者。我们之所以“危言耸听”地称其可能沦为“二流的互联网公司”,是因为我们看到了更多的“月之暗面”。
第一,当当效仿的“美国偶像”Amazon已经成为全球最具影响力和创造力的互联网公司,年营收191亿美元,净利润6亿多美元。而中国最早的B2C探路者却连上市都一直未能如愿,更遑论其它。
第二,在年增速超过48%的中国B2C市场上,5年前还占据40%以上份额的当当如今只有12%了,在它之上的有淘宝商城、京东商城,还有旗鼓相当的对手卓越亚马逊,以及北斗、麦网、红孩子、Vancl等一批紧追的对手(以上数据来自艾瑞和易观国际)。
第三,只有在传统的出版物网购市场上,当当的占有率超过第二名卓越亚马逊10%。但问题在于,这个市场早已不再是B2C生意的全部,整个蛋糕中它只占有18%,而日用百货和3C产品合计获得了接近70%的份额。可当当80%以上的业务还在出版物这一块(目前Amazon的相关业务只占到总销售额的57%)。结果是,当当从未像新浪对付雅虎中国、淘宝对付eBay易趣、百度对付谷歌、QQ对付MSN一样,把它的国际对手牢牢压在身下。
当然,棋至中盘,现在就断定输赢,可能还为时尚早。但在我看来,十年当当的身上,其实承载着一些看似悖论的商业命题:主动求变还是坚持不变;生存第一还是发展第一;做好产品还是做大平台;控制权更重要还是舞台更重要。
而个中的权衡取舍,又跟行业本质、竞争环境,乃至创始人的性格心态息息相关。
变还是不变
一个最基本的问题:B2C电子商务到底是属于IT互联网还是零售业?贝索斯的答案早就有了,“亚马逊是一家IT公司”,“我们要做电子商务的孵化器”。我们问俞渝,她说“我们做的是零售业,互联网只是工具。在这个行业里,沃尔玛是九段,我们顶多是三段”。
互联网和零售业是两个本质截然不同的行业。巴菲特大叔是喜爱零售业的,因为它的产业结构变化缓慢,企业更容易确立起“护城河”,如果你能像沃尔玛一样坚持“天天平价”50年不变,铁杵也能磨成针了。
他害怕互联网,因为这是年轻人的专利。变化太快,创新太容易,可能一夜之间就能起来一个巨人,统治者也可能在一夜之间倒下。互联网没有围墙,强调的是眼球经济学、长尾和免费万岁,想尽办法先让消费者的福利最大化,然后才能构建商业模式。只有让他们先爽了,他们才愿意送钱过来。
本质上,零售业更适合PE投资,只有你的雪球已经滚到足够大的时候,别人才会来帮你。而VC天生就跟IT互联网如影随形。他们干的就是赌博,十个车库小子里,有一两个成功就OK了。
零售业的胜利往往是成本和细节的胜利,绝不碰赔钱赚吆喝的生意,就算毛利率再低,也得从毛巾里拧出利润来才行。而互联网的胜利首先是思想的胜利。陈天桥就说过他最害怕的竞争对手,不是他面前的谁谁谁,而是某个角落里埋头苦干的年轻人。
所以,你可以理解俞渝为什么不看好那些以互联网方式扩张的同行,比如以300%速度成长的京东商城,“一个无法长期做下去的生意,平进平出,做生意再低也要有一个利润”。
十年前,俞渝和李国庆写的商业计划书里想干三件事:图书音像、信息服务、媒体价值。十年以后,俞渝承认,“我们只做到了第一个。剩下两个都没有做成功”。再问俞渝,未来做什么?“下一个十年,当当还会坚持现在的模式,只是产品更加丰富。”
但在互联网业里,坚持在大多数时候都不是正确的。如果马化腾仅仅满足于做一个IM软件的开发商,那么腾讯会成为今天中国市值最大的互联网公司吗?如果马云不做淘宝和支付宝?如果百度不做贴吧、知道、MP3?如果盛大只运营《传奇》游戏? 它们还是今天的它们吗?
同一件事情,看的高度和角度不同,就会产生不同的理解,又进而决定不同的行为方式。
生存第一还是发展第一
前一阵跟土豆的王微交流。他讲自己的亲身故事。2005年刚开始做视频的时候,连他自个在一起就5条人枪,IDGVC给了50万美刀。2006年5月B轮融资850万美元完成,也才20来个人。当时,他跟JAFCO(集富亚洲)的投资人说,这钱够做个三年,一步步来。结果没想到,当年10月,Google宣布16亿美金收购Youtube,马上无数国人跟进。王微不得不改变策略,隔几个月就动手融一次资(2007年4月C轮1900万美元,2008年4月D轮融资5700万美元),也没有什么对赌协议,就是最简单的拿股份换资本,根本没有什么年度计划,只有月度顶多季度计划,不管外界怎么说,就是要用百米冲刺的速度抢跑。
当时早期一块起来的某同行放话说,不跟风,不烧钱,要用长跑的心态去做事。结果今天,这位同行虽然还有现金没花完,但已经没人把他们当棵菜。而土豆牢牢占据了市场前两把交椅,投资者也只认前两名。“我们毕竟没有那么深的口袋,一旦别人超出你一大截之后,就很难追上。”
“从去年下半年开始,我们有年度计划了,控制速度,巩固基础,学会赚钱。因为我已经从同类的对手中跑出来了,现在我关注的是QQ、新浪、搜狐甚至央视。他们每一个都比我大很多。”他微笑着讲。
在快速变化的新兴产业里,竞争就不是自己跟自己较劲,更不是你和对手两家下围棋,更像是四国军棋甚至海陆空混战,对竞争形势和大环境的判断就变得非常重要。
3G门户的张向东跟我谈过类似的观点。他参观日本最赚钱的无线互联公司DeNA和MIX,很感慨的一点就是:对方的产业环境、商业文化比我们好。只要你找到了一条成功的道路,其它对手就会自动地选择其它道路去摸索。不会“一窝蜂”地冲上去。“所以人家PV流量虽然不如我们,应用也没我们多,但赚钱比我们容易多了。”
没人能教你如何在乱中取胜,如何抢装备抢地盘,什么时候冲锋,什么时候卧倒,怎样在看似没可能的情况下过江,你只能依靠自己的本能、直觉和运气,就跟《我的团长我的团》里面的龙文章一样。美军教官也教不了这些东西。
真正第一流的公司会抢在市场变化之前调整节奏,第二流的公司始终只会坚持一种节奏,而第三流,则只能跟着市场做随机的布朗运动。
其实,商界跟时尚界无二。前几年,高速成长、追逐高风险高回报的“快公司”倍受追捧,金融风暴之后,昔日明星被纷纷打入冷宫,而谨慎保守、现金为王的“慢公司”似乎又成为业界欣赏的对象。
的确,相比曾经喧嚣一时的8848和PPG,坚持一种节奏的当当看上去更值得尊敬。但我相信这很大程度上只是因为人们的心态发生了变化。保守成为不冒进的代名词。
如今,人人都知道,现金是奶妈。但你不能永远躺在奶妈的怀抱里。你可以裁人裁到只剩你和CFO、砍掉一切面向未来的业务,只保留一间办公室,这样也许能让你的公司活100年。但这有意义吗?你可以忘掉市盈率,但你还得考虑回报率。
没有投入企业就无法发展,如果资金投入后,未来3年有30%的回报,你冒不冒险?投资商关注的可不是你保存了多少现金,而是你能否把现金用好,真正构建起你的竞争力优势,在环境复苏的时候,成为最大的受益者。
所以,当俞渝很自豪地告诉我们,“我们第三轮融资的钱还放在银行里,发愁的是选什么理财产品”的时候,我在轻叹,如果不知道怎么用,干嘛选择股权融资这种最昂贵的选择?
特定情境下,生存和发展就是一对悖论。不做是等死,做是找死。中国的第一流创业家,大多找过死。张朝阳、丁磊、李彦宏、马化腾、陈天桥、史玉柱等等,都有过濒死体验,敢置之死地而后生的,才有机会大成。你敢吗?
做产品还是做平台
不管是做平台的陈天桥、马云、李彦宏,还是做产品的丁磊、马化腾,两条道路都是可以成功的。
过去十年中,当当其实做过很多尝试,它们做过C2C的“当当宝”,干过分类信息和联营商城,也卖过衬衫,还尝试到进入唱片发行的渠道,但几乎全都浅尝辄止。平均每项新业务的寿命可能不超过半年。
在我看来,当当始终没有想清楚的一件事是“做好产品,还是做大平台”。
好吧,先说做产品。马化腾的意见是,产品的核心能力必须做到极致,让用户片刻难离,让对手望尘莫及。像QQ一样覆盖每台电脑的桌面,像《征途》一样让有钱人没钱人都上瘾。
用户对于B2C服务的实际需求其实就四条:品种够丰富、价格够低廉、购物够放心、送货够及时。但作为一个多年的VIP用户,我却不得不长期忍受当当的缺货,延期迟到,书籍的脏污皱折以及塑料提袋。如果俞渝总裁去百度贴吧看一下,相信这应该不是我个人的感受。因为当当并没有比对手做得更好,所以它不是不可替代的。
当你的核心产品比所有对手都要强的时候(市占率超过50%),你就可以扩展,从一个点扩散到一个面或者蔓延到整个链条,这才能让生意有持续的想象力。我不太理解的是,为什么俞渝认为当当不能做衬衫直销(“如果你要丰富产品就要有专业的团队,当当会变成一个服装公司了。”)我也不明白,为什么豆瓣也好,起点中文网也罢,都是别人做的事情,而不应该是当当做的事情。
假设当当真正深耕出版物这个市场。我查了一下,2007年,光中国图书出版市场的销售额就超过500亿元。作为全球最大的中文网上书店,如果能切掉十分之一的蛋糕,日子也可以过得很舒服了,甚至足以对产业产生影响。但当当却连1/50不到。
2007年,美国图书出版业的总销售额是250亿美元,而Amzon“媒体出版物业务”的北美营收数字是46亿美金。
回过头来说做平台,2005年,正式上线不过3天,“当当宝”宣布暂停卖家认证。俞渝当时的理由是,“四道贩子的个人交易模式不能带给用户真正的低价;出现危险商户,目前规则不能够保证消费者的权益,诚信形象将受到损失;违规商品出售,给客户购买造成风险;当当网服务的对象就是中国正在崛起的中产阶段,不低价、勿宁死,不诚信,就关门。”
没有诚信、物流很烂,支付系统不完善,这些都是客观环境。但它们不是理由,摆在你面前的问题,也摆在你的对手面前。马云说过一句话:如果银行不改变,我们改变银行。今天,支付宝的确在改变银行。
当当也想过做B2C的平台。但做平台商,得有服务意识。就跟中关村的电子市场一个道理,你门脸越大,停车越方便,配套越齐全,人流才越多,大家才有钱赚,你才能收租。而我在当当的联营商户上订过鲜花,结果晚了足足两周才送到。理由是当当只出租虚拟店面。物流配送客户自己做的。
现在,当当再想做B2C平台已经很难了,因为众多的品牌大企业已经跟随淘宝商城而去,后者通过C2C带来的用户流、阿里旺旺、支付宝已彻底压倒了当当。
其实,当当不必“要到外太空寻找成功的榜样”,看看Amazon,看看淘宝,看看腾讯,就该知道自己到底做得怎么样?
控制权更重要还是舞台更重要
关于创始人和投资人斗智斗勇的故事,一直是商业媒体最喜爱的题材。通常情况不外以下几种:1,投资人不满创始人工作不力,另立新主,杯葛创始人,比如中华英才网;2,创始人与投资人严重意见分歧,最后一拍两散,投资人主动退场,比如盛大;3,创始人A与投资人C、D联手,做掉创始人B,比如新浪;4,创始人把投资人哄入场,把投给A公司的钱秘密输送到自己名下的B公司,最后创始人和投资人两败俱伤,比如亚洲传媒……
核心是控制权的问题。创始人(Founder)是不是一定要做CEO,企业一旦接受了投资,还是不是创始人说了算的企业。
在硅谷,游戏规则比较透明。选贤任能,大家比较能够坦然接受事实。也尊重合同,条款里把丑话都写在前头。
当当也算经历过反复折腾的公司,李国庆甚至曾以辞职相威胁。而最终的结果,是经历三轮融资之后,两夫妻仍然控制着50%以上的股份。“如果你对当当的行为感到费解,那只是因为你对这个公司并不了解。”一位离职的当当中层告诉我们。在这个控制权高度集中的公司,几乎没有人能影响俞渝和李国庆的决定,不管你是投资人还是副总裁。
好的一面,创始人不用再听投资人指手画脚,可以完全按照自己的意志来做事;坏的一面是,创始人的境界决定了这个企业的天花板。
心有多大,舞台就有多大。如果你就愿意小富即安,这是你的追求,别人也无权责备你。但互联网又的确是一个制造梦想的行业,所以特别适合雄心勃勃的年轻人。
贝索斯是做对冲基金出身的,见惯了大赢大输。无论在上市前,还是在上市后,Amazon从未停止过大手笔的投资。从最早的网上书店,到音乐、玩具、拍卖商店,再到zShop店中店,再到投入巨资构筑物流平台、云计算平台,而Kindle更可能变成出版物的数字平台。所有这些都不是华尔街希望贝索斯做的,是贝索斯逼着华尔街认可的。(现在,所有Amazon内部人持有的股份是23%)。
所以,KPCB的“VC教父”John Doerr一直挺他到现在,是Amazon的董事会成员,还拿着不少其股票。而Amazon也是硅谷大公司里少有几个创始人一直干CEO到现在的。
李国庆在创办当当前,是有十年图书出版经验的“老江湖”。俞渝在华尔街干过多年,专长是做债券融资,一名石油客户给俞渝的评价是从来不能在油田找到利润,却能在铅笔间上找到利润。
一个对行业熟悉,另一个关注细节,两人的特点都是低头做事,不抬头望天。2005年,卓越亚马逊还因为艰苦整合而一片混乱的时候,当当却没有抓住机会彻底打垮对手。现在,当当的先发优势和主场之利不再,而卓越亚马逊的系统优势开始显现。
在互联网业,最好的防守就是进攻。企业做到一定时候,必须跨越甚至自废武功,去做新的。心脏不好的人干不了。丁磊和马化腾是国内两个出名的谨慎派,但做新业务从不手软。
拿破仑有句名言:不想当将军的士兵不是好士兵。但显然,能够最后成为将军的士兵万千人中才有一个。“彼得定律”的解释:大多数人都会一直爬升,直到一个你不能胜任的位置而停滞下来。
创业或许同样如此,绝大多数个体会在通往巅峰的道路上半途而废。几个伟大人物改变世界,少数一流人物可以影响世界,一些二流的人物为自己和员工改善生活,而三流四流者则是在徒耗资源。

2009年3月15日星期日

PHP autoload机制详解

关于SPL的autoload的函数手册,请参考 SPL函数手册:

http://www.phpinternals.com/blog/2008/12/translate-refs-spl-functions/

(1) autoload机制概述

在使用PHP的OO模式开发系统时,通常大家习惯上将每个类的实现都存放在一个单独的文件里,这样会很容易实现对类进行复用,同时将来维护时也很便利。这也是OO设计的基本思想之一。在PHP5之前,如果需要使用一个类,只需要直接使用include/require将其包含进来即可。下面是一个实际的例子:

/* Person.class.php */
class Person {
var $name, $age;

function __construct ($name, $age)
{
$this->name = $name;
$this->age = $age;
}
}
?>

/* no_autoload.php */
require_once (”Person.class.php”);

$person = new Person(”Altair”, 6);
var_dump ($person);
?>

在这个例子中,no-autoload.php文件需要使用Person类,它使用了require_once将其包含,然后就可以直接使用Person类来实例化一个对象。

但随着项目规模的不断扩大,使用这种方式会带来一些隐含的问题:如果一个PHP文件需要使用很多其它类,那么就需要很多的require/include语句,这样有可能会造成遗漏或者包含进不必要的类文件。如果大量的文件都需要使用其它的类,那么要保证每个文件都包含正确的类文件肯定是一个噩梦。

PHP5为这个问题提供了一个解决方案,这就是类的自动装载(autoload)机制。autoload机制可以使得PHP程序有可能在使用类时才自动包含类文件,而不是一开始就将所有的类文件include进来,这种机制也称为lazy loading。

下面是使用autoload机制加载Person类的例子:

/* autoload.php */
function __autoload($classname) {
require_once ($classname . “class.php”);
}

$person = new Person(”Altair”, 6);
var_dump ($person);
?>

通常PHP5在使用一个类时,如果发现这个类没有加载,就会自动运行__autoload()函数,在这个函数中我们可以加载需要使用的类。在我们这个简单的例子中,我们直接将类名加上扩展名”.class.php”构成了类文件名,然后使用require_once将其加载。从这个例子中,我们可以看出autoload至少要做三件事情,第一件事是根据类名确定类文件名,第二件事是确定类文件所在的磁盘路径(在我们的例子是最简单的情况,类与调用它们的PHP程序文件在同一个文件夹下),第三件事是将类从磁盘文件中加载到系统中。第三步最简单,只需要使用include/require即可。要实现第一步,第二步的功能,必须在开发时约定类名与磁盘文件的映射方法,只有这样我们才能根据类名找到它对应的磁盘文件。

因此,当有大量的类文件要包含的时候,我们只要确定相应的规则,然后在__autoload()函数中,将类名与实际的磁盘文件对应起来,就可以实现lazy loading的效果。从这里我们也可以看出__autoload()函数的实现中最重要的是类名与实际的磁盘文件映射规则的实现。

但现在问题来了,如果在一个系统的实现中,如果需要使用很多其它的类库,这些类库可能是由不同的开发人员编写的,其类名与实际的磁盘文件的映射规则不尽相同。这时如果要实现类库文件的自动加载,就必须在__autoload()函数中将所有的映射规则全部实现,这样的话__autoload()函数有可能会非常复杂,甚至无法实现。最后可能会导致__autoload()函数十分臃肿,这时即便能够实现,也会给将来的维护和系统效率带来很大的负面影响。在这种情况下,难道就没有更简单清晰的解决办法了吧?答案当然是:NO! 在看进一步的解决方法之前,我们先来看一下PHP中的autoload机制是如何实现的。

(2) PHP的autoload机制的实现

我们知道,PHP文件的执行分为两个独立的过程,第一步是将PHP文件编译成普通称之为OPCODE的字节码序列(实际上是编译成一个叫做zend_op_array的字节数组),第二步是由一个虚拟机来执行这些OPCODE。PHP的所有行为都是由这些OPCODE来实现的。因此,为了研究PHP中autoload的实现机制,我们将autoload.php文件编译成opcode,然后根据这些OPCODE来研究PHP在这过程中都做了些什么:

/* autoload.php 编译后的OPCODE列表,是使用作者开发的OPDUMP工具
* 生成的结果,可以到网站 http://www.phpinternals.com/ 下载该软件。
*/
1: 2: // require_once (”Person.php”);
3:
4: function __autoload ($classname) {
0 NOP
0 RECV 1
5: if (!class_exists($classname)) {
1 SEND_VAR !0
2 DO_FCALL ‘class_exists’ [extval:1]
3 BOOL_NOT $0 =>RES[~1]
4 JMPZ ~1, ->8
6: require_once ($classname. “.class.php”);
5 CONCAT !0, ‘.class.php’ =>RES[~2]
6 INCLUDE_OR_EVAL ~2, REQUIRE_ONCE
7: }
7 JMP ->8
8: }
8 RETURN null
9:
10: $p = new Person(’Fred’, 35);
1 FETCH_CLASS ‘Person’ =>RES[:0]
2 NEW :0 =>RES[$1]
3 SEND_VAL ‘Fred’
4 SEND_VAL 35
5 DO_FCALL_BY_NAME [extval:2]
6 ASSIGN !0, $1
11:
12: var_dump ($p);
7 SEND_VAR !0
8 DO_FCALL ‘var_dump’ [extval:1]
13: ?>

在autoload.php的第10行代码中我们需要为类Person实例化一个对象。因此autoload机制一定会在该行编译后的opcode中有所体现。从上面的第10行代码生成的OPCODE中我们知道,在实例化对象Person时,首先要执行FETCH_CLASS指令。我们就从PHP对FETCH_CLASS指令的处理过程开始我们的探索之旅。

通过查阅PHP的源代码(我使用的是PHP 5.3alpha2版本)可以发现如下的调用序列:

ZEND_VM_HANDLER(109, ZEND_FETCH_CLASS, …) (zend_vm_def.h 1864行)
=> zend_fetch_class (zend_execute_API.c 1434行)
=>zend_lookup_class_ex (zend_execute_API.c 964行)
=> zend_call_function(&fcall_info, &fcall_cache) (zend_execute_API.c 1040行)

在最后一步的调用之前,我们先看一下调用时的关键参数:

/* 设置autoload_function变量值为”__autoload” */
fcall_info.function_name = &autoload_function; // Ooops, 终于发现”__autoload”了

fcall_cache.function_handler = EG(autoload_func); // autoload_func !

zend_call_function是Zend Engine中最重要的函数之一,其主要功能是执行用户在PHP程序中自定义的函数或者PHP本身的库函数。zend_call_function有两个重要的指针形参数fcall_info, fcall_cache,它们分别指向两个重要的结构,一个是zend_fcall_info, 另一个是zend_fcall_info_cache。zend_call_function主要工作流程如下:如果fcall_cache.function_handler指针为NULL,则尝试查找函数名为fcall_info.function_name的函数,如果存在的话,则执行之;如果fcall_cache.function_handler不为NULL,则直接执行fcall_cache.function_handler指向的函数。

现在我们清楚了,PHP在实例化一个对象时(实际上在实现接口,使用类常数或类中的静态变量,调用类中的静态方法时都会如此),首先会在系统中查找该类(或接口)是否存在,如果不存在的话就尝试使用autoload机制来加载该类。而autoload机制的主要执行过程为:

(1) 检查执行器全局变量函数指针autoload_func是否为NULL。
(2) 如果autoload_func==NULL, 则查找系统中是否定义有__autoload()函数,如果没有,则报告错误并退出。
(3) 如果定义了__autoload()函数,则执行__autoload()尝试加载类,并返回加载结果。
(4) 如果autoload_func不为NULL,则直接执行autoload_func指针指向的函数用来加载类。注意此时并不检查__autoload()函数是否定义。

真相终于大白,PHP提供了两种方法来实现自动装载机制,一种我们前面已经提到过,是使用用户定义的__autoload()函数,这通常在PHP源程序中来实现;另外一种就是设计一个函数,将autoload_func指针指向它,这通常使用C语言在PHP扩展中实现。如果既实现了__autoload()函数,又实现了autoload_func(将autoload_func指向某一PHP函数),那么只执行autoload_func函数。

(3) SPL autoload机制的实现

SPL是Standard PHP Library(标准PHP库)的缩写。它是PHP5引入的一个扩展库,其主要功能包括autoload机制的实现及包括各种Iterator接口或类。SPL autoload机制的实现是通过将函数指针autoload_func指向自己实现的具有自动装载功能的函数来实现的。SPL有两个不同的函数spl_autoload, spl_autoload_call,通过将autoload_func指向这两个不同的函数地址来实现不同的自动加载机制。

spl_autoload是SPL实现的默认的自动加载函数,它的功能比较简单。它可以接收两个参数,第一个参数是$class_name,表示类名,第二个参数$file_extensions是可选的,表示类文件的扩展名,可以在$file_extensions中指定多个扩展名,护展名之间用分号隔开即可;如果不指定的话,它将使用默认的扩展名.inc或.php。spl_autoload首先将$class_name变为小写,然后在所有的include path中搜索$class_name.inc或$class_name.php文件(如果不指定$file_extensions参数的话),如果找到,就加载该类文件。你可以手动使用spl_autoload(”Person”, “.class.php”)来加载Person类。实际上,它跟require/include差不多,不同的它可以指定多个扩展名。

怎样让spl_autoload自动起作用呢,也就是将autoload_func指向spl_autoload?答案是使用spl_autoload_register函数。在PHP脚本中第一次调用spl_autoload_register()时不使用任何参数,就可以将autoload_func指向spl_autoload。

通过上面的说明我们知道,spl_autoload的功能比较简单,而且它是在SPL扩展中实现的,我们无法扩充它的功能。如果想实现自己的更灵活的自动加载机制怎么办呢?这时,spl_autoload_call函数闪亮登场了。

我们先看一下spl_autoload_call的实现有何奇妙之处。在SPL模块内部,有一个全局变量autoload_functions,它本质上是一个HashTable,不过我们可以将其简单的看作一个链表,链表中的每一个元素都是一个函数指针,指向一个具有自动加载类功能的函数。spl_autoload_call本身的实现很简单,只是简单的按顺序执行这个链表中每个函数,在每个函数执行完成后都判断一次需要的类是否已经加载,如果加载成功就直接返回,不再继续执行链表中的其它函数。如果这个链表中所有的函数都执行完成后类还没有加载,spl_autoload_call就直接退出,并不向用户报告错误。因此,使用了autoload机制,并不能保证类就一定能正确的自动加载,关键还是要看你的自动加载函数如何实现。

那么自动加载函数链表autoload_functions是谁来维护呢?就是前面提到的spl_autoload_register函数。它可以将用户定义的自动加载函数注册到这个链表中,并将autoload_func函数指针指向spl_autoload_call函数(注意有一种情况例外,具体是哪种情况留给大家思考)。我们也可以通过spl_autoload_unregister函数将已经注册的函数从autoload_functions链表中删除。

上节说过,当autoload_func指针非空时,就不会自动执行__autoload()函数了,现在autoload_func已经指向了spl_autoload_call,如果我们还想让__autoload()函数起作用应该怎么办呢?当然还是使用spl_autoload_register(__autoload)调用将它注册到autoload_functions链表中。

现在回到第一节最后的问题,我们有了解决方案:根据每个类库不同的命名机制实现各自的自动加载函数,然后使用spl_autoload_register分别将其注册到SPL自动加载函数队列中就可了。这样我们就不用维护一个非常复杂的__autoload函数了。

(4) autoload效率问题及对策

使用autoload机制时,很多人的第一反应就是使用autoload会降低系统效率,甚至有人干脆提议为了效率不要使用autoload。在我们了解了autoload实现的原理后,我们知道autoload机制本身并不是影响系统效率的原因,甚至它还有可能提高系统效率,因为它不会将不需要的类加载到系统中。

那么为什么很多人都有一个使用autoload会降低系统效率的印象呢?实际上,影响autoload机制效率本身恰恰是用户设计的自动加载函数。如果它不能高效的将类名与实际的磁盘文件(注意,这里指实际的磁盘文件,而不仅仅是文件名)对应起来,系统将不得不做大量的文件是否存在(需要在每个include path中包含的路径中去寻找)的判断,而判断文件是否存在需要做磁盘I/O操作,众所周知磁盘I/O操作的效率很低,因此这才是使得autoload机制效率降低的罪魁祸首!

因此,我们在系统设计时,需要定义一套清晰的将类名与实际磁盘文件映射的机制。这个规则越简单越明确,autoload机制的效率就越高。

结论:autoload机制并不是天然的效率低下,只有滥用autoload,设计不好的自动装载函数才会导致其效率的降低。

PHP Snippet

1. When using REQUEST variables, there are always some COOKIE variables we don't want, beside POST, GET, SESSION. We can insert a piece of code below to filter all COOKies


if (count ( $_COOKIE )) {
foreach ( array_keys ( $_COOKIE ) as $value ) {
unset ( $_REQUEST [$value] );
}
}

Note: array_keys()函数返回一个数组中的所有键



2. PHP check the format of input Emails.

if(isset($_REQUEST['email']) && !empty($_REQUEST['email']))

{

$_REQUEST ['email'] = trim ( $_REQUEST ['email'] );

if(substr_count($_REQUEST['email'],"@") != 1 stristr($_REQUEST['email']," "))
{
$errors [] = "Email address is invalid";
}else
{
$exploded_email = explode ( "@", $_REQUEST ['email'] );if(empty($exploded_email[0]) strlen($exploded_email[0]) > 64 empty($exploded_email[1]))
{
$errors [] = "Email address is invalid";
}else
{
if (substr_count ( $exploded_email [1], "." ) == 0) {
$errors [] = "Email address is invalid";
} else {
$exploded_domain = explode ( ".", $exploded_email [1] );
if (in_array ( "", $exploded_domain )) {
$errors [] = "Email address is invalid";
} else {
foreach ( $exploded_domain as $value ) {if(strlen($value) > 63 !preg_match('/^[a-z0-9-]+$/i',$value))
{
$errors [] = "Email address is invalid";
break;
}
}
}
}
}
}
}
?>


3. Check the referer is from the same website.

if(!(isset($_SERVER['HTTP_REFERER']) && !empty($_SERVER['HTTP_REFERER']) && stristr($_SERVER['HTTP_REFERER'],$_SERVER['HTTP_HOST']))){$errors[] = "You must enable referrer logging to use the form";}
?>


Note: stristr() 函数查找字符串在另一个字符串中第一次出现的位置。
如果成功,则返回字符串的其余部分(从匹配点)。如果没有找到该字符串,则返回 false。



4.Check for a blank form


function recursive_array_check_blank($element_value) {

global $set;

if (! is_array ( $element_value )) {
if (! empty ( $element_value )) {
$set = 1;
}
} else {

foreach ( $element_value as $value ) {
if ($set) {
break;
}
recursive_array_check_blank ( $value );
}

}

}

recursive_array_check_blank ( $_REQUEST );

if (! $set) {
$errors [] = "You cannot send a blank form";
}

unset ( $set );

?>



5. Using constant PHP_EOL as \n


if (! defined ( "PHP_EOL" )) {
define ( "PHP_EOL", strtoupper ( substr ( PHP_OS, 0, 3 ) == "WIN" ) ? "\r\n" : "\n" );
}
?>