写在前面
本人开发的框架在2021年年初开发完成,后面没有再做过任何维护和修改。是仅供大家参考交流的学习项目,请勿使用在生产环境,也勿用作商业用途。
框架地址: github.com/yijiebaiyi/...
整体思路
开发一款web框架,首先要考虑这个框架的整体运行架构,然后具体到那些功能的扩展。那么我开发框架的时候想的是,精简为主,实用为主。主要功能需要包括入口文件、路由解析、异常处理、日志记录、ORM、缓存、类依赖注入。
入口文件
入口文件需要定义全局变量,主要是核心框架文件的所在路径,然后,通过include_once引入框架核心类文件,初始化框架进行初始化操作。
PHP
<?php
define("FAST_PATH", $_SERVER["DOCUMENT_ROOT"] . DIRECTORY_SEPARATOR . "fast");
// 初始化
include_once FAST_PATH . DIRECTORY_SEPARATOR . "App.php";
(new \fast\App())->init();
应用核心类
应用核心类主要是用来注册类的自动加载、加载环境变量文件、注册错误异常以及注册路由。下面是应用初始化init方法。
PHP
public function init()
{
if (false === $this->isInit) {
define("DOCUMENT_ROOT", $_SERVER["DOCUMENT_ROOT"]);
define("ROOT_PATH", $_SERVER["DOCUMENT_ROOT"]);
define("RUNTIME_PATH", $_SERVER["DOCUMENT_ROOT"] . DIRECTORY_SEPARATOR . "runtime");
define("APP_PATH", $_SERVER["DOCUMENT_ROOT"]);
// 注册自动加载
require_once FAST_PATH . DIRECTORY_SEPARATOR . "Autoload.php";
(new Autoload())->init();
// 注册配置
(new Config())->init();
// 加载env
(new Env())->init();
// 注册错误和异常
(new Exception())->init();
(new Error())->init();
(new Shutdown())->init();
// 检验运行环境
$this->validateEnv();
// 注册路由
(new Route())->init();
$this->isInit = true;
}
}
上面初始化的方法中,我们需要先判断框架是否已经初始化,如果已经初始化则不需要再进行操作了。init方法中所涉及到的类都在框架核心文件根目录下面,需要注意的是,一定要先注册自动加载,不然使用new 关键字生成对象就会报错。下面是自动加载类的自动加载方法。
PHP
public function init()
{
if (false === $this->isInit) {
spl_autoload_register(array($this, 'autoload'));
$this->isInit = true;
}
}
/**
* @var array 类加载次
*/
private static array $loadedClassNum = [];
/**
* 自动加载
* @param $name
* @throws Exception
*/
public static function autoload($name): void
{
if (trim($name) == '') {
throw new Exception("No class for loading");
}
$file = self::formatClassName($name);
if (isset(self::$loadedClassNum[$file])) {
self::$loadedClassNum[$file]++;
return;
}
if (!$file || !is_file($file)) {
return;
}
// 导入文件
include $file;
if (empty(self::$loadedClassNum[$file])) {
self::$loadedClassNum[$file] = 1;
}
}
/**
* 返回全路径
* @param $className
* @return string
*/
private static function formatClassName($className): string
{
return $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . $className . '.php';
}
使用PHP提供的spl_autoload_register自动加载器函数,注册autoload方法实现自动加载,可以看到我们自动加载的类必须都在项目根目录下才可以实现。这是一个简单的约定。
加载配置
我们知道php使用include 导入文件是可以获取到文件的返回值的(如果有的话),所以使用php文件返回一个数组来实现项目的配置文件,框架里面支持默认的config.php文件,以及额外用户可以自定义的配置:extra.php。这个也是我们约定好的。
配置文件示例代码config.php:
PHP
<?php
return [
"Cache" => [
"default" => "redis",
"redis" => [
"master" => [
"pconnect" => false,
"host" => "localhost",
"port" => 6379,
"timeout" => 0,
],
],
],
"Log" => [
"default" => "file",
"file" => [
"path" => RUNTIME_PATH
],
]
];
引入配置文件的关键代码:
PHP
/**
* 加载配置
* @param $filename
*/
private static function addConfig($filename): void
{
$configArr = include_once($filename);
if (is_array($configArr)) {
self::$configs = Arr::arrayMergeRecursiveUnique(self::$configs, $configArr);
}
}
/**
* 导入配置
* @param $paths
*/
private static function importConfig($paths): void
{
foreach ($paths as $path) {
self::addConfig($path);
}
}
加载环境变量
环境变量文件,我们默认的就是项目根目录的.env文件。.env文件配置项是标准的*.ini类型配置文件的书写方式,且.env文件里面的配置项不区分大小写 ,小写配置项最终会被转化成大写。.env文件的加载使用php的函数parse_ini_file来实现:
PHP
/**
* 加载环境变量定义文件
* @param string $file 环境变量定义文件
* @return void
*/
public static function load(string $file): void
{
$env = parse_ini_file($file, true) ?: [];
static::set($env);
}
框架支持环境变量的写入、读取和检测。
错误和异常
异常信息抓取到之后,我们将他格式化处理,主要记录异常码、异常文件和所在行号。然后将异常写入日志。(注意,如果是生产模式,需要关闭错误显示)
PHP
public static function handler($exception)
{
// 设置http状态码,发送header
if (in_array($exception->getCode(), array_keys(Http::$httpStatus))) {
self::$httpCode = $exception->getCode();
} else {
self::$httpCode = 500;
}
Http::sendHeader(self::$httpCode);
// 异常信息格式化输出
$echoExceptionString = "<b>message</b>: {$exception->getMessage()}<br/>" .
"<b>code</b>: {$exception->getCode()}<br/>" .
"<b>file</b>: {$exception->getFile()}<br/>" .
"<b>line</b>: {$exception->getLine()}<br/>";
$serverVarDump = Str::dump(false, $_SERVER);
$postVarDump = Str::dump(false, $_POST);
$filesVarDump = Str::dump(false, $_FILES);
$cookieVarDump = Str::dump(false, $_COOKIE);
$logExceptionString = "message: {$exception->getMessage()}" . PHP_EOL .
"code: {$exception->getCode()}" . PHP_EOL .
"file: {$exception->getFile()}" . PHP_EOL .
"line: {$exception->getLine()}" . PHP_EOL .
"\$_SERVER: {$serverVarDump}" . PHP_EOL .
"\$_POST: {$postVarDump}" . PHP_EOL .
"\$_COOKIE: {$cookieVarDump}" . PHP_EOL .
"\$_FILES: {$filesVarDump}";
Log::write($logExceptionString, Log::ERROR);
// debug模式将错误输出
if (static::isDebugging()) {
if (self::$isJson) {
echo Json::encode(["message" => $exception->getMessage(), "code" => 0]);
App::_end();
} else {
echo $echoExceptionString;
}
}
}
路由分发
路由的实现思路是:我们根据请求的地址,截取到请求的路径信息(根据PHP全局变量$_SERVER['PATH_INFO']获取),根据路径信息的格式,定位到某个控制器类的某个方法,然后将其触发。实现代码:
PHP
public function distribute()
{
// 解析path_info
if (isset($_SERVER['PATH_INFO'])) {
$url = explode('/', trim($_SERVER['PATH_INFO'], "/"));
if (count($url) < 3) {
$url = array_pad($url, 3, "index");
}
} else {
$url = array_pad([], 3, "index");
}
// 获取类名和方法名
$className = self::formatClassName($url);
$actionName = self::formatActionName($url);
if (!class_exists($className)) {
throw new Exception("the controller is not exist: {$className}", 404);
}
$class = new $className();
if (!is_callable([$class, $actionName])) {
throw new Exception("the action is not exist: {$className} -> {$actionName}", 404);
}
if (!$class instanceof Controller) {
throw new Exception("the controller not belongs to fast\\Controller: {$className}", 403);
}
// 将请求分发
$class->$actionName();
}