自己写一个PHP框架思路

2019-03-13  本文已影响0人  xunzhaoanan

在用了一段时间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);
    }
}

components变量就是上面定义的数组,`set()`方法会把我们的组件数组以键值对的方式放在ServiceLocator类的`_definitions`属性中, 当我们需要用到数据库的时候,通常会调用这个方法`Yii::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,那么可以通过app类中的方法实例化index控制器了。

执行控制器方法

在上面的步骤中,我们得到了一个控制器的实例、方法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-Typecharsetcontent等等。
然后将返回结果赋值到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默认错误处理机制,方法就是注册异常,错误,退出处理函数,如下:

只要我们在上述函数中注册了自定义错误处理方法,那么框架里所有的错误类型我们都能处理,达到了监控整个程序流程的目的。

核心组件

完成了前面所说的功能,那么现在我们的框架可以简单的输出一些结果了。但是这是不够的,在业务中,往往会用到数据库、日志系统等等一些功能,这些功能往往是由不同的类来封装的。
前面说到,在框架初始化的时候会有组件配置加载,这些类的配置就是需要被加载的,我们称之为核心组件

核心组件的配置一旦在框架中定义了,就需要写相应的类去实现它的功能;

下面简单聊几个比较重要核心组件。

数据库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思想,谁试谁知道!
目前笔者的按照这个思路开发了一个自己的框架,并且在公司的项目中用到了,实际运行结果良好。

结束!

本文章系博主原创,如需转载,请注明出处。

上一篇 下一篇

猜你喜欢

热点阅读