likes
comments
collection
share

如何设计一个自己的PHP框架

作者站长头像
站长
· 阅读数 46

如何开发一个自己的PHP框架

目前市面上已经有很多成熟的PHP框架,例如ThinkPHP、Laravel、Yii等,相信很多小伙伴在使用它们时都会有一些疑问:这些框架有什么不同啊?它们是如何运转的?以及我能不能自己来实现一个框架等等。那么本文将为大家讲述如何开发一个属于自己的PHP框架,揭开框架的神秘面纱。

我们现在可以思考一下,一个最简单最基本的框架需要具备什么功能?是不是只需要通过url找到对应的控制器以及方法并实现这个接口的具体逻辑就好了,那么这个步骤就涉及到一个很关键的点了:类的自动加载。下面我们就通过类的自动加载来带着大家设计一个自己的PHP框架。

类的自动加载

打开PhpStorm,这里我新建了一个文件夹并创建了Index.php以及Api.php,然后在Api.php文件中定义一个Api的类。代码如下:

Index.php:

<?php

$api = new Api();

Api.php:

<?php

class Api
{

}

此时执行php Index.php命令我们会发现PHP抛了一个Class 'Api' not found的致命错误:

PHP Fatal error:  Uncaught Error: Class 'Api' not found in /Users/locust/Desktop/php/Index.php:3
Stack trace:
#0 {main}
  thrown in /Users/locust/Desktop/php/Index.php on line 3

Fatal error: Uncaught Error: Class 'Api' not found in /Users/locust/Desktop/php/Index.php:3
Stack trace:
#0 {main}
  thrown in /Users/locust/Desktop/php/Index.php on line 

这是因为我们在使用Api类的时候并没有引入Api.php文件,导致我们的程序在执行时找不到这个类。通常的解决办法是在Index.php中加一行require_once Api.php代码,此时我们再来执行php Index.php就没有错误了。但是在实际开发中我们基本上不会主动用到include(include_once)/require(require_once)关键字来加载类,因为这样做会使得代码维护相当的困难,我们更希望在需要的时候直接new这个类就好了,那么有什么解决办法呢?

spl_autoload_register

这个时候我们就要引入一个php内置函数spl_autoload_register了,查询官网得知spl_autoload_register() 函数可以注册任意数量的自动加载器,当使用尚未被定义的类(class)和接口(interface)时自动去加载。

spl_autoload_register(callable $autoload_function = ?, bool $throw = true, bool $prepend = false): bool

我们重点关注$autoload_function这个参数,它是一个callable类型,当php发现一个尚未定义的类或接口时就会执行这个回调函数,该回调需要传入一个string类型的参数,这个参数值便是未定义的类或接口名了。

还是用刚刚的两个文件我们来实操一下,在Index.php文件中我们使用spl_autoload_register这个函数并实现了回调,该回调中会输出$className,对应该例子期望值便是Api了。

<?php

spl_autoload_register(function (string $className) {
	echo $className . PHP_EOL;
	exit();
}, true, true);

$api = new Api();

再来执行php Index.php命令,程序输出了Api这个值,符合预期。

通过类名引入该类所在文件

我们再来改造一下Index.php代码:

<?php

spl_autoload_register(function (string $className) {
	if ($className == 'Api') {
		include_once "Api.php";
	}
}, true, true);

$api = new Api();

这样便实现了类的自动加载,可是在实际开发中我们肯定不止用到一个类,如果每新增一个需要加载的类就得写一个if else分支,代码维护工作依旧困难。那么有没有办法不写那么多if else也能让程序知道每个类所在的文件呢?我们可以看看开源框架是怎么做的,这里我以ThinkPhp5.1为例。

打开项目,假设我们现在需要加载Index这个类,它的类名为app\index\controller\Index,所在文件路径为__di r__/application/index/controller/inedx.php(__dir__是项目根目录)。然后我们在/vendor/composer下找到autoload_static.php这个文件,可以看到里面定义了$prefixLengthsPsr4以及$prefixDirsPsr4这两个静态变量:

public static $prefixLengthsPsr4 = array (
        't' => 
        array (
            'think\\composer\\' => 15,
        ),
        'a' => 
        array (
            'app\\' => 4,
        ),
    );

    public static $prefixDirsPsr4 = array (
        'think\\composer\\' => 
        array (
            0 => __DIR__ . '/..' . '/topthink/think-installer/src',
        ),
        'app\\' => 
        array (
            0 => __DIR__ . '/../..' . '/application',
        ),
    );

然后再找到Loader类中的findFile方法,可以看到该方法中有以下代码块:

// 查找 PSR-4
		$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . '.php';
		$first = $class[0];

		if (isset(self::$prefixLengthsPsr4[$first])) {
			foreach (self::$prefixLengthsPsr4[$first] as $prefix => $length) {
				if (0 === strpos($class, $prefix)) {
					foreach (self::$prefixDirsPsr4[$prefix] as $dir) {
						if (is_file($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
							return $file;
						}
					}
				}
			}
		}

结合代码我们来看看TP5.1是如何加载Index这个类的:首先我们知道$classapp\index\controller\Index,其所在文件路径为__dir__/application/index/controller/inedx.php$logicalPathPsr4等于app/index/controller/Index.php,进入foreach循环拿到$prefixapp$length4,因为$prefix的值是$class的子串,所以会进入第二个foreach循环并拿到dir值为__dir__/application/。可能有点乱,没关系,我们来梳理一下刚刚提到的变量以及变量值:

$class = 'app\index\controller\Index';
$classDir(Index类所在文件路径)= '__dir__/application/index/controller/inedx.php';
$first = 'a';
$logicalPathPsr4 = 'app/index/controller/Index.php';
$prefix = 'app\\';
$length = 4;
$dir = '__dir__/application/';

可以发现只要将$logicalPathPsr4变量中的app/截取掉并再和$dir做个字符串拼接就能得到$classDir了。

再来看看代码块中的最后一个if判断if (is_file($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length)))。没错,TP5.1就是这样实现的。

通过路由调用目标类以及方法

在使用其他开源框架时我们每新增一个接口都会去路由文件中定义该接口的请求类型以及所在控制器和方法,这个实现思路就很简单了,我们可以定义一个map并存储上述关系。

class Route
{
	public static $routes = [
		'user/info' => [
			'GET',
			'app\controllers\User@getUserInfo'
		]
	];
}

这里我定义了一个user/info的接口并填充了路由相关信息,然后在框架入口处获取该路由信息并实现相关逻辑就可以完成一个最基本的框架了。

public static function run ()
	{
		$uri = $_SERVER['REQUEST_URI'];
		$method = $_SERVER['REQUEST_METHOD'];
		// 假设我们的框架仅支持GET以及POST两种请求方式
		if ($method == 'GET') {
			$uri = explode('?', $uri)[0];
			$data = $_GET;
		} else {
			$data = $_POST;
		}

		if (empty(Route::$routes[$uri]) || Route::$routes[$uri][0] != $method) {
			header("HTTP/1.1 " . 404);
			return;
		}

		list($controller, $function) = explode('@', Route::$routes[$uri][1]);
		if (!class_exists($controller)) {
			header("HTTP/1.1 " . 500);
			return;
		}
		$instance = new $controller();
		if (!method_exists($instance, $function)) {
			header("HTTP/1.1 " . 500);
			return;
		}

		$resp = $instance->$function($data);
		echo json_encode($resp);
	}

然后配置一下nginx

server {
         listen       8084;
         server_name  localhost;
         location / {
             fastcgi_pass   127.0.0.1:9000;
             fastcgi_param  SCRIPT_FILENAME  /Users/locust/Desktop/php/Index.php;
             include        fastcgi_params;
         }
 }

使用Postman发起一个http请求

如何设计一个自己的PHP框架

至此一个最简单的框架就开发完成了。当然了,现有功能肯定无法满足咱们的日常开发需求,不过只要完成上述功能模块,其他的就好办了。

本demo代码:github.com/Colocust/ph…

广告

tiny2.0是我两年前开发的一个PHP框架,除上述功能外该框架还实现了API类的封装、参数自动校验、接口文档的一键生成等功能,感兴趣的同学可以看看。

转载自:https://juejin.cn/post/7093525226973036575
评论
请登录