1.4 Implementation of router
下一步我们实现简单的路由,来进行
controller
的调用
Step1
:在start_php_framework
根文件夹下新建application
文件夹,然后修改composer.json
,新增一个命名空间app
。修改之后重新在cmd中进入start_php_framework
根文件夹,然后运行composer install
,新命名空间会自动生效。修改的内容如下:
{
...
"autoload": {
"psr-4": {
"core\\": "core",
"app\\": "application"
}
}
...
}
Step2
:在application
文件夹下新建home
文件夹,用于表示默认模块。再在home
文件夹下新建controller
文件夹,这就是接下来的主战场了。
Step3
:在上述controller
文件夹下新建Index.php
,内容如下:
<?php
namespace app\home\controller;
class Index
{
public function index()
{
// 当前目标是刷新浏览器后能输出如下字符串
echo 'Current location: start_php_framework\application\home\controller\Index\index';
}
}
Step4
:实现路由的大体思路是:设置路由表,获取到URI
信息后与路由表进行对比,如果URI
合法,就调用相应的类和方法,否则抛错,找不到页面。路由表中设置两种方式,一种是设置合法的字符串,URI
与字符串进行对比;还有一种是设定合法命名空间,接收到的URI
去判定命名空间下的类及方法是否存在。为了加快判定速度,路由文件设置好之后我们运行一个启动脚本,刷新路由表,直接存储到文件中。当路由配置更新后重新运行脚本,更新文件中的路由信息。于是问题在此退化为:根据路由配置编写脚本,生成路由文件。附一个config/router.php
的内容:
<?php
/**
* 路由表,两种配置方式:
* 第一种:进行字符串的拼接,对比pathinfo
* 第二种:使用命名空间进行配置,那么需要查看类内的方法是否存在
*/
return [
'path' => [
['/', '\app\home\controller\Index\index', 'get'],
['/home/index/index', '\app\home\controller\Index\index', 'get'],
['/home/index/index2', '\app\home\controller\Index\index2', 'get'],
],
'namespace' => [
// 模块级别
'\app\home\controller' => [
// 模块内控制器
'Index' => [
['index', 'get'],
],
],
],
];
Step5
:编写路由更新脚本文件,命名为script_update_router.php
,放置在start_php_framework/web
目录下,其内容如下(编写之后进入start_php_framework/web
目录下执行php script_update_router.php
来生成路由表,执行之后会自动在根目录创建runtime
文件夹及json
格式的路由表):
<?php
require_once '../vendor/autoload.php';
class Router
{
public static function updateRouter()
{
$routerConfig = include_once '../config/router.php';
$router = [];
// 处理字符串类型的路由配置
if (!empty($routerConfig['path'])) {
$router = array_merge($router, self::processPathRouter($routerConfig['path']));
}
// 处理字符串类型的路由配置
if (!empty($routerConfig['namespace'])) {
$router = array_merge($router, self::processNamespaceRouter($routerConfig['namespace']));
}
// var_dump($router);
self::saveRouterToFile($router);
}
/**
* 处理命名空间类型的路由配置
*/
private static function processPathRouter($pathRouter = [])
{
if (empty($pathRouter)) {
return [];
}
$router = [];
foreach ($pathRouter as $kRouter => $vRouter) {
$router[strtolower($vRouter[0])] = $vRouter;
}
return $router;
}
/**
* 处理命名空间类型的路由配置
*/
private static function processNamespaceRouter($nsRouter = [])
{
if (empty($nsRouter)) {
return [];
}
$router = [];
foreach ($nsRouter as $kNs => $vNs) {
foreach ($vNs as $kRouter => $vRouter) {
list($empty, $app, $moduleName) = explode('\\', $kNs);
if ($vRouter[0] == '*') {
// 如果是通配符,就反射获取类下面的public方法,统统放到路由表中
$reflection = new ReflectionClass($kNs . '\\' . $kRouter);
$methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC);
foreach ($methods as $kName => $vName) {
$router[strtolower('/' . $moduleName . '/' . $kRouter . '/' . $vName->name)] = ['/' . $moduleName . '/' . $kRouter . '/' . strtolower($vName->name), $kNs . '\\' . $kRouter . '\\' . strtolower($vName->name), '*'];
}
} else {
foreach ($vRouter as $kName => $vName) {
$router[strtolower('/' . $moduleName . '/' . $kRouter . '/' . $vName[0])] = ['/' . $moduleName . '/' . $kRouter . '/' . strtolower($vName[0]), $kNs . '\\' . $kRouter . '\\' . strtolower($vName[0]), $vName[1]];
}
}
}
}
return $router;
}
/**
* 将路由数组写入文件
*/
private static function saveRouterToFile($routerArr)
{
$routerFile = '../runtime/router/router.json';
if (!file_exists('../runtime/router/')) {
$mkRes = mkdir('../runtime/router/', 0777, true);
if (!$mkRes) {
return '没有相对应的文件夹,并且创建失败';
}
}
$fd = fopen($routerFile, 'w');
fwrite($fd, json_encode($routerArr));
fclose($fd);
}
}
if (file_exists('../config/router.php')) {
Router::updateRouter();
} else {
echo '../config/router.php not exist';
}
Step6
:路由表已经创建,接下来创建start_php_framework/core/Router.php
对其进行读取,内容如下:
<?php
namespace core;
class Router
{
public static function getRouterTable()
{
$routerFile = '../runtime/router/router.json';
$routerTableJson = file_get_contents($routerFile);
$routerTable = json_decode($routerTableJson, true);
// var_dump($routerTable);
return $routerTable;
}
}
Step7
:为了配合测试路由表,更新start_php_framework/application/home/controller/Index.php
的内容,更新之后其内容如下:
<?php
namespace app\home\controller;
// use core\Db;
class Index
{
public function index()
{
echo 'Current location: start_php_framework\application\home\controller\Index\index';
}
public function index2()
{
echo 'Current location: start_php_framework\application\home\controller\Index\index2';
}
private function index3()
{
echo 'Current location: start_php_framework\application\home\controller\Index\index3';
}
}
Step8
:在App.php
中调用Step8
中新建的Router.php
,
更新后的App.php
内容如下:
<?php
namespace core;
use core\Router;
class App
{
public static $cfg;
/**
* 框架运行入口
*/
public static function run()
{
$routerTable = Router::getRouterTable();
$uri = $_SERVER['REQUEST_URI'];
$uri = str_replace('/index.php', '', $uri);
$expRes = explode('/', $uri);
if (isset($expRes[1]) && $expRes[1] != '') {
// 有1就一定有2和3,路由使用严格模式,不进行默认的猜测
$module = $expRes[1];
$controller = $expRes[2];
$function = $expRes[3];
$callStr = strtolower('/' . $module . '/' . $controller . '/' . $function);
if (array_key_exists($callStr, $routerTable)) {
$call = $routerTable[$callStr];
} else {
// TODO.. 后续可以替换为自定义的404页面
http_response_code(404);
die;
}
} else {
$module = '';
$controller = '';
$function = '';
// 未指定模块,调用“/”对应的路由
$call = $routerTable['/'];
}
if (strtolower($_SERVER['REQUEST_METHOD']) != strtolower($call[2])) {
return '请求方式不匹配,请检查URI的请求方式。。';
}
$callInfo = explode('\\', $call[1]);
$functionName = array_pop($callInfo);
$className = implode('\\', $callInfo);
$class = new $className();
$response = $class->$functionName();
}
}
Step9
:关闭入口文件index.php
中的var_dump
,理论上所有的输出以后已经转移到controller
层,关闭后index.php
内容如下:
<?php
// composer自动加载
require_once '../vendor/autoload.php';
// App
require_once '../core/App.php';
\core\App::run();
Step10
:另外再实现一下隐藏url
中的index.php
的功能,在StartPHP/web
目录下新建.htaccess
文件(因为此处我使用的Apache
作为Web Server
,所以rewrite
需要配合.htaccess
文件来实现,Nginx
的话此前给出的样例配置中已经实现了rewrite
),.htaccess
内容如下:
<IfModule mod_rewrite.c>
Options +FollowSymlinks -Multiviews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?/$1 [QSA,PT,L]
</IfModule>
Step11
:浏览器输入http://zsc.spf.com/
和http://zsc.spf.com/home/index/index
会得到一样的输出Current location: start_php_framework\application\home\controller\Index\index
。而访问http://zsc.spf.com/home/index/index2
则会得到输出:Current location: start_php_framework\application\home\controller\Index\index2
。访问http://zsc.spf.com/home/index/index3
则会报404
错误,至此,路由已经初步实现。
项目结构
此时的项目目录结构:
start_php_framework [框架根目录]
├─ application [应用运行主目录]
│ └─ home [默认模块]
│ └─ controller [home模块的控制器文件夹]
├─ config [配置文件目录]
│ ├─ config.php [主配置文件]
│ ├─ db.php [数据库配置文件]
│ └─ router.php [路由配置文件]
├─ core [框架核心源码目录]
│ ├─ db [各类数据库驱动文件存储目录]
│ │ └─ Mysql.php [MySQL连接驱动]
│ ├─ App.php [应用启动文件]
│ ├─ Config.php [读取配置文件]
│ ├─ Db.php [数据库操作文件]
│ └─ Router.php [获取路由表的文件]
├─ runtime [未来存放路由表、运行日志等文件]
│ └─ router [存放路由表的文件夹]
│ └─ router.json [json格式的路由表]
├─ vendor [composer自有文件夹,将来存储第三方扩展]
│ ├─ composer [composer自有文件夹]
│ └─ autoload.php [自动加载关键文件,一定要在入口文件引用,且在App.php之前]
├─ web [框架入口]
│ ├─ .htaccess [Apache重定向描述文件]
│ ├─ index.php [框架入口文件]
│ └─ script_update_router.php [更新路由表的脚本]
└─ composer.json [composer描述文件]