分类
php

徒手实现微框架

在掌握了框架的基础知识之后,我们来实现一个最基础的框架。不要觉得太难,实际上只有二三十行代码。不要觉得太基础,确是框架的核心内容。实际上,大型的框架一方面是堆积了非常完善的类库,另一方面是做了大量的防御性工程,个性化的定制,框架的核心只有几十到几百行代码而已。

使用 Composer 来管理组织代码

Composer 的存在,使得我们可以非常方便的加入第三方的代码库。在安装了 Composer 之后,我们首先初始化项目:

composer init		# 需要填写框架对应的信息,比如作者、最低版本、名字、描述等
composer install

完成之后,我们的项目中就有了如下的目录结构:

  • composer.json  : 项目的基本信息以及依赖信息
  • composer.lock : 项目的依赖的版本锁定文件,当其他人克隆你的项目并 composer install 的时候,会保持版本一致
  • vendor : 项目所有的第三方依赖的代码都会包含其中

现在,我们就可以使用 composer 来管理我们的项目了。为项目创建如下的目录结构:

mkdir App  	# 所有的业务逻辑代码都放在这个目录中
mkdir Configs   # 所有的配置文件都放在这个目录中
mkdir MyPHP		# 所有的框架代码都放在这个目录中
mkdir Tests   # 所有的单元测试代码都放在这个目录中
mkdir Public   # 所有的对外公开的文件都放在这个目录中
touch Public/index.php   # 项目的入口文件

接着,我们需要让 Composer 来加载除了 vendor 目录外,我们自己创建的目录中的代码,按照 psr-4 的标准。编辑 composer.json 文件,加入如下内容:

"autoload": {
	"psr-4": {
		"App\\": "App/",
		"MyPHP\\": "MyPHP/"
	}
}

最后,我们来实现代码的自动加载,编辑入口文件加入如下的代码:

<?php

if (!file_exists('vendor/autoload.php')) {
    die('Please init composer, run `composer init` Comment in CLI.');
}
include 'vendor/autoload.php';		// 使用 Composer 来管理类的自动加载

检查 PHP 的版本

一个框架的编写,会基于一定版本的 PHP 的语法。现在大部分的框架都会基于 PHP7.2 以上的语法来编写,也就是说,这个版本一下的 PHP 是不支持的。所以在一开始,我们就需要去检查 PHP 版本。我们的框架是学习性质的,所以可以把 PHP 的最低版本设置的高一些,基于最新的 PHP8 版本来编写:

<?php
// 检查 PHP 版本
if (version_compare(PHP_VERSION, '8.0', '<')) {
    die('The current PHP version is too low, please upgrade to version 8.0!');
}

实现配置

在项目中,我们可能有非常多的静态的配置需要管理。之前创建的目录结构中,也有 Configs 的目录,用来保存项目中所有的配置文件。我们先来创建一个框架所需的配置文件,命名为 Configs/app.php ,这个配置文件非常简单,就是返回一个数组即可,比如:

<?php

return [
  'app_name' => 'MyPHP',
];

接着,我们创建 MyPHP/Configs.php ,实现加载和读取配置的功能:

<?php
namespace MyPHP;

class Configs
{
    private static array $configs = [];		// 用来存放所有的配置
}

接着,我们来实现加载配置的方法:

<?php

public static function load(array $configs)
{
	self::$configs = array_merge(self::$configs, $configs);
}

就一行代码,将默认的配置和传入的配置进行数组的合并,避免传入的配置覆盖了已经存在的配置。再来,我们来实现读取方法:

<?php
public static function get(string $key)
{
	// 如果传入的 key 为 null 或者为空,则返回所有的配置
	if ($key === null || $key === '') {
		return self::$configs;
	}
	// 如果指定的 key 不存在,则返回为空
	return self::$configs[$key] ?? null;
}

如果没有传入 $key 则返回全部的配置,否则,检查 $key 在配置中是否存在,存在则返回,不存在则返回 null 。最后,我们在入口文件中加载配置即可:

<?php
Configs::get(include 'Configs/app.php');

基本的错误处理

一个工程化的项目,必须要考虑如何处理错误的问题。我们需要对框架中异常进行最终的捕获并处理,以优雅的形式返回给前端:

<?php
set_exception_handler(function (Exception $exception) {
    // 发送了错误进行处理
    header('Content-Type:application/json');
  	// 将异常转换为 json
    echo json_encode([
        'message' => $exception->getMessage(),
        'code' => $exception->getCode(),
    ]);
  	die();		// 终止程序执行
});

实现路由

路由的本质就是通过 URL 中的路由字符串,来找到我们要执行的代码。因此,我们通过 GET 请求中的指定的参数,来获取路由字符串:

<?php
// 获取到的字符串例如 `/Member/User/login`
$routerStr = $_GET['s'];	
// 去除第一个`/`字符之后,使用`explode`方法,将字符串根据`/`分隔为数组,
// 最后分别赋值给 `$module`、`$controller` 和 `$action`变量
[$module, $controller, $action] = explode('/', substr($routerStr, 1, strlen($routerStr)));

比如说,当我们访问 /Member/User/login 的路由的时候,实际上访问的就是 /App/Http/Controller/Member/User.php 类中的 login 方法。当然,在很多的 PHP 框架中,都支持路由表的配置,这个我们在后期实现即可。

执行应用

在获取到路由之后,执行应用也非常容易,这得益于 PHP 这种动态灵活的语言。首先我们拼接出要执行的类的名字,包含命名空间:

<?php
$clazz = "App\\Http\\Controller\\$module\\$controller";

然后我们来执行应用:

<?php
$response = (new $clazz)->$action();

最后,我们来判断 Response 的类型,根据不同的类型,设定不同的 Content-Type :

<?php
if (is_string($response)) {
    header('Content-Type:text/html');
} else if (is_array($response)) {
    header('Content-Type:application/json');
    $response = json_encode($response);		// 如果是数组,则返回 JSON 格式
} else if($response == null) {
    $response = '';			// 如果没有返回,则返回空字符串
}
echo $response;		// 返回响应
exit();		// 退出程序

发表评论

邮箱地址不会被公开。 必填项已用*标注