关于PHP程序中的URL匹配设计模式的思考

这次的主题是PHP程序处理中的URL匹配的设计模式,考者,非考古,乃我自己对这些模式的考虑与思考,所以本文不存在经典理论。

模式一,文件式路由

这是最老式的做法,所谓文件路由,实际上,就是以Web目录内实际存在的.php文件为请求依托。这种处理方式中,每个对应标准请求输出的页面中,先引用项目中的底层文件,往往是include.php,functions.php,等,而页面处理逻辑则在该页面中进行,最后调用各自的输出引擎(一般是模版引擎)输出。

大多数的PHP的开源产品,都是基于此模式设计的。这种模式的好处在于,每个页面都是基于同样的模式铺排前进的,思路比较清晰、明朗,伪静态的实现,往往通过对指定页面的url rewrite实现。

但是这种模式也存在着很严重的弊端:

  1. 文件无约束,无下限,即开发人员可以不断的添加相应的.php文件,使项目的统一性越来越差。

  2. 类库之间缺乏组织性,某些页面调用A+B+C,某些调用A+B,有时候某些类库的设计又十分具有局限性,无法统一调用。简单的说,就是这种基于传统模式下的PHP编码,代码实现上缺乏统一的设计模式标准。大量的检查function_exists或class_exists。

  3. 页面中间的逻辑代码混乱、恐怖,国外某著名论坛的开源代码,页面包含SQL拼装,请求处理,十分混乱,为实现而实现,读之无味、形同嚼蜡。

  4. 模版引擎,可能我天生对PHP的模版引擎,有个别看法,我终归认为,最有效的输出模式是PHP的原生代码。当然如果一个模版引擎有提前的全编译输出,是好的,但同时也增加部署的复杂度。

我知道,上述的这些问题,通过项目规范,约束,都能很好的避免问题的发生,这里只是对一种模式的天然属性的描述。事实上,众多成功的开源项目,说明了这种模式的成功之处。而这种模式还有一个变种,就是switch语法的使用,根据请求的url中的某个queryString字段进行分发。这种模式实现了逻辑代码的统一与分布,某种程度上优化了上述问题。

上述这些问题的一个核心本质是:代码组织缺乏规范性。这里说的规范性,并不是开发团队约定的开发规范,而是指一个项目的架构、代码组织形式,提供给开发人员的日常操作习惯,这里并不存在对这种模式的褒贬之意。

模式二,虚拟化的url路由配置

这是由Ruby on Rails的Rest成功实践后,在PHP中实现的大量临摹者(呃,不要提REST的理论了,我对那个东西好不感冒,我只接收这种模式下所带来对于开发的好处)。这种模式是一种集中控制的思想,在实现上,所有的url请求,指向一个文件承载,而后通过一个路由配置表,实现对不同的url的解析,而后进入对应的请求控制模块中,从而实现对编码区域的区隔。

这种做法的好处是,基于一套路由配置表,控制模块通过OOP的设计,实现逻辑的统一与分布,项目代码较为规范(实际上即便如此,这种模式并不等于实践上的规范化),而这种模式伪静态的实现上是最简单的。

但是这种模式,也诞生出了两个恶心的问题,router match的算法问题和项目过于结构化,导致项目的学习成本陡然增加,具体上:

  1. url routers的算法里面,最核心的问题就是,如何以最小的循环,满足最大的匹配。

  2. 其中,url里的/controller/index/post,index是很恶心的,让人费解的一个东西。这也是rails实践中所带来的问题。

  3. url路由配置方法,那真是百花齐放,要学一个框架的配置,得花很多功夫,尤其是有些项目的路由设计真的是猥琐+BT,比如wordpress的,不但BT,而且由于其运行时的优先顺序,项目核心代码保留对routers的优先处理和解释——代码特权。

  4. 项目结构过于复杂,要学一个框架,先学其项目结构。

  5. 项目结构过于机械化,每个人、每个团队对PHP实践有自己的理解和经验,但是如果你使用了某某框架,请放下你的理解和经验——但,这些框架的设计思想本身并不是什么经典,所以很多使用者对框架核心各种改……

  6. 即便存在了Controller的Class,但是每个框架对于Action里面的编码内容,并没有很好的建议或规范,所以Action里面代码,要多混乱有多混乱,这就是上述提及的,虽然使用了规范化的思想,但实践过程却是狗血的,不规范的。有些框架,Action加载模块,Action里面直接读写数据库,拼接SQL,Action里面还有对view层输出内容实现变量绑定。我的天,这样搞,你回去搞基于文件模式好了,M什么VC?

  7. 各种框架的扩展思路,为了达到对view层的可重用,进行可扩展性打包,各个框架做了各种设定。怎么说呢,这也是很无聊的做法,你不如集成一个模版引擎,好歹还能让前端人员能输出调试。我在看Yii、CakePHP的这部分的设计和代码,有时候忍不住觉得好笑……学全Rails就那么重要吗?

要说url配置的复杂度,尤其是cakephp这种框架,为了满足更多更具体的复杂要求,url配置,要多复杂有多复杂……

我自己写的框架里面,2.x - 3.x的版本使用的就是url路由配置模式,因为考虑到配置简洁性、算法复杂度的合理性与实用性,我做出了简化。我设计的方案是基于url的前缀进行循环匹配的分布。算法思路如下:

  1. 根url解析,基于/controller/action和/controller => /controller/index的模式进行匹配。

  2. 指定url前缀空间,如:/admin/post/edit,url匹配会进入该前缀空间的循环,而不进行根url的循环。

  3. url前缀是优先匹配,url的切割从右(尾)开始切割,即当存在admin和admin/post两个前缀,则优先匹配admin/post。

这种算法在实践中,效率还可以,另,我忽略了get、post、put、delete的匹配,PHP本质上只支持get、post,何必自我设限、画蛇添足呢?

但是,当离开框架一段时间,再重新拾起,有些问题会让我十分讨厌:

  1. 配置、配置,为啥要配置?路由、路由,为啥要路由?

  2. 繁琐、不直观,如:array('controller' => 'Index'),每条配置都要写大量这种东西,Ctrl左键点击Index,并不会进入该Controller的代码(Rails好像也存在同样的问题,后来2.x、3.x我没使用过了,有错请批评)。

  3. 为什么一定要Controller\Action\View呢?谁规定的?

之所以提自己的框架,是为了说明一个框架创作者在使用自己框架的感受。MVC是个蛋,谁规定了一定要以MVC模式进行的?为什么url match的匹配算法,不能交予具体项目的编码中实现,框架为何要集成这个match的算法(事实上,框架实现match算法,需要考虑满足各种环境的最大集合,而项目实现的match算法,只要考虑满足当前条件,从算法成本来看,一定是基于项目的match算法成本最低,也最符合项目需要),你保证你的算法是最经典的?

也许正是因为有这种反思,所以存在第三种模式——

模式三,即时url解析分发

这个是Ruby的sinatra框架带来的一个实践模式,所有的url解析,透过请求承载文件即时做解析,对应响应的匿名函数做即时处理。这种模式一度引来的临摹的风潮,各种语言也存在对其的实现。但优点和弊端是明显的:

  1. url解析结果,是函数执行时的参数,好处是直观,坏处是重用性差。

  2. 匿名函数这个问题,好处是直观,坏处是,项目大了,满篇都是匿名函数。

  3. 整页都是url解析,好处是直观,坏处是,当url设计复杂了,就缺乏统一性了,而且不要忘记了,这一次又一次的get或者post匹配,实际上就是上面模式二中的url路由配置表的循环迭代匹配。

直观的东西,往往存在相应的问题,过于纯粹化,导致这种解决方案过于一根筋,缺乏可稍微调节优化的地方。

我对这种模式的实现,做过一些尝试(包括3.x的某个版本实现这种模式,然后以这种模式开发项目),小项目——对应只有几条的url规则的项目,优势还是十分明显的,不过复杂的url,尤其是当项目属于重构级的项目,你要考虑的问题就很多了。

当匿名函数大量存在的时候,所有处理都在这个函数中,代码的混乱也是相当恐怖的。即便稍微调用MVC的模式结构,仍然难以缓解,毕竟那么多个匿名函数就像一块一块的膏药贴在那里。

补充一点:其实还可以,在各个匿名函数中,call不同的function,来实现代码的集中管理。不过状态的控制会是另外一个问题,毕竟从一个function -> 另一个function,对于程序处理过程,有不同的状态,因为状态的不同,即便同样的调用一个函数,也应该有不同的输出。Ruby的sinatra框架,提供了全局的set和get,不过还是稍嫌有些简单了。

结尾

好吧,不得不承认,之所以半夜爬起来写这么一篇,是在实际编码中碰到一些问题。

写的目的就是为了厘清一下思路,因为各种模式都接触过,也都实现过,怎样才是最合理、最轻量的实现呢?

不过行文至此,我心里的答案已经清晰了。