自己写一个PHP框架思路
在用了一段时间PHP框架后,自己也想着手写一个精简版框架。
前后断断续续经过大半年时间,终于有了小成果,并且成功在公司的项目中跑起来了。
这里简单聊聊编写一个PHP框架的思路,欢迎交流。
(此篇文章以YII2框架为参考)。
框架初始化
框架里最重要的就是核心类(core class)了,核心类中最重要的就是基类了,类似于YII2框架里的Application
类.
基类(后文称作App
类)应该包含能启动框架的功能,它是与业务无关的。每次程序运行的时候,都会加载这个文件,一般在入口文件中被加载(带上配置参数)然后实例化。
下面聊聊App
类初始化需要的一些功能。
定义系统默认常量。
我们经常在入口文件中的第一行就看到开始一些系统常量的定义,在App
类中,可以定义这些常量的默认值。
类的自动加载
在使用PHP主流框架的过程中,当需要使用某个类的某个方法时,往往只需要use一下目标类的命名空间就行了,而不需要引入目标文件,
这在原生环境中是绝对不行的。那么框架是如何做到的呢?答案就是用到了PHP中的一个函数:spl_autoload_register。
在YII2中,是这样使用的:
spl_autoload_register(['Yii', 'autoload'], true, true);
这句话的意思是,当使用一个没有包含的类方法时,框架会定位到Yii类中的autoload()
方法。在此方法中,把需要的类包含进来就可以了。
此函数在手册中有详细的说明,在使用这个函数后,我们也能像其他框架那样自由使用类了。
加载配置&组件
一般情况下,我们会有一个或多个配置文件,此配置文件一般以数组形式存放着系统变量和组件。
在入口文件实例化App
类时,可以在初始化的时候加载这个配置文件,存放在类属性里,这样在程序任何地方,都可以使用到这个配置变量。
普通配置的使用比较简单,无非就是引用App
类的某个属性,这里重点说一下组件的加载和使用。
参考一下YII2,一个典型的例子就是数据库配置。
我们写框架的时候,如果有组件这个概念,那么在使用框架的时候会方便许多,让我们先看看组件是怎么定义的:
'components' => [
'cache' => [
'class' => 'yii\caching\FileCache',
],
'db' => [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=127.0.0.1;dbname=test',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
],
]
这是YII2框架中组件定义的方法,这个数组会在初始化的时候触发setComponents()
函数,此函数如下:
public function setComponents($components)
{
foreach ($components as $id => $component) {
$this->set($id, $component);
}
}
app->db
,其实Yii类中是不存在db这个变量的,此时会调用 核心类的
__get()魔术方法,在
__get()里调用ServiceLocator类
get()`方法。
get()
方法通过获取初始化时注册在_definitions
中的值,实例化了数据库数组中class元素值的类,也就是yii\db\Connection
类,并且返回到Yii::$app->db
,
从而我们就可以通过用Yii::$app->db
调用yii\db\Connection
类中的方法。其他组件也是类似这样去获取的。
所以,我们在自己的框架中也可以像YII2这样引入组件功能,可以不用那么复杂,先写个简单的。
用组件可以使配置统一管理,代码更加整洁;同时使OOP思想更加深刻,特别是在自己写框架时。
记得要定义一个实例化组件(对象)的方法(
createObject()
),把对象放在一个容器类里,通过App
类引用,这样可以在程序任何阶段调用。
run()
在初始化之后,就要执行App
类中的run()
方法进入业务代码了,此方法一般也是在入口文件调用,每次运行代码的时候都会执行一次,下面我们看看run()
方法都有哪些功能。
运行run()
App
类定义的run()
方法作为一个大入口,可以说整个业务代码的生命周期都在这个方法里。
注意run()
方法要在try{...}catch{...}
中完成,因为我们需要记录错误并处理。
解析请求
要执行某个控制器里的某个方法,首先要做的是根据url解析到相应的方法上。解析请求看起来简单,其实根据框架功能丰富程度,也许会很复杂。
假设我们的访问地址如下:
http://127.0.0.1/index/update?id=1
-
获取
route
,也就是除主机名称和查询参数的值,即index/update
。注意url美化的问题,可以通过配置参数,在获取route的时候需要区分pathinfo模式或默认模式。上面的url为美化的url。
-
获取请求参数
params
,数组形式,即Array([id] => 1)
。对于美化的url,这个比较简单,可以直接使用PHP中的
$_GET
变量。
实例化控制器
既然已经获取到了route
,那么可以通过app
类中的方法实例化index控制器了。
-
获取控制器ID,即
index
;以及方法ID,即update
。自己写框架可以自己定义一套获取规则,一般情况下,在
route
中根据字符/
分割出最后一个字符串就是方法ID,其余作为控制器ID。
如果控制器ID中还带有/
字符串,就在多module情况了,需要实现module组件化,最终目的是实例化自定义module(目录)下的控制器。 -
实例化控制器,这里可以使用ReflectionClass函数。
在App
类中,将获取的控制器ID作为参数传入创建对象实例的函数中,在得到控制器的实例后,返回到核心类中,接下来需要用到。$reflection = new ReflectionClass($class); $reflection->newInstanceArgs($args);
在使用反射机制实例化一个类时,要注意命名空间是否正确,更好的做法是在配置中定义好控制器的命名空间,
拼接到控制器ID前面,作为参数传入ReflectionClass()
方法。
执行控制器方法
在上面的步骤中,我们得到了一个控制器的实例、方法ID和请求参数,这里定义一个类似YII2
中的runAction()
的方法(App
类中调用)。
在runAction
中调用函数call_user_func_array。
根据前文的url,假设我们在实际项目中定义了一个IndexController的类,并且在此类中定义一个update的方法。这样,
我们只需要把前文得到的IndexController
(object类型)、update
(string类型)和Array([id] => 1)
(array类型,注意数组格式需要转换成函数需要的)传入下面的函数就行了。
return call_user_func_array([$this->controller, $this->actionMethod], $args);}
这个函数会执行传入类中的指定方法,这样我们框架就实现了根据路由定位到类方法的功能,
如果需要在执行方法前后做点什么,可以在控制器类(IndexController
)的父类中(controller
类)定义beforeAction()
和afterAction()
两个空方法,
然后在controller
类调用runActionWithParam()
的前后加入这两个方法。这样,当我们的IndexController
中包含beforeAction()
或afterAction()
方法时,
就会通过PHP的重写机制,执行我们自己的定义的内容,大致如下:
//这里是controller类的runAction()方法
//执行beforeAction()方法,这里$this为IndexController实例
$this->beforeAction();
//执行用户方法,这里$this为IndexController实例
$this->runActionWithParam();
//执行afterAction()方法,这里$this为IndexController实例
$this->afterAction();
在App
类中也可以按照这样做
//这里是App类的runAction()方法
//运行控制器前置方法,这里$this为App实例
$this->beforeRun();
//运行控制器,这里$this为App实例,在此方法中,调用controller类runAction()方法
$this->runAction();
//运行控制器后置方法,这里$this为App实例
$this->afterRun();
输出结果
在执行了url中的类方法后,会得到一个返回结果,此时我们需要将返回结果经过一些处理后输出。
我们可以定义一个response
类,在此类中定义在html中的一些元素,比如Content-Type
、charset
和content
等等。
然后将返回结果赋值到response
类的content
属性上,经过一系列预处理后通过response
类方法send()
出来。
//前面的步骤可以整理header
//这里是response类的send()部分方法,$this为response实例
echo $this->content;
在渲染视图的时候,可能会用到各种前端框架或组件,这里不是本文讨论的重点,也就是说,
本文所讲的框架更适合作为接口使用。
错误监控
我们是在App
类中执行run()
方法的,作为入口,所以在这里加上代码的监控比较合适。
在代码的运行过程中,可能会出现系统或业务级错误,这个时候需要把错误记录下来并写入日志文件,必要时加入报警机制,方便及时处理。
所以正如前面所说,我们在运行run()
方法时,需要用到try{...}catch{...}
,这样在捕获到错误的时候,可以通过自定义方法处理。
//App类中的run()方法
public function run()
{
try {
//do something
} catch (ExitException $e) {
//这里自定义错误处理,$this为app实例
$this->errorHandler($e);
}
}
这样通过errorHandler()
方法,我们能够自定义处理框架监控到的整个业务代码所throw
的错误。
但是这样还不够,也许在业务代码中,有一些运行中发生中错误并没有被throw
(比如内存不足,磁盘满等等),这些虽然PHP会有报错,但是我们仍然需要捕获并写入日志,方便定位问题。
这里我们需要自定义PHP默认错误处理机制,方法就是注册异常,错误,退出处理函数,如下:
- set_exception_handler() - 设置默认的异常处理程序,用于没有用 try/catch 块来捕获的异常。
- set_error_handler() - 设置用户自定义的错误处理函数。
- register_shutdown_function() - 注册一个会在php中止时执行的函数,用于处理致命错误。
只要我们在上述函数中注册了自定义错误处理方法,那么框架里所有的错误类型我们都能处理,达到了监控整个程序流程的目的。
核心组件
完成了前面所说的功能,那么现在我们的框架可以简单的输出一些结果了。但是这是不够的,在业务中,往往会用到数据库、日志系统等等一些功能,这些功能往往是由不同的类来封装的。
前面说到,在框架初始化的时候会有组件配置加载,这些类的配置就是需要被加载的,我们称之为核心组件。
核心组件的配置一旦在框架中定义了,就需要写相应的类去实现它的功能;
下面简单聊几个比较重要核心组件。
数据库db组件
要在PHP中使用数据库,首先需要数据库扩展。举例来说,Mysql目前有三个扩展:mysql、mysqli和PDO。
由于我们的框架可能不只是用到Mysql,所以我们在框架里用PDO扩展。
一般来说,框架是通过db组件(类)对PDO进行封装的,我们暂且把这个类叫做myConnection
。
myConnection
类中主要要实现的功能是通过配置参数实例化PDO,定义sql的一些组装方法(比如说from()、where()、all()、one()等等)。
如果做到了这些,我们的框架也能像YII2那样,使用自己的语句来表达sql了,比如:
$customer = Customer::find()
->where(['id' => 123])
->one();
这里可以定义一个model
的基类,用Customer
类去继承它。在语句Customer::find()
方法中,我们实际是调用父类model
类里的方法,
在这个方法我们可以实例化myConnection
类。然后通过myConnection
类调用where()
方法,组装sql(sql返回到类属性)。
one()
方法除了继续组装一个完整sql外,还需要实例化PDO并执行sql。
这是一个简单的思路,只要是PDO提供的功能,我们都可以用自己的方法去封装,从而使代码更加简化。
非关系数据库组件也可以按照这样去设计:比如说redis用phpredis扩展以及memcache用memcached扩展来封装组件。
自定义错误组件
前面说过,我们在运行框架的时候会注册PHP错误自定义处理,那么这些错误的处理方法可以放到这个组件里。
我们可以用框架自己的方式展示PHP错误,甚至可以像YII2那样根据是否开启调试决定需不需要在屏幕输出错误。在这些错误处理方法里,再加上写日志功能,基本就完成了。
日志组件
一般程序中的日志有两大作用:一个是记录错误信息,方便定位问题;还有一个就是记录业务数据,用作统计分析等。
所以说,一个框架如果有完善的日志系统,能在以后的开发中省不少时间,提高效率。
日志组件的设计比较简单,主要在属性中定义日志默认路径、错误级别等,方法是写日志、读日志、清除日志甚至是发送远端等等。
文件缓存组件
在有前端页面输出的时候,有时候需要服务端缓存整个页面内容来提高访问速度。随后当同一个页面被请求时,内容将从缓存中取出,而不是重新生成。
所以这里需要一个基于文件缓存的组件,在组件方法里定义生成缓存、读取缓存、设置缓存时间和清除缓存等功能。
记住要根据业务对实时数据不敏感的页面进行缓存。
上面举了几个例子简单说明了一下框架核心组件的设计思路,还有其他的功能就不一一在这里说了,读者可以参考其他的框架来设计自己的组件。
扩展组件
到这里,框架的核心功能基本都设计完毕,也基本能够在项目中跑起来了。如果想使用额外的组件,但又不想包含在最小功能里,我们可以用扩展组件来实现。
所谓最小功能,就像YII2的基本版本和高级版本一样。有时候项目追求速度,只需要最轻量级的代码就能完成,这里我们就可以使用一个不包含扩展组件的框架。
下面举几个例子来说说用得比较多的扩展组件
文件管理组件
文件操作在PHP项目中是比较常见的,一般此类的方法包括读取目录下的文件、判断某个文件是否存在、读取删除文件等等。
图片组件
现在项目中图片的应用的越来越广泛,所以在框架中加上图片处理组件式非常必要的。此组件可以用来生成验证码、二维码、制作缩略图和添加水印等功能。
ftp组件
一些网络服务性的应用或许会用到ftp传输文件的功能。在我们的组件里,可以包含连接ftp服务器、解析ftp字符串、执行上传等功能。
结束语
完成上面的功能后,我们的框架基本就成型了。虽然没有那么多复杂的功能,但是应付平常的项目应该不成问题,
再次说明,这边博文的框架设计思路是根据YII2框架写的,如果读者有更好的思路,欢迎交流。
另外博主自己以后有更好的设计思路,也会在这里更新的。
如果真的能自己写一个框架,对于提升技术水平和扩展知识面是非常有帮助的,特别是可以加深OOP思想,谁试谁知道!
目前笔者的按照这个思路开发了一个自己的框架,并且在公司的项目中用到了,实际运行结果良好。
结束!
本文章系博主原创,如需转载,请注明出处。