从零实现一个 PHP 微框架 - Bootstrap 启动加载
前言
抽了个空更新一下,给博客除除草。
Laravel 框架中在启动的时候,会依次调用 Illuminate\Foundation\Http\Kernel::$bootstrappers
中的启动类,这些启动类会完成对 .env
文件的加载,配置文件的加载,配置错误处理器,注册 Provider
,并启动 Provider
。XK-PHP 也参考该流程制作了对应的 Bootstrap,但是 XK-PHP 并没有用 Kernel 来加载,而是使用 Application
这个类来作为核心,调控框架。
定义 Bootstrap
首先我们需要创建一个 Bootstrap
接口和抽象类来管理 Bootstrap
,Bootstrap
接 口和抽象类如下:
_8<?php_8_8namespace App\Contracts;_8_8interface Bootstrap_8{_8 public function boot(): void;_8}
_18<?php_18_18namespace App\Bootstrap;_18_18use App\Application;_18_18abstract class Bootstrap implements \App\Contracts\Bootstrap_18{_18 /**_18 * @var Application_18 */_18 protected $app;_18_18 public function __construct(Application $app)_18 {_18 $this->app = $app;_18 }_18}
由于我们在 Bootstrap
中可能需要用到 Application
中的实例或方法,或者需要将将类或实例绑定到 Application
中,我们可以通过构造器传入一个 Application
实例,当 Bootstrap
具体的实现类需要使用的时候就可以直接使用 $this->app
来取得 Application
,而不再需要使用 Application
的静态方法来调用。
编写 Bootstrap
首先我们先看下需要哪些 Bootstrap
,同时需要知道那些需要在 Bootstrap
中加载。
Bootstrap
的作用是在应用启动时对一些配置文件,环境变量,门面,异常处理这些进行读取注册,主要的工作是在 Provider
未开始执行的时候提供 Application
运作的基础服务,而具体的服务,如切面,路由,数据库等不是基础服务,应在 Provider
中进行加载,关于 Provider
我会在下一篇中介绍。
我们直接看看有那些 Bootstrap
吧,Bootstrap
列表是在 Application
中定义的,如下:
_28<?php_28class Application extends Container_28{_28 /**_28 * 存储 App 中所有的单例 instance_28 *_28 * @var Application_28 */_28 public static $app;_28_28 /**_28 * @var string[]_28 */_28 protected $bootstraps = [_28 // 加载 env 文件_28 LoadEnvironmentVariables::class,_28 // 加载配置_28 LoadConfiguration::class,_28 // 注册门面_28 RegisterFacades::class,_28 // 注册异常处理_28 HandleExceptions::class,_28 // 注册服务提供者管理器_28 RegisterProviders::class,_28 // 启动服务_28 BootProviders::class,_28 ];_28}
LoadEnvironmentVariables
该 Bootstrap
主要的工作是加载环境变量和 .env 文件,使用的是 vlucas/phpdotenv
这个库,所以加载的部分就很简单了,直接调用其方法即可。
_15<?php_15_15namespace App\Bootstrap;_15_15use Dotenv\Dotenv;_15_15class LoadEnvironmentVariables extends Bootstrap_15{_15 public function boot(): void_15 {_15 // .env 文件所在的文件夹_15 $dotenv = Dotenv::createImmutable(__DIR__ . '/../../');_15 $dotenv->load();_15 }_15}
LoadConfiguration
然后就是加载配置文件了,之所以不直接使用 env 来作为配置,主要的是 env 并不需要将所有的配置都写全,应用要调用的时候就需要设置默认值,当该配置文件多了,需要改默认值的时候,就容易遗漏,非常不好管理。所以我们需要另外的配置文件来集中的管理。LoadConfiguration
的内容很简单,就是绑定 Config
类实例到容器中,其中配置文件的读取是在 Config
类中完成的。
_13<?php_13_13namespace App\Bootstrap;_13_13use App\Utils\Config;_13_13class LoadConfiguration extends Bootstrap_13{_13 public function boot(): void_13 {_13 $this->app->instance(Config::class, new Config(), 'config');_13 }_13}
_23<?php_23_23namespace App\Utils;_23_23// ..._23_23class Config_23{_23 /**_23 * 配置项,动态读取_23 *_23 * @var array_23 */_23 private static $config;_23_23 public function __construct()_23 {_23 // 匹配 config_path 下的所有 php 文件,并解析存入 $config 静态属性_23 foreach (glob(config_path() . '/*.php') as $path) {_23 self::$config[pathinfo($path, PATHINFO_FILENAME)] = require $path;_23 }_23 }_23}
在 Laravel 中,LoadConfiguration
还进行了许多的操作,比如,如果有 Config 缓存文件则直接读取缓存文件,当没有缓存文件的话,Laravel 才会扫描 Config 目录,在 Laravel 中,Config 是支持文件夹的,不过 XK-PHP 就没对文件夹进行处理。Laravel 在 LoadConfiguration
还会对一些值进行初始化,如设置时区 date_default_timezone_set
,设置编码 mb_internal_encoding
,然后检测是开发环境还是生产环境,并将该值设置到容器。
RegisterFacades
在 Laravel 中,门面是一个很方便的东西,我们可以通过门面快速对方法进行调用,同时也不需要关系我们调用的方法来自哪个类,是静态方法还是实例方法。XK-PHP 也采用这一设计,不过 XK-PHP 也缩减掉了部分功能,比如同一个门面调用不同类的方法等等。
XK-PHP 中注册门面的过程也是很简单,就是将 Application
设置到门面抽象类中的静态属性,如下:
_13<?php_13_13namespace App\Bootstrap;_13_13use App\Facades\Facade;_13_13class RegisterFacades extends Bootstrap_13{_13 public function boot(): void_13 {_13 Facade::setApplication($this->app);_13 }_13}
具体的实现这里就先不说明了,后续讲门面的时候在说吧。
HandleExceptions
接下来就是注册异常处理了,在说明具体的实现之前,我们需要先了解一下 PHP 异常处理相关的函数。
error_reporting
:设置报告的异常等级,0
表示不报告错误,-1
表示报告所有错误。set_error_handler
:设置自定义的错误处理函数,当应用抛出错误没有使用try/catch
捕获的时候,会调用该函数设置的处理函数对错误进行处理。set_exception_handler
:设置自定义的异常处理函数。register_shutdown_function
:设置一个在 PHP 脚本停止时调用的函数。
有了上面这些函数,我们就可以很方便的进行异常处理了:
_87<?php_87_87namespace App\Bootstrap;_87_87// ..._87_87class HandleExceptions extends Bootstrap_87{_87 /**_87 * @var string_87 */_87 protected $env;_87_87 public function boot(): void_87 {_87 $this->env = $this->app->environment();_87 if ($this->env !== 'development') {_87 // 不处于开发环境的时候则设置异常处理_87 error_reporting(-1);_87 set_error_handler([$this, 'handleError']);_87 set_exception_handler([$this, 'handleException']);_87 register_shutdown_function([$this, 'handleDown']);_87 ini_set('display_errors', 'Off');_87 }_87 }_87_87 public function handleError(_87 $level,_87 $message,_87 $file = '',_87 $line = 0,_87 $context = []_87 ): void {_87 // 将 Error 级别的错误转化成 Exception,交给 handleException 处理_87 if (error_reporting() & $level) {_87 throw new ErrorException($message, 0, $level, $file, $line);_87 }_87 }_87_87 public function handleException(Throwable $e): void_87 {_87 // 渲染 Response,并发送_87 $this->renderResponse($e)->send();_87 }_87_87 protected function renderResponse(Throwable $e): Response_87 {_87 try {_87 // 首先获取 Request_87 $request = $this->app->make(Request::class);_87 } catch (\Exception $ex) {_87 // 如果获取 Request 异常了,那也没办法,直接设为 null_87 $request = null;_87 }_87 // 如果不是集成自 XK-PHP 的可渲染异常,那么就包装一下异常_87 if (!$e instanceof \App\Contracts\Exception) {_87 $e = new Exception(Response::$phrases[500], 500, $e->getPrevious());_87 }_87 // 对异常进行渲染,当是浏览器请求的时候返回 HTML,若是异步请求,则返回 JSON_87 return $e->render($request);_87 }_87_87 public function handleDown(): void_87 {_87 // 获取最后一个异常_87 $error = error_get_last();_87 if (_87 $error !== null &&_87 in_array(_87 $error['type'],_87 [E_COMPILE_ERROR, E_CORE_ERROR, E_ERROR, E_PARSE],_87 true_87 )_87 ) {_87 // 如果有异常,则处理异常_87 $this->handleException(_87 new ErrorException(_87 $error['message'],_87 $error['type'],_87 0,_87 $error['file'],_87 $error['line']_87 )_87 );_87 }_87 }_87}
RegisterProviders
注册 Providers,在一些基础服务加载后,就可以注册服务提供者了,XK-PHP 另外写了一个类来管理 Provider
,所以 Bootstrap
的代码也挺简单的:
_20<?php_20_20namespace App\Bootstrap;_20_20use App\Kernel\ProviderManager;_20use function config;_20_20class RegisterProviders extends Bootstrap_20{_20 public function boot(): void_20 {_20 $this->app->setProviderManager(new ProviderManager($this->app));_20 $this->app->getProviderManager()->registers(config('app.providers'));_20 $this->app->instance(_20 ProviderManager::class,_20 $this->app->getProviderManager(),_20 'provider_manager'_20 );_20 }_20}
关于 ProviderManager
的内容会在下一篇讲解,本篇就不做太多的说明了。
BootProviders
最后就是启动 Provider
了:
_11<?php_11_11namespace App\Bootstrap;_11_11class BootProviders extends Bootstrap_11{_11 public function boot(): void_11 {_11 $this->app->getProviderManager()->boot();_11 }_11}
结语
最近学院里安排了考试,同时又安排了 JavaWeb 体验实习的课程(说白了就是培训班的课程,真的浪费时间,会的都会了,不会的这期末开课也没人想听),所以最近没有什么时间来更新文章和项目了,等 6 月底考完后应该就会有比较宽松的时间,虽然说还有体验实 习的课程和对应的课程设计,不过 XK-Java 也已经完成了,写项目会比纯 JavaWeb 写起来容易多了,所以应该还行。

从零实现一个 PHP 微框架 - Bootstrap 启动加载
https://blog.ixk.me/post/implement-a-php-microframework-from-zero-4许可协议
发布于
2020-06-18
本文作者
Otstar Lin
转载或引用本文时请遵守许可协议,注明出处、不得用于商业用途!