TP6實(shí)現(xiàn)原理分析系列(一):生命周期

講在前面的話:

無數(shù)人在使用TP6的過程中,被各種問題所困擾。比如:無法根據(jù)錯誤信息快速定位到問題所在,對于手冊模棱兩可的介紹不知所云,對于同事的高級使用技巧不知所措,想擴(kuò)展框架功能卻不知如何下手等等。究其根本原因就是對框架的實(shí)現(xiàn)原理不熟悉,但框架歷經(jīng)十四年發(fā)展,代碼量龐大,紛繁復(fù)雜,無數(shù)人想深入其中一探究竟,但也只能望洋興嘆,止步不前,苦于無人領(lǐng)路。為了徹底扭轉(zhuǎn)這一局面,決定編寫和錄制一套全面、系統(tǒng)、深入介紹TP6實(shí)現(xiàn)原理的文字課程和視頻課程以幫助大家。

本套課程分為10個章節(jié),分別從生命周期、請求與響應(yīng)、數(shù)據(jù)庫、視圖、錯誤和日志、驗(yàn)證、session和cookie、模型、路由、命令行系統(tǒng)、全面、深入介紹框架實(shí)現(xiàn)原理。本章節(jié)為生命周期詳解,分為九小節(jié),分別為應(yīng)用創(chuàng)建、配置加載、服務(wù)注冊、事件、加載中間件、中間件執(zhí)行、路由解析、執(zhí)行控制器方法、響應(yīng)輸出,以下本章節(jié)詳細(xì)內(nèi)容。

1.1 應(yīng)用創(chuàng)建:

TP6框架采用MVC模型組織項(xiàng)目,采用單一入口(所有請求都是請求同一個文件),由路由模塊解析請求,獲取應(yīng)用、控制器、方法,接著執(zhí)行方法,方法返回的內(nèi)容有響應(yīng)對象輸出。整個生命周期的輪廓,在入口文件中一覽無余。代碼如下:

namespace think;
 
require __DIR__ . '/../vendor/autoload.php';
 
// 執(zhí)行HTTP應(yīng)用并響應(yīng)
$http = (new App())->http;
 
// 執(zhí)行應(yīng)用
$response = $http->run();
 
// 內(nèi)容輸出
$response->send();
 
// 結(jié)束應(yīng)用
$http->end($response);

首先創(chuàng)建 App 對象,此對象為框架中最重要的一個對象,管理后續(xù)所創(chuàng)建的系統(tǒng)類對象,相當(dāng)于對象容器。在創(chuàng)建的過程,會做一些初始化工作,主要是系統(tǒng)目錄的獲取,代碼如下:

    // vendor\topthink\framework\src\think\App.php  163行
    public function __construct(string $rootPath = '')
    {
        // 框架核心目錄    src/
        $this->thinkPath   = dirname(__DIR__) . DIRECTORY_SEPARATOR;
        // 應(yīng)用根目錄 比如:frshop/
        $this->rootPath    = $rootPath ? rtrim($rootPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR : $this->getDefaultRootPath();
        $this->appPath     = $this->rootPath . 'app' . DIRECTORY_SEPARATOR;
        $this->runtimePath = $this->rootPath . 'runtime' . DIRECTORY_SEPARATOR;
 
        if (is_file($this->appPath . 'provider.php')) {
            $this->bind(include $this->appPath . 'provider.php');
        }
 
        static::setInstance($this);
 
        $this->instance('app', $this);
        $this->instance('think\Container', $this);
    }

接著創(chuàng)建 HTTP 應(yīng)用,HTTP應(yīng)用的創(chuàng)建比較特殊,通過獲取 APP 對象不存在屬性,從而觸發(fā) __get() 魔術(shù)方法,最終調(diào)用 make() 方法來創(chuàng)建對象,并將對象放置于一個容器中,方便后期的獲取,代碼如下:

// vendor\topthink\framework\src\think\Container.php  239 行
public function make(string $abstract, array $vars = [], bool $newInstance = false)
{
        $abstract = $this->getAlias($abstract);
 
        if (isset($this->instances[$abstract]) && !$newInstance) {
            return $this->instances[$abstract];
        }
 
        if (isset($this->bind[$abstract]) && $this->bind[$abstract] instanceof Closure) {
            $object = $this->invokeFunction($this->bind[$abstract], $vars);
        } else {
            $object = $this->invokeClass($abstract, $vars);
        }
 
        if (!$newInstance) {
            $this->instances[$abstract] = $object;
        }
 
        return $object;
}

緊接著執(zhí)行 HTTP 應(yīng)用類的 run() 方法啟動一個 HTTP 應(yīng)用。

1.2 配置加載:

其實(shí),對于任何一個軟件而言,應(yīng)用執(zhí)行的第一步都是加載配置,當(dāng)然TP6也不例外。TP6的配置加載不局限于配置文件的加載,其中包括環(huán)境變量加載、助手函數(shù)文件加載、配置文件加載、事件文件加載、服務(wù)文件加載等等,代碼如下:

// vendor\topthink\framework\src\think\App.php 493 行
protected function load(): void
    {
        $appPath = $this->getAppPath();
 
        if (is_file($appPath . 'common.php')) {
            include_once $appPath . 'common.php';
        }
 
        include_once $this->thinkPath . 'helper.php';
 
        $configPath = $this->getConfigPath();
 
        $files = [];
 
        if (is_dir($configPath)) {
            $files = glob($configPath . '*' . $this->configExt);
        }
 
        foreach ($files as $file) {
            $this->config->load($file, pathinfo($file, PATHINFO_FILENAME));
        }
 
        if (is_file($appPath . 'event.php')) {
            $this->loadEvent(include $appPath . 'event.php');
        }
 
        if (is_file($appPath . 'service.php')) {
            $services = include $appPath . 'service.php';
            foreach ($services as $service) {
                $this->register($service);
            }
        }
    }

框架配置信息皆由 Config 對象接管,所有配置信息的設(shè)置和獲取都通過 Config 對象所提供的的接口設(shè)置和獲取,其中重要的接口有 has() 、set() 、 get()。

1.3 服務(wù)注冊:

什么是服務(wù)?服務(wù)就是一個插件,用來擴(kuò)展內(nèi)核的功能,分為自定義服務(wù)和內(nèi)置服務(wù),自定義服務(wù)可由命令行的 make 命令創(chuàng)建,在通過配置文件配置,就可被框架加載,配置方式如下:

// app\service.php
 
use app\AppService;
 
// 系統(tǒng)服務(wù)定義文件
// 服務(wù)在完成全局初始化之后執(zhí)行
return [
    AppService::class,
];

注冊方式如下:

// vendor\topthink\framework\src\think\App.php 519行
if (is_file($appPath . 'service.php')) {
     $services = include $appPath . 'service.php';
     foreach ($services as $service) {
          $this->register($service);
     }
}

系統(tǒng)服務(wù)注冊由 RegisterService 類完成,代碼如下:

// vendor\topthink\framework\src\think\initializer\RegisterService.php
/**
 * 注冊系統(tǒng)服務(wù)
 */
class RegisterService
{
 
    protected $services = [
        PaginatorService::class,
        ValidateService::class,
        ModelService::class,
    ];
 
    public function init(App $app)
    {
        $file = $app->getRootPath() . 'vendor/services.php';
 
        $services = $this->services;
 
        if (is_file($file)) {
            $services = array_merge($services, include $file);
        }
 
        foreach ($services as $service) {
            if (class_exists($service)) {
                $app->register($service);
            }
        }
    }
}

1.4 事件:

什么是事件?在某個地點(diǎn),某個時間發(fā)生的一件事。純粹討論事件本身并沒有多大意義的,而是事件發(fā)生后我們?yōu)榇俗鲂┦裁词虏攀怯幸饬x的,這就是事件綁定操作,也就是所謂的事件機(jī)制。事件機(jī)制能靈活擴(kuò)展框架的功能,整個事件機(jī)制的完成需要三步,一、創(chuàng)建操作(或者叫事件監(jiān)聽),二、注冊事件監(jiān)聽,三、觸發(fā)事件,代碼如下:

創(chuàng)建事件監(jiān)聽:

// 事件類可以通過命令行快速生成
php think make:event AppInit
 
// 修改 event.php
return [
    'bind'      => [
    ],
 
    'listen'    => [
        'AppInit'  => ['app\listener\AppInit'],
        'HttpRun'  => [],
        'HttpEnd'  => [],
        'LogLevel' => [],
        'LogWrite' => [],
    ],
 
    'subscribe' => [
    ],
];

注冊事件監(jiān)聽(此步驟由系統(tǒng)完成)

// vendor\topthink\framework\src\think\Event.php  59
public function listenEvents(array $events)
{
        foreach ($events as $event => $listeners) {
            if (isset($this->bind[$event])) {
                $event = $this->bind[$event];
            }
 
            $this->listener[$event] = array_merge($this->listener[$event] ?? [], $listeners);
        }
 
        return $this;
}

觸發(fā)事件(此步驟由程序員完成)

// 觸發(fā)事件
$app->event->trigger(AppInit::class);

1.5 加載中間件:

什么是中間件?顧名思義,就是位于中間的組件,也就是位于生命周期中間的組件,作用就是擴(kuò)展內(nèi)核功能。中間件對于框架十分重要,好幾個重要的工功能都是由中間件完成,像 session、請求緩存等等。我們也可以通過中間件來做權(quán)限的驗(yàn)證,不管是自己定義的中間件還是內(nèi)置的中間件,他們的加載都依賴于配置文件,只有定義在配置文件中的中間件才能加載,代碼如下:

// 全局中間件定義文件  app\middleware.php
return [
    // 全局請求緩存
     \think\middleware\CheckRequestCache::class,
    // 多語言加載
     \think\middleware\LoadLangPack::class,
    // Session初始化
     \think\middleware\SessionInit::class
];

中間價加載,代碼如下:

// 加載全局中間件  vendor\topthink\framework\src\think\Http.php 192 行
$this->loadMiddleware();
 
// loadMiddleware() vendor\topthink\framework\src\think\Http.php 216行
protected function loadMiddleware(): void
{
        if (is_file($this->app->getBasePath() . 'middleware.php')) {
            $this->app->middleware->import(include $this->app->getBasePath() . 'middleware.php');
        }
 }
 
// import() 方法實(shí)現(xiàn) vendor\topthink\framework\src\think\Middleware.php 51行
public function import(array $middlewares = [], string $type = 'global'): void
{
        foreach ($middlewares as $middleware) {
            $this->add($middleware, $type);
        }
 }

中間價由 Middleware 類接管,中間價存儲在 Middleware 對象的 $queue 屬性中。

1.6 執(zhí)行中間件:

中間件的執(zhí)行應(yīng)該是整個框架中最核心的地方,也是最難理解的地方,實(shí)現(xiàn)的非常巧妙,用文字難以表達(dá)清楚,大家可以觀看視頻教程,這里把代碼貼出來。

// 執(zhí)行應(yīng)用程序 vendor\topthink\framework\src\think\Http.php 187 行
protected function runWithRequest(Request $request)
{
        $this->initialize();
 
        // 加載全局中間件
        $this->loadMiddleware();
 
        // 監(jiān)聽HttpRun
        $this->app->event->trigger(HttpRun::class);
 
        // 這段代碼涵蓋生命周期的90%,其中包括中間件的執(zhí)行
        return $this->app->middleware->pipeline()
            ->send($request)
            ->then(function ($request) {
                return $this->dispatchToRoute($request);
            });
}
// 調(diào)度管道 vendor\topthink\framework\src\think\Middleware.php 133 行
public function pipeline(string $type = 'global')
{
        return (new Pipeline())
            ->through(array_map(function ($middleware) {
                // 這里中間件的執(zhí)行邏輯,但中間件并沒有在這里執(zhí)行,只是一個回調(diào)函數(shù)而已
                return function ($request, $next) use ($middleware) {
                    [$call, $params] = $middleware;
                    if (is_array($call) && is_string($call[0])) {
                        $call = [$this->app->make($call[0]), $call[1]];
                    }
                    $response = call_user_func($call, $request, $next, ...$params);
 
                    if (!$response instanceof Response) {
                        throw new LogicException('The middleware must return Response instance');
                    }
                    return $response;
                };
            }, $this->sortMiddleware($this->queue[$type] ?? [])))
            ->whenException([$this, 'handleException']);
}
// 執(zhí)行 vendor\topthink\framework\src\think\Pipeline.php  52 行
// 這段代碼非常的拗口,其難點(diǎn)在于 array_reduce 這個函數(shù),吃透這個函數(shù),就能理解這段代碼
public function then(Closure $destination)
{
        $pipeline = array_reduce(
            array_reverse($this->pipes),
            $this->carry(),
            function ($passable) use ($destination) {
                try {
                    return $destination($passable);
                } catch (Throwable | Exception $e) {
                    return $this->handleException($passable, $e);
                }
            });
 
        return $pipeline($this->passable);
}

1.7 路由解析

什么是路由?說穿了路由就是解決從哪里來要到哪里去的問題,框架的最小執(zhí)行單元是方法,也就是所有的請求最終的落腳點(diǎn)都在方法,但是我們也知道框架是單一入口,所有的請求都是請求同一個文件,那怎么去定位方法呢,這個事由路由完成。路由就是通過解析請求信息,分析出應(yīng)用->控制器->方法,然后調(diào)用方法,并且將方法返回的內(nèi)容交給響應(yīng)。核心代碼如下:

路由調(diào)度

// 通過此方法 將路由解析 從 http 對象轉(zhuǎn)給 route 對象
// vendor\topthink\framework\src\think\Http.php  204 行
protected function dispatchToRoute($request)
 {
        $withRoute = $this->app->config->get('app.with_route', true) ? function () {
            $this->loadRoutes();
        } : null;
 
        return $this->app->route->dispatch($request, $withRoute);
 }
 
 
// 路由調(diào)度 這是路由解析的核心代碼
// vendor\topthink\framework\src\think\Route.php  739 行
public function dispatch(Request $request, $withRoute = true)
{
        $this->request = $request;
        $this->host    = $this->request->host(true);
        $this->init();
 
        if ($withRoute) {
            //加載路由
            if ($withRoute instanceof Closure) {
                $withRoute();
            }
            $dispatch = $this->check();
        } else {
            $dispatch = $this->url($this->path());
        }
 
        $dispatch->init($this->app);
 
        return $this->app->middleware->pipeline('route')
            ->send($request)
            ->then(function () use ($dispatch) {
                return $dispatch->run();
            });
}

解析 url 獲取控制器和方法

// 解析 url vendor\topthink\framework\src\think\route\dispatch\Url.php
protected function parseUrl(string $url): array
{
        $depr = $this->rule->config('pathinfo_depr');
        $bind = $this->rule->getRouter()->getDomainBind();
 
        if ($bind && preg_match('/^[a-z]/is', $bind)) {
            $bind = str_replace('/', $depr, $bind);
            // 如果有域名綁定
            $url = $bind . ('.' != substr($bind, -1) ? $depr : '') . ltrim($url, $depr);
        }
 
        $path = $this->rule->parseUrlPath($url);
        if (empty($path)) {
            return [null, null];
        }
 
        // 解析控制器
        $controller = !empty($path) ? array_shift($path) : null;
 
        if ($controller && !preg_match('/^[A-Za-z0-9][\w|\.]*$/', $controller)) {
            throw new HttpException(404, 'controller not exists:' . $controller);
        }
 
        // 解析操作
        $action = !empty($path) ? array_shift($path) : null;
        $var    = [];
 
        // 解析額外參數(shù)
        if ($path) {
            preg_replace_callback('/(\w+)\|([^\|]+)/', function ($match) use (&$var) {
                $var[$match[1]] = strip_tags($match[2]);
            }, implode('|', $path));
        }
 
        $panDomain = $this->request->panDomain();
        if ($panDomain && $key = array_search('*', $var)) {
            // 泛域名賦值
            $var[$key] = $panDomain;
        }
 
        // 設(shè)置當(dāng)前請求的參數(shù)
        $this->param = $var;
 
        // 封裝路由
        $route = [$controller, $action];
 
        if ($this->hasDefinedRoute($route)) {
            throw new HttpException(404, 'invalid request:' . str_replace('|', $depr, $url));
        }
 
        return $route;
 }

1.8 執(zhí)行控制器方法:

 通過 url 解析之后,我們就可以拿到控制器和方法(如果是多應(yīng)用,那就是應(yīng)用->控制器->方法,url 的解析定義在多應(yīng)用包里面),接下來就是執(zhí)行方法。代碼如下:
// 執(zhí)行路由調(diào)度 vendor\topthink\framework\src\think\route\Dispatch.php
public function run(): Response
{
        if ($this->rule instanceof RuleItem && $this->request->method() == 'OPTIONS' && $this->rule->isAutoOptions()) {
            $rules = $this->rule->getRouter()->getRule($this->rule->getRule());
            $allow = [];
            foreach ($rules as $item) {
                $allow[] = strtoupper($item->getMethod());
            }
 
            return Response::create('', 'html', 204)->header(['Allow' => implode(', ', $allow)]);
        }
 
        // 這里是執(zhí)行控制器方法返回的數(shù)據(jù)
        $data = $this->exec();
        return $this->autoResponse($data);
}

執(zhí)行方法

// 執(zhí)行方法 
// vendor\topthink\framework\src\think\route\dispatch\Controller.php  70行
public function exec()
{
        try {
            // 實(shí)例化控制器
            $instance = $this->controller($this->controller);
        } catch (ClassNotFoundException $e) {
            throw new HttpException(404, 'controller not exists:' . $e->getClass());
        }
 
        // 注冊控制器中間件
        $this->registerControllerMiddleware($instance);
 
        return $this->app->middleware->pipeline('controller')
            ->send($this->request)
            ->then(function () use ($instance) {
                // 獲取當(dāng)前操作名
                $suffix = $this->rule->config('action_suffix');
                $action = $this->actionName . $suffix;
 
                if (is_callable([$instance, $action])) {
                    $vars = $this->request->param();
                    try {
                        $reflect = new ReflectionMethod($instance, $action);
                        // 嚴(yán)格獲取當(dāng)前操作方法名
                        $actionName = $reflect->getName();
                        if ($suffix) {
                            $actionName = substr($actionName, 0, -strlen($suffix));
                        }
 
                        $this->request->setAction($actionName);
                    } catch (ReflectionException $e) {
                        $reflect = new ReflectionMethod($instance, '__call');
                        $vars    = [$action, $vars];
                        $this->request->setAction($action);
                    }
                } else {
                    // 操作不存在
                    throw new HttpException(404, 'method not exists:' . get_class($instance) . '->' . $action . '()');
                }
                
                // 通過反射執(zhí)行 控制器方法
                $data = $this->app->invokeReflectMethod($instance, $reflect, $vars);
 
                return $this->autoResponse($data);
            });
}

1.9 響應(yīng)輸出:

控制器方法執(zhí)行返回的內(nèi)容由響應(yīng)對象接管,響應(yīng)對象經(jīng)過一系列處理之后將內(nèi)容進(jìn)行輸出,輸出之后響應(yīng)對象將會關(guān)閉請求,但此時應(yīng)用并沒有結(jié)束,而是做一些收尾工作,比如 session 寫入、日志寫入,接下來看看詳細(xì)代碼:

接管內(nèi)容

// 接管內(nèi)容 根據(jù)內(nèi)容創(chuàng)建不同的響應(yīng)對象
protected function autoResponse($data): Response
{
        if ($data instanceof Response) {
            $response = $data;
        } elseif (!is_null($data)) {
            // 默認(rèn)自動識別響應(yīng)輸出類型
            $type     = $this->request->isJson() ? 'json' : 'html';
            $response = Response::create($data, $type);
        } else {
            $data = ob_get_clean();
 
            $content  = false === $data ? '' : $data;
            $status   = '' === $content && $this->request->isJson() ? 204 : 200;
            $response = Response::create($content, 'html', $status);
        }
 
        return $response;
 }

輸出

// 輸出內(nèi)容 vendor\topthink\framework\src\think\Response.php 128
public function send(): void
{
        // 處理輸出數(shù)據(jù)
        $data = $this->getContent();
 
        if (!headers_sent() && !empty($this->header)) {
            // 發(fā)送狀態(tài)碼
            http_response_code($this->code);
            // 發(fā)送頭部信息
            foreach ($this->header as $name => $val) {
                header($name . (!is_null($val) ? ':' . $val : ''));
            }
        }
        if ($this->cookie) {
            $this->cookie->save();
        }
 
        $this->sendData($data);
 
        // 關(guān)閉請求
        if (function_exists('fastcgi_finish_request')) {
            // 提高頁面響應(yīng)
            fastcgi_finish_request();
        }
 }

收尾

// 收尾    vendor\topthink\framework\src\think\Http.php  271 行
public function end(Response $response): void
{
        $this->app->event->trigger(HttpEnd::class, $response);
 
        //執(zhí)行中間件
        $this->app->middleware->end($response);
 
        // 寫入日志
        $this->app->log->save();
 }

整個生命周期的介紹到這里就全部結(jié)束了,感謝您的閱讀。

讀完此文,如果感覺意猶未盡,看移步觀看視頻教程,并且可以和作者一對一交流。

地址:https://edu.csdn.net/course/detail/28045

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容