一次無心的訪問,點擊到了一個專門作PHP性能測試的網站,看這裏PHP Benchmarks。php
在裏面發現了框架性能測試的結果,發現Laravel的框架性能盡然是最低的。瞬間受到了一萬點的暴擊,誰讓最近一直用Laravel開發項目的呢。html
說到底仍是Laravel好用呀,方便不說,各方面支持的也不錯,業務方面作的也是內部系統,哪怕性能慢點,也能夠用先後端分離、負載均衡等手段解決掉,大致上也是夠用。git
不過,做爲一個開發人員,理想仍是要有的,這時就在想能不能採起Laravel框架的優勢,用到什麼就裝什麼,去掉一些請求到響應之間用不到的組件,精簡框架。github
以前也熟讀過Laravel的源碼,知道它的底層用的是Symfony的組件,畢竟不必重複的造輪子。那麼咱們的框架之旅也將基於Symfony組件。。。web
1、Composer運行機制算法
2、框架前期準備編程
3、HttpFoundation組件封裝Request、Responsejson
4、路由處理後端
5、控制器處理相應功能(C)緩存
6、分離模板(V)
7、分離模型(M)
8、剝離核心代碼
9、優化框架
10、依賴注入(Dependency Injection)
Composer的使用最關鍵的得益於PHP標準規範的出現,特別是其中的psr4,自動加載規範,規範瞭如何指定文件路徑從而自動加載類定義,以及自動加載文件的位置。
既然講到php文件的加載,咱們就要聊一聊PHP的加載機制了。
在早前時,加載文件用的都是include、require,但這種加載有很大的侷限性,相信同窗們都知道,不管用到用不到都要加載大量的文件,至關繁瑣。
因而就出現了autoload加載機制,它能夠實現懶加載。
function __autoload($class) { require_once ($class.".php"); }
當程序引用了未加載的類,就會自動調用__autoload方法,只要維護了__autoload方法,就能夠懶加載文件。
但這裏有一個很大的問題,就是程序中只能定義一次__autoload,這就須要花大盡力在__autoload中維護文件和空間的對應關係,特別是在大型項目,多人合做中更是繁瑣。
而解決這個問題就是SPL Autoload。
SPL Autoload:__autoload調用堆棧。
怎麼理解這個堆棧呢,舉個例子。
現有的框架好比ThinkPHP、Laravel等都有一個vendor目錄,用於存放第三方庫,如今vendor下有兩個庫。
monolog 處理系統日誌
guzzlehttp 處理HTTP
當程序引用這兩個庫的命名空間,並調用monolog、guzzlehttp下面的類時,發現調用的類文件都能被找到。
這主要原理是monolog、guzzlehttp都自定義了相似autoload的方法,而後用spl_autoload_register將方法註冊到了SPL堆棧中。
這樣的話,當程序調用類的時候,就會統一到SPL堆棧中尋找註冊到堆棧中的autoload方法,並加載相應的文件。
以上就是php加載文件的方式,下面就用實戰談一談composer的運行機制。
建立composer項目
# mkdir phoenix # cd phoenix composer init
phoenix是接下來搭建的框架名。
建立成功後,發現當前文件夾下會生成一個composer.json文件,裏面是剛寫入的內容。
composer dump
tree後,就會發現多了一個vendor的目錄,裏面的autoload.php以及composer文件夾下文件就是整個框架的加載核心。
接下來看一遍這些文件。
在整個框架中,第一行必然要引用 vendor/autoload.php 文件,畢竟這是加載核心,那麼就從autoload.php看起。
# autoload.php require_once __DIR__ . '/composer/autoload_real.php'; return ComposerAutoloaderInit599fa618dd1395bdde5fc3a08ff3e4e6::getLoader();
只調用了autoload_real.php裏面的getLoader()方法。
#autoload_real.php 精簡後的代碼 public static function loadClassLoader($class) { if ('Composer\Autoload\ClassLoader' === $class) { require __DIR__ . '/ClassLoader.php'; } } public static function getLoader() { #建立ClassLoader類 spl_autoload_register(array('ComposerAutoloaderInit599fa618dd1395bdde5fc3a08ff3e4e6', 'loadClassLoader'), true, true); #初始化ClassLoader對象(主要就是將命名空間和文件的映射寫入ClassLoader的屬性中) self::$loader = $loader = new \Composer\Autoload\ClassLoader(); spl_autoload_unregister(array('ComposerAutoloaderInit599fa618dd1395bdde5fc3a08ff3e4e6', 'loadClassLoader')); #loadClass方法(相似autoload方法)註冊到 SPL Autoload $loader->register(true); }
autoload_real.php 的做用就是引入ClassLoader類、初始化ClassLoader類,並註冊到SPL堆棧中。
ClassLoader類中有不少屬性,這些屬性的做用也很簡單:主要就是方便後面程序快速的經過命名空間找到它所映射的類文件。
具體用到這些屬性的方法就在ClassLoader類中。
# ClassLoader.php # 一個快速找到文件的算法,頗有意思,感興趣的能夠研究下 # 主要經過首字符找到命名空間以及長度,再根據命名空間以及長度找到文件 private function findFileWithExtension($class, $ext) { ...... }
那麼ClassLoader類屬性裏面的值是何時寫入的呢?
答案很簡單:當爲項目安裝組件時,即composer require xxx時,會更新ClassLoader類的屬性值,也就是將命名空間和文件地址作一個關聯。
接下來看看它的register方法。
# ClassLoader.php public function register($prepend = false) { spl_autoload_register(array($this, 'loadClass'), true, $prepend); }
看,其實很簡單,就是將loadClass註冊到SPL堆棧中。
那麼如今就很清楚了,當程序使用了一個還未加載的類時,會調用什麼方法?
固然是loadClass方法,再來看看loadClass方法。
# ClassLoader.php public function loadClass($class) { if ($file = $this->findFile($class)) { includeFile($file); return true; } }
根據方法的名稱就能看出它的功能:一、找到文件 二、加載文件。
總結一下Composer的運行機制:
一、在composer require安裝時,更新ClassLoader類的屬性 。
二、運行對象時(new \Test()),若是未加載就會執行loadClass(),經過首字符找到命名空間以及長度,再根據命名空間以及長度找到文件,最後include文件。
以上就是Composer的運行機制,接下來,就進入真正的框架搭建了。
在正式進入搭建框架以前,先看下總體的架構圖以及一些前期準備。
整個架構跟Laravel、ThinkPHP等框架是差很少的,一次請求,一次返回,一個入口,中間根據路由規則交給相應的控制器去執行,在控制器中處理數據以及視圖。
接下來作一些前期準備,進入phoenix項目。
# vi index.php 一個入口 ini_set('display_errors', 1); # 顯示錯誤 error_reporting(-1); require_once __DIR__.'/vendor/autoload.php'; # 引入核心加載類 $name = $_GET['name']; dump($name);
# dump() composer require symfony/var-dumper # 相似var_dump,輸出的變量體驗更好些。
配置Nginx,訪問域名爲:http://dev.phoenix.goods/?name=SexyPhoenix, 能夠正常顯示SexyPhoenix。
現有的程序只是一個面向過程的代碼,一個簡單的請求,響應。
對於搭建web框架,這種痛苦寫法固然是要被捨棄的,OOP編程纔是正路。
既然要面向對象編程,首先要作的就是對流程中的Request、Response進行封裝。而Symfony中專門的組件。
composer require symfony/http-foundation
改造代碼
# index.php ini_set('display_errors', 1); error_reporting(-1); require_once __DIR__.'/vendor/autoload.php'; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; $request = Request::createFromGlobals(); # 建立request對象 $name = $request->get('name', 'World'); # 獲取參數,可移入控制器或從模型獲得數據 $response = new Response(); $response->setContent('<b>Hello '.$name.'</b>'); # 設置內容,可用view處理 $response->send(); # 返回
下面來作一個簡單的分析。
$request = Request::createFromGlobals();
這一行代碼,是至關重要的,它從對象層面上處理了php的全局變量,例如 GET,POST,SESSION......。
這樣處理就能夠輕易的從request對象中獲取所須要的信息以及對請求頭等信息的修改。
後期路由這些附加的信息也是存在request的attributes屬性中,及其好用。
$response = new Response();
經過response對象,能夠輕易的控制返回的信息。好比頭信息的各類緩存策略......
從架構圖上看,接着就要處理路由了。
phoneix框架用了廣泛的作法,統一index.php入口。
那麼下面要作的就是如何將路由的附加參數和要處理的控制器進行映射。
對於路由通常框架都是經過配置來的,這裏也同樣作成可配置,方便。
Yaml格式配置路由
在phoenix項目下,建立routes文件夾,在routes下繼續建立web.yaml文件。
dashboard: path: /dashboard defaults: { _controller: 'App\Http\Controllers\DashboardController::index' }
下載symfony的Config組件、Yaml組件、Routing組件。
composer require symfony/config composer require symfony/yaml composer require symfony/routing
更新代碼
# index.php ini_set('display_errors', 1); error_reporting(-1); require_once __DIR__.'/vendor/autoload.php'; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Loader\YamlFileLoader; # add use Symfony\Component\Config\FileLocator; # add $request = Request::createFromGlobals(); $fileLoader = new YamlFileLoader(new FileLocator(array(__DIR__))); # add $collection = $fileLoader->load('routes/web.yaml'); # add $name = $request->get('name', 'World'); $response = new Response(); $response->setContent('<b>Hello '.$name.'</b>'); $response->send();
dump($collection),能夠看到返回了路由的Collection對象,裏面有定義的路由。
這個時候,框架只是獲得了定義的路由,但尚未和URL作映射,下面改造繼續。
URL和配置路由映射
ini_set('display_errors', 1); error_reporting(-1); require_once __DIR__.'/vendor/autoload.php'; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Loader\YamlFileLoader; use Symfony\Component\Config\FileLocator; use Symfony\Component\Routing\RequestContext; # add use Symfony\Component\Routing\Matcher\UrlMatcher; # add $request = Request::createFromGlobals(); $fileLoader = new YamlFileLoader(new FileLocator(array(__DIR__))); $collection = $fileLoader->load('routes/web.yaml'); #解析url $context = new RequestContext(); # add $context->fromRequest($request); # add #初始化UrlMatcher $matcher = new UrlMatcher($collection, $context); # add #url和路由配置映射 $route = $matcher->match($request->getPathInfo()) # add $name = $request->get('name', 'World'); $response = new Response(); $response->setContent('<b>Hello '.$name.'</b>'); $response->send();
繼續分析。
$context = new RequestContext(); $context->fromRequest($request);
context對象主要就是對url進行解析。如今的域名:http://dev.phoenix.goods/dashboard
既然解析出url的參數,就要用解析出的參數和配置中的路由作精準關聯了,初始化matcher,傳入路由配置和url對象。
獲得url和配置中的路由的映射。
$route = $matcher->match($request->getPathInfo());
在路由處理中,框架已經獲得了路由和控制器的關聯關係。下面就要執行相應的控制器(上面的_controller值)。
首先,在phoenix項目下,建立app/Http/Controllers/DashboardController.php(仿造Laravel的目錄結構)。
# DashboardController.php namespace App\Http\Controllers; # 注意這裏App命名空間,本身定義,並無註冊到autoload class DashboardController{ public function index() { echo 'Hello SexyPhoenix'; } }
App命名空間是框架定義的,須要註冊後,才能用,打開項目的composer.json文件。
# composer.json "autoload": { "psr-4": { "App\\": "app/" } }
composer dump-autoload # 更新命名空間
到這裏,控制器的準備工做就作完了,接下來的問題就是若是利用獲得的路由和控制器的映射關係去執行控制器,也就是下面的代碼。
App\Http\Controllers\DashboardController::index
其實也很簡單,就是用"::"分隔,獲得兩個值,一個是類名,一個是方法名,再用php的call_user_func去執行。
但本身去寫可能過去粗暴,可用性低,在執行前,要先判斷DashboardController類是否存在,index方法是否存在,index方法的權限,是不是公共方法,以及各類參數等等,
本身去寫的話,會很麻煩,爲了方便,繼續用symfony的組件。
composer require symfony/http-kernel
http-kernel組件,是框架的內核,很重要的組件,它提供了各類鉤子,及其方便框架擴展,也提供了控制器及其參數的「解析器」(這裏須要瞭解下php的反射機制)。
更新index.php代碼。
# index.php ...... use Symfony\Component\HttpKernel\Controller\ControllerResolver; # add use Symfony\Component\HttpKernel\Controller\ArgumentResolver; # add ...... $route = $matcher->match($request->getPathInfo()); $request->attributes->add($route); # add 將路由映射關係寫入request對象的附加屬性中。 $controller = (new ControllerResolver())->getController($request); # add 處理控制器 $arguments = (new ArgumentResolver())->getArguments($request, $controller); # add 處理方法的參數 $response = call_user_func_array($controller, $arguments); $response->send();
更新DashboardController.php代碼。
namespace App\Http\Controllers; use Symfony\Component\HttpFoundation\Request; # add use Symfony\Component\HttpFoundation\Response;# add class DashboardController{ public function index(Request $request) { $name = $request->get('name', 'world'); # add return new Response('Hello '.$name); # add } }
用http-kernel好處就是能夠處理各類問題,好比Request做爲參數注入。
訪問 http://dev.phoenix.goods/dashboard?name=SexyPhoenix, 獲得 Hello SexyPhoenix。
如今的框架只是簡單的輸出字符串,在正式環境中固然不可能這麼簡單,要可以返回正常的HTML頁面。
而複雜的HTML也不能放在控制器中處理,須要分離出來,單獨處理。Symfony爲框架一樣提供了相關的組件。
composer require symfony/templating
處理框架的目錄結構。
在phoenix項目下,建立resources/views文件夾,繼續在views下建立dashboard.php文件。
# dashboard.php <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Phoenix</title> <style> html, body { color: #000; font-family: 'Raleway', sans-serif; font-weight: 100; height: 100vh; margin: 0; } </style> </head> <body> <div> <h2>Hello, <b><?php echo $name?></b></h2> <h3>your mailbox:<?php echo $email?></h3> <h3>your github:<?php echo $github?></h3> </div> </body> </html>
在app/Http/Controllers下建立Controller.php文件。
# Controller.php namespace App\Http\Controllers; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Templating\PhpEngine; use Symfony\Component\Templating\TemplateNameParser; use Symfony\Component\Templating\Loader\FilesystemLoader; class Controller { /** * $templete 模板文件 * $data 數據 */ public function render($templete, array $data) { return new Response( (new PhpEngine( new TemplateNameParser(), new FilesystemLoader(getcwd().'/resources/views/%name%') )) ->render($templete, $data) ); } }
改造DashboardController.php 代碼。
namespace App\Http\Controllers; use Symfony\Component\HttpFoundation\Request; class DashboardController extends Controller{ # 繼承Controller public function index(Request $request) { $name = $request->get('name', 'world'); $data = [ 'name' => $name, 'email' => 'sexyphoenix@163.com', 'github' => 'https://github.com/SexyPhoenix' ]; return $this->render('dashboard.php', $data); } }
訪問 http://dev.phoenix.goods/dashboard?name=SexyPhoenix, 頁面正常顯示。
分離完模板後,架構的數據仍是在控制器中處理,一樣要作分離。不過這一步,同窗們能夠根據本身的意願來,好比你能夠添加倉庫層、服務層等。
這裏就作簡單點,在app目錄下,建立Models文件夾,繼續建立User.php文件。
# User.php namespace App\Models; class User { protected $emails = []; protected $githubs = []; public function getEmailByName(string $name) { $this->setEmails(); return array_key_exists($name, $this->emails) ? $this->emails[$name] : ''; } public function getGithubByName($name) { $this->setGithubs(); return array_key_exists($name, $this->githubs) ? $this->githubs[$name] : ''; } public function setEmails() { $this->emails = [ 'SexyPhoenix' => 'sexyphoenix@163.com' ]; } public function setGithubs() { $this->githubs = [ 'SexyPhoenix' => 'https://github.com/SexyPhoenix' ]; } }
更新DashboardController.php。
# DashboardController.php ...... use App\Models\User #add ...... public function index(Request $request) { $name = $request->get('name', 'world'); $user = new User(); # add $data = [ 'name' => $name, 'email' => $user->getEmailByName($name), # update 'github' => $user->getGithubByName($name),# update ]; return $this->render('dashboard.php', $data); }
訪問頁面,正常顯示。
框架的基本架構已經搭建完成,但此時的核心代碼都寫在了index.php裏面,另寫項目的話,沒法複用此架構,接下來剝離出核心代碼。
在phoenix項目下建立Core文件夾,繼續建立Phoenix.php文件,移入核心代碼並優化。
# Phoenix.php namespace Core; #注意此命名空間須要註冊 use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Loader\YamlFileLoader; use Symfony\Component\Config\FileLocator; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\Matcher\UrlMatcher; use Symfony\Component\HttpKernel\Controller\ControllerResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver; class Phoenix { public $request; public $routeMap; public function handle(Request $request) { $this->request = $request; try { //url map $this->getRouteMap(); $this->setRequestRoute(); $controller = (new ControllerResolver())->getController($request); $arguments = (new ArgumentResolver())->getArguments($request, $controller); return call_user_func_array($controller, $arguments); } catch(\Exception $e) { return new Response('File Not Found', 404); } } public function setRequestRoute() { $this->request->attributes->add($this->routeMap->match($this->request->getPathInfo())); } public function getRouteMap() { $this->routeMap = new UrlMatcher( $this->getCollection(), (new RequestContext())->fromRequest($this->request) ); } public function getCollection() { return ( new YamlFileLoader( new FileLocator(array(getcwd())) ) )->load('routes/web.yaml'); } }
更新index.php代碼。
ini_set('display_errors', 1); error_reporting(-1); require_once __DIR__.'/vendor/autoload.php'; $kernel = new Core\Phoenix(); $response = $kernel->handle( Symfony\Component\HttpFoundation\Request::createFromGlobals() ); $response->send();
註冊Core命名空間,打開composer.json文件。
# composer.json "autoload": { "psr-4": { "App\\": "app/", "Core\\": "core/" } }
composer dump-autoload # 更新命名空間
刷新頁面,顯示正常。
在前面用到HttpKernel組件時,爲何介紹它是框架的內核呢?
由於HttpKernel裏面有個很重要的概念,派遣事件,給註冊過的不一樣監聽器監聽。
是用Mediator模式設計的,這種模式帶來的好處,就是使框架的擴展性獲得極大的提升。
在請求到響應以前設計了八種鉤子,方便後期擴展,詳情看下面的連接。
同時,也能夠用另外一種監聽事件的方式,經過一個event subscriber(事件訂閱器),向派遣器精確通報它要訂閱哪些事件。下面對路由優化時,會用到這。
HttpKernel組件的功能僅止於此嗎? 固然不,它裏面有一個很重要的類「HttpKernel類」,將框架的核心Core/Phoenix.php的程序都實現了。
只要phoenix框架核心類Phoenix繼承HttpKernel,並調用它的構造方法就好了。
下面來改造Core/Phoenix.php代碼。
# Phoenix.php namespace Core; use Symfony\Component\HttpFoundation\RequestStack; # add use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Loader\YamlFileLoader; use Symfony\Component\Config\FileLocator; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\Matcher\UrlMatcher; use Symfony\Component\EventDispatcher\EventDispatcher; # add use Symfony\Component\HttpKernel\HttpKernel; # add use Symfony\Component\HttpKernel\Controller\ControllerResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpKernel\EventListener\RouterListener; class Phoenix extends HttpKernel{ # 繼承HttpKernel public function __construct() { $matcher = new UrlMatcher($this->getCollection(), new RequestContext()); $requestStack = new RequestStack(); $dispatcher = new EventDispatcher(); $dispatcher->addSubscriber(new RouterListener($matcher, $requestStack)); # 訂閱路由 # HttpKernel的構造函數,能夠點下面的連接進去看看 parent::__construct( $dispatcher, new ControllerResolver(), $requestStack, new ArgumentResolver() ); } public function getCollection() { return ( new YamlFileLoader( new FileLocator(array(getcwd())) ) )->load('routes/web.yaml'); } }
index.php的代碼不用變,HttpKernel類裏面也有handle方法。建議同窗們看看HttpKernel類的源碼。
Phoenix類繼承了HttpKernel,是整個架構的核心,在框架裏面定義了「路由監聽」,但若是框架不只僅要對路由進行監聽,還要對response階段進行監聽呢?是否是繼續修改Phoenix類呢?
這樣的設計對於框架來講,是絕對不友好的。那有沒有方法解決呢?
固然有,能夠經過在外面注入對象,框架經過type檢測,自動引入相關對象。
首先下載Symfony的DependencyInjection組件。
composer require symfony/dependency-injection
在core文件夾下建立container.php文件
# container.php namespace Core; use Symfony\Component\DependencyInjection; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\Routing\Loader\YamlFileLoader; use Symfony\Component\Config\FileLocator; $app = new ContainerBuilder(); $app->register('context', 'Symfony\Component\Routing\RequestContext'); $app->register('matcher', 'Symfony\Component\Routing\Matcher\UrlMatcher') ->setArguments(array(getCollection(), new Reference('context'))); $app->register('request_stack', 'Symfony\Component\HttpFoundation\RequestStack'); $app->register('controller_resolver', 'Symfony\Component\HttpKernel\Controller\ControllerResolver'); $app->register('argument_resolver', 'Symfony\Component\HttpKernel\Controller\ArgumentResolver'); $app->register('listener.router', 'Symfony\Component\HttpKernel\EventListener\RouterListener') # 路由監聽 ->setArguments(array(new Reference('matcher'), new Reference('request_stack'))); $app->register('dispatcher', 'Symfony\Component\EventDispatcher\EventDispatcher') ->addMethodCall('addSubscriber', array(new Reference('listener.router'))); $app->register('phoenix', 'Core\Phoenix') ->setArguments(array( new Reference('dispatcher'), new Reference('controller_resolver'), new Reference('request_stack'), new Reference('argument_resolver'), )); return $app; function getCollection() { return ( new YamlFileLoader( new FileLocator(array(getcwd())) ) )->load('routes/web.yaml'); }
別名和對象一一對應,後面能夠經過別名獲取對象。
去掉core/phoenix.php裏面的代碼。
namespace Core; use Symfony\Component\HttpKernel\HttpKernel; class Phoenix extends HttpKernel{ // public function __construct() // { // $matcher = new UrlMatcher($this->getCollection(), new RequestContext()); // $requestStack = new RequestStack(); // $dispatcher = new EventDispatcher(); // $dispatcher->addSubscriber(new RouterListener($matcher, $requestStack)); // parent::__construct( // $dispatcher, // new ControllerResolver(), // $requestStack, // new ArgumentResolver() // ); // } // public function getCollection() // { // return ( // new YamlFileLoader( // new FileLocator(array(getcwd())) // ) // )->load('routes/web.yaml'); // } }
更新index.php代碼。
ini_set('display_errors', 1); error_reporting(-1); require_once __DIR__.'/vendor/autoload.php'; $app = require_once __DIR__.'/core/container.php'; # add $response = $app->get('phoenix') # 經過別名獲取 ->handle( Symfony\Component\HttpFoundation\Request::createFromGlobals() ); $response->send();
訪問 http://dev.phoenix.goods/dashboard?name=SexyPhoenix, 顯示正常。
到這裏,框架的整個基本設計就結束了,以後須要什麼功能,就能夠本身用composer安裝組件了,composer仍是很好用的。
同窗們若是有什麼疑問的,歡迎在評論區一塊兒交流,ヾ(●´∀`●) 。
最後,附一份最終代碼 phoenix web 架構。