1、PATHINFO功能简述
搞PHP的都知道ThinkPHP是一个免费开源的轻量级PHP框架,虽说轻量但它的功能却很强大。
这也是我接触学习的第一个框架。TP框架中的URL默认模式即是PathInfo模式。这个模式很强大,每当你访问一个网站必然带有一长串参数,但是太长又显得不太友好。对于访问一个以MVC模式搭建的网站,必然带有M、C、A三个参数即module、controller、action,这些参数需要还需要用&符号隔开,假若参数量很多,就显得特别的不友好啦。然而PathInfo模式功能就是将这一长串缩短简化,让这个路径变得更加友好的显示。
传统的访问路径是这样子的:
http://www.example.com/index.php?m=module&c=controller&a=action&var1=vaule1&var2=vaule2.....
而ThinkPHP在默认的URL模式下能够做到这样子的路径:
http://www.example.com/index.php?module/controller/action/var1/vaule1/var2/value2.....
两者相比较很容易就得出结论:PathInfo模式下的访问路径显示更加友好!
然而在这篇文章中我所要讲述的就是如何搭建好这种友好的访问路径。
我的目标路径是这样子的(当然TP也可以很轻松做到这样子):
http://www.example.com/module/controller/action/var1/vaule1/var2/value2.....
以上三个路径所表示的意思是一样的即都访问同一个站点,带有同样的参数
2、写作小背景
由于最近打算模仿写一个小的框架,加强与巩固一下自己基础知识。以MVC模式进行搭建,所以就少不接触模型呀控制器呀行为之类的了。之前一直用ThinkPHP,感觉ThinkPHP中的URL中的默认模式PATHINFO很强大。所以就决定制作一个这样的功能用于自己的小框架的URL上啦。之前一直想去研究一下ThinkPHP的原码,由于没有时间,到现在都还没有去实施。打算在这个寒假里好好研究一下这个框架的原码,大学生涯最后一个寒假啦。
----为学须刚与恒,不刚则隋隳,不恒则退。
与大家共勉吧,坚持就是success嘛!好啦扯远啦,言归正传,我所制作的PATHINFO功能上和ThinkPHP是一致的,至于里面的深层原理效率问题什么的,是否和TP中的PATHINFO一样就不太清楚啦,毕竟还没有去研究TP原码,这里就按照我自己的思路来写。
3、所涉及的核心知识
1、apache的rewrite模块。
采用apache的rewrite模块,将所有访问这个站点的路径都只能从单一index.php入口进入。(由于apache重写规则也是一块硬骨头,在这里就不展开来细讲啦,到时候再另外写一篇文章来总结这个重写规则,与大家一起相互学习学习。作者博客:http://www.cnblogs.com/phpstudy2015-6/)
2、正则表达式
正则表达式的基本知识、PHP中的preg_match()函数,这个函数是制作这个功能的关键,所以需要重点理解。
3、类文件的自动载入与路径问题
在MVC模式中最基础且需要处理的就是M、C、A三个参数,这三个参数思想贯穿于整个模式代码中。
在这里我们需要处理的就是URL,即是我们只需要通过路径的module、controller、action就能够确定所访问的哪个类哪个控制器以及行为。对于这些类的对象object生成以及行为方法的调用都是自动的,不需要我们另外再去编写代码一一处理。
因此对于如何精准将类文件载入以及调用方法是个很关键的步骤。PHP中内部自带有一个new Object时自动触发的函数,那就是__autoload(),它扩展函数spl_autoload_register()注册自动加载函数。
对于路径的问题,由于需要实现自动化即自动载入类文件等等,所以需要相对健壮的载入路径代码,让其移植性强一点。例如在Window和Linux系统下能够畅通无阻,所以需要用到PHP中的一个魔术常量__DIR__来写路径代码。
4、环境说明
Linux虚拟机、PHP5.3.6、域名www.test2.com
5、代码实例
1、建立好相应文件夹。虽然这个例子很简单但是我们也不能含糊过去,养成良好的习惯,争取早入成为大神,哈哈
这个文件夹的话,随个人的想法来建立。要是用于框架上的话,这一步就显得很重要啦。具体可以参考各个框架的文件目录结构。我的文件目录如下图:
才刚开始搭建的,目录很简陋,还未完善哈,毕竟还是菜鸟级别,大神们勿喷,可以的话请,还请各位指点指点哈。
这里主要是展示一下我的文件夹,方便下面的理解分析。这个功能重点是Url.class.php文件。
2、开启apache的rewrite模块
在相应的配置文件将其打开就好,这里就不讲解了。
接着在根目录建立.htaccess文件,这里主要是放重写规则,如下所示:
1 <IfModule mod_rewrite.c> 2 RewriteEngine on 3 RewriteCond %{Request_FILENAME} !-f 4 RewriteRule !.(js|ico|gif|jpg|png|css)$ /index.php 5 </IfModule>
简单解析:
1、RewriteEngine on 开启重写
2、RewriteRule 重写规则,表示非上述后缀的路径都适合
3、RewriteCond 判断是不是文件
这里的作用就是将所有访问www.test2.com的路径都只能index.php路径进入,即为单一入口。
3、主要代码
Url.class.php
我将此文件放入/Framework/Core文件夹中
1 <?php 2 /* 3 *@作 者 :那一叶随风 4 *@博 客 :http://www.cnblogs.com/phpstudy2015-6/ 5 *@时 间 :2017.1.3 6 */ 7 class Url 8 { 9 //定义正则表达式常量 10 const REGEX_ANY="([^/]+?)"; #非/开头任意个任意字符 11 const REGEx_INT="([0-9]+?)"; #数字 12 const REGEX_ALPHA="([a-zA-Z_-]+?)"; #字母 13 const REGEX_ALPHANUMERIC="([0-9a-zA-Z_-]+?)"; #任意个字母数字_- 14 const REGEX_STATIC="%s"; #占位符 15 const ANY="(/[^/]+?)*"; #任意个非/开头字符 16 17 protected $routes=array(); #保存路径正则表达式 18 19 #添加路由正则表达式 20 public function addRoute($route) 21 { 22 $this->routes[]=$this->_ParseRoute($route); 23 } 24 25 /*private 26 @input :$route 输入路由规则 27 @output:return 返回路由正则规则 28 */ 29 private function _ParseRoute($route) 30 { 31 $parts=explode("/", $route); #分解路由规则 32 $regex="@^"; #开始拼接正则路由规则 33 if(!$parts[0]) 34 { 35 array_shift($parts); #除去第一个空元素 36 } 37 foreach ($parts as $part) 38 { 39 $regex.="/"; 40 $args=explode(":",$part); 41 if(!sizeof($args)==2) 42 { 43 continue; 44 } 45 $type=array_shift($args); 46 $key=array_shift($args); 47 $this->_normalize($key); #使参数标准化,排除其他非法符号 48 $regex.='(?P<'.$key.'>'; #为了后面preg_match正则匹配做铺垫 49 switch (strtolower($type)) 50 { 51 case 'int': #纯数字 52 $regex.=self::REGEX_INT; 53 break; 54 case 'alpha': #纯字母 55 $regex.=self::REGEX_ALPHA; 56 break; 57 case 'alphanum': #字母数字 58 $regex.=self::REGEX_ALPHANUMBERIC; 59 break; 60 default: 61 $regex.=$type; #自定义正则表达式 62 break; 63 } 64 $regex.=")"; 65 } 66 $regex.=self::ANY; #其他URL参数 67 $regex.='$@u'; 68 return $regex; 69 } 70 71 /*public,将输入的URL与定义正则表达式进行匹配 72 @input :$request 输入进来的URL 73 @output :return 成功则输出规则数组数据 失败输出false 74 */ 75 public function getRoute($request) 76 { 77 #处理request,进行参数处理,不足M、C、A,则自动补为home、index、index,即构建MVC结构URL 78 $request=rtrim($request,'/'); #除去右边多余的斜杠/ 79 $arguments=explode('/',$request); 80 $arguments=array_filter($arguments); #除去数组中的空元素 81 $long=sizeof($arguments); #数组中的个数 82 switch ($long) #判断个数,不足就补够 83 { 84 case '0': 85 $request='/home/index/index'; 86 break; 87 case '1': 88 $request.='/index/index'; 89 break; 90 case '2': 91 $request.='/index'; 92 break; 93 } 94 $matches=array(); #定义匹配后存贮的数组 95 $temp=array(); #中间缓存数组 96 97 foreach ($this->routes as $v) #开始匹配 98 { 99 preg_match($v, $request, $temp); #需要重点理解这个数组 100 $temp?$matches=$temp:''; 101 } 102 if($matches) #判断$matches是否有数据,无返回false 103 { 104 foreach ($matches as $key => $value) #筛选 105 { 106 if(is_int($key)) 107 { 108 unset($matches[$key]); #除去数字key元素,保留关联元素。与上面的preg_match一起理解 109 } 110 } 111 $result=$matches; 112 if($long > sizeof($result)) #URL参数超过后的处理 113 { 114 $i=1; 115 foreach ($arguments as $k => $v) 116 { 117 if($k > sizeof($result)) 118 { 119 if($i==1) 120 { 121 $result[$v]=''; 122 $temp=$v; 123 $i=2; 124 } 125 else 126 { 127 $result[$temp]=$v; 128 $i=1; 129 } 130 } 131 } 132 } 133 return $result; 134 } 135 return false; 136 } 137 #使参数标准化,不能存在符号,只能是a-zA-Z0-9组合 138 private function _normalize(&$param) 139 { 140 $param=preg_replace("/[^a-zA-Z0-9]/", '', $param); 141 } 142 } 143 /*使用实例: 144 include './Url.class.php'; 145 $router=new Url(); 146 $router->addRoute("/alpha:module/alpha:controller/[0-9]+?:a1a"); 147 $router->addRoute("/alpha:module/alpha:controller/alpha:action"); 148 $router->addRoute("/alpha:module/alpha:controller/alpha:action/(www[0-9]+?):id"); 149 $url=$_SERVER['REQUEST_URI']; 150 $urls=$router->getRoute($url); 151 echo "<pre>"; 152 print_r($urls); 153 echo "</pre>"; 154 */ 155 ?>
代码功能解析:
上面这个Url.class.php类文件代码大概可以分为两部分,在75行即方法getRoute那个地方可以将其分为上半部分和下半部分。
上半部分是方法addRoute,是用来添加路径正则表达式的,并将其存贮在类属性$routes里。
下半部分是方法getRoute,是用来匹配处理访问路径的。即将访问的路径传进来,再与$routes里面的正则表达式进行匹配,成功后再进一步处理,返回处理结果。
针对上面的Url.class.php类文件,我们可以在根目录建立一个test.php测试文件或者直接在index.php文件上测试(方便快捷),帮助我们进一步了解这个类文件的原理与功能。(这里我就不建立test文件啦,直接在index.php文件上进行测试啦)
测试一:
index.php代码如下
1 <?php 2 include "./Framework/Core/Url.class.php"; #载入类文件 3 $router=new Url(); 4 #添加规则 5 $router->addRoute("/alpha:module/alpha:controller/alpha:action"); 6 ?>
再在Url.class.php的addRoute方法中添加一个输出,用来观察,如下图:
开始访问:http://www.test2.com/
结果:
小结:
1、很明显,输出的类属性$routes里面存贮的是正则表达式。
2、私有方法_ParseRoute中,调用了_normalize()方法处理$key,这个方法就是将$key除a-zA-Z0-9以外的符号过滤掉。
3、正则表达式中,【P<'.$key.'>】,是用来后面的preg_match匹配用的,后面讲解。
4、switch中,就是匹配选择正则表达式,可以是已经定义好的,也可以是自己所写。例如:
int代表self::REGEX_INT即正则表达为=》([0-9]+?)
alpha代表self::REGEX_ALPHA即正则表达式=》([a-zA-Z_-]+?)
alphanum代表self::REGEX_ALPHANUMBERIC=》([0-9a-zA-Z_-]+?)
(正则表达式)代表采用自己所写的表达式=》例如:(www[0-9]+?)
因此添加正则路劲addRoute的参数形式:【/int:module/alpha:controller/alphanum:action/(www[0-9]+?):id】任意组合(无数个都可以),冒号后面的参数与preg_match共同使用,后面讲解。
5、$regex.=self::ANY; 这里的作用是用来匹配URL路劲其他参数用的,即http://www.example.com/module/controller/action/var1/values1中,action后面var1、values1等等参数。
测试一结束后,将Url.class.php类文件恢复原状!
测试二:
index.php代码更改如下:
1 <?php 2 header("content-type:text/html;charset=utf8"); 3 include "./Framework/Core/Url.class.php"; #载入类文件 4 $router=new Url(); 5 #添加规则 6 $router->addRoute("/alpha:module/alpha:controller/[0-9]+?:a1a"); 7 $router->addRoute("/alpha:module/alpha:controller/alpha:action"); 8 $router->addRoute("/alpha:module/alpha:controller/alpha:action/(www[0-9]+?):id"); 9 $url=$_SERVER['REQUEST_URI']; 10 echo "<pre>"; 11 echo "index.php第一个输出<br/>"; 12 print_r($url); 13 echo "</pre>"; 14 $urls=$router->getRoute($url); 15 echo "<pre>"; 16 echo "index.php getRoute输出结果 第五个输出<br/>"; 17 print_r($urls); 18 echo "</pre>";die; 19 ?>
再在Url.class.php的getRoute方法中添加以下输出:
第二个输出,用来查看多个正则表达式时$routes的值,如下图:
第三个输出,如下图
第四个输出,如下图
三、四输出是用来查看理解preg_match()函数用的
开始访问:http://www.test2.com/m/c/a/var/value
结果与小结:
1、输出一
此处需要理解体会$_SERVER
2、输出二
当多个路径时,将会全部保存在$routes中
3、输出三与四
这里需要重点讲解preg_matches()功能。
注意:
当使用 PCRE 函数的时候,模式需要由分隔符闭合包裹。分隔符可以使任意非字母数字、非反斜线、非空白字符。如果分隔符经常在 模式内出现, 一个更好的选择就是是用其他分隔符来提高可读性。
由此可以知道$routes中的值@的意思了,就是分隔符,只是我们经常用/而已。
preg_matches()第一参数为正则表达式,此处我们将$routes中的放入进去。
preg_matches()第二参数为需要匹配的数据,这里我们将传入进来的URL放进去(此处URL是输出一的值)。
preg_matches()第二参数为不必要参数,填了此参数,则将匹配成功的值全部放入这个数组中。
preg_matches()在PHP5.2.2是新增了一个小语法,在这里小语法很关键。
假若使用了这个小语法(?P<name>),假若这个子组匹配了的话,那么它会将匹配的数据与这个name参数形成一对关联元素,存贮于preg_matches()的第三参数数组中。这就很好的解释上述addRoute()的参数冒号后的值为何用了,以及的用法。
特别注意:
foreach匹配时,假若$routes含有多个正则表示式时,它将会按顺序一个一个表达式的与URL匹配,若都匹配成功,那么后面的将会覆盖前面的值。
4、输出五
这里就是getRoute()方法处理URL返回的结果。
测试完毕需要将Url.class.php文件恢复原样
到这里整个Url.class.php类文件讲解分析完毕,接下来就是MVC的访问啦。
下面要是简单介绍自动载入类文件,生成对象,并调用方法。
可以看看上面的文件目录来理解下面的各个文件。
index.php文件
1 <?php 2 include './Framework/Core/Core.php'; 3 4 $router=new Url(); 5 $router->addRoute("/alpha:module/alpha:controller/[0-9]+?:a1a"); 6 $router->addRoute("/alpha:module/alpha:controller/alpha:action"); 7 $router->addRoute("/alpha:module/alpha:controller/alpha:action/(www[0-9]+?):id"); 8 9 $url=$_SERVER['REQUEST_URI']; 10 $urls=$router->getRoute($url); 11 $_GET['urls']=$urls; 12 $m=$urls['module']; 13 $c=$urls['controller']; 14 $a=$urls['action']; 15 if($m&&$c) 16 { 17 $autoload=new Autoload($m,$c); 18 $autoload->PutFile(); 19 } 20 $object=new $c; 21 $object->$a(); 22 23 ?>
/Framework/Core/Core.php
1 <?php 2 #核心文件 3 #载入PHP自动加载函数文件 4 include_once(__DIR__."/../Function/AutoLoad.function.php"); 5 ?>
/Framework/Function/AutoLoad.function.php
1 <?php 2 #用于加载核心文件 3 spl_autoload_register('CoreFunction'); 4 function CoreFunction($classname) 5 { 6 $file=__DIR__."/../Core/".$classname.".class.php"; 7 if(is_file($file)) 8 { 9 require_once($file); 10 } 11 } 12 ?>
/Framework/Core/Autoload.class.php
1 <?php 2 #用于加载控制器文件 3 class Autoload 4 { 5 private $path=''; 6 public function __construct($module,$controller) 7 { 8 $this->path=__DIR__."/../../Application/".$module."/Controller/".$controller."Controller.class.php"; 9 } 10 public function PutFile() 11 { 12 if(is_file($this->path)) #判断文件是否存在 13 { 14 require_once($this->path); 15 } 16 else 17 { 18 return false; 19 } 20 } 21 } 22 23 ?>
还有两个模块里面的控制器文件
Home中的TestController.class.php
1 <?php 2 class Test 3 { 4 public $aa=2; 5 public function action() 6 { 7 echo "home---->test----->action"; 8 echo "<pre>"; 9 print_r($_GET); 10 echo "</pre>"; 11 } 12 }
Admin中的TestController.class.php
1 <?php 2 class Test 3 { 4 public $aa=2; 5 public function action() 6 { 7 echo "admin---->test----->action"; 8 echo "<pre>"; 9 print_r($_GET); 10 echo "</pre>"; 11 } 12 }
开始访问:
1、http://www.test2.com/Home/Test/action/var1/vaule1/var2/value2
结果为:
2、http://www.test2.com/Admin/Test/action/var1/vaule1/var2/value2
结果为:
大功告成,码字码到手都累啦。这里URL路径是没有处理大小写的,所以module、controller、action都是对大小写敏感的。
多一点思考、多一点琢磨、多一点敲代码,争取早日迈入大神行列!
下一次打算将它改成存储式的,将addRoute的存入在$routes的正则路径存贮在文件中,getRoute用的时候再取出来。再用apache的ab进行压力测试。
(以上是自己的一些见解,若有不足或者错误的地方请各位指出)
作者:那一叶随风
声明:本博客文章为原创,只代表本人在工作学习中某一时间内总结的观点或结论。转载时请在文章页面明显位置给出原文链接。