摘要:本文將從零開始搭建一個現代化的框架,該框架會擁有現代框架的一切特征,如單入口,路由,依賴注入,類自動加載機制等等,如同時下最流行的框架一樣。執行控制器文件中的邏輯代碼,最終將數據通過對應的視圖層顯示出來。
一、開發環境搭建 1、開發環境搭建本文將從零開始搭建一個現代化的PHP框架,該框架會擁有現代框架的一切特征,如單入口,路由,依賴注入,composer類自動加載機制等等,如同時下最流行的Laravel框架一樣。
這里我們使用 Homestead 來作為我們的集成開發環境,里邊集成了PHP、MySQL我們需要的軟件環境,或者也可以用Xampp集成環境來開發,只要你安裝PHP、MySQL即可,我這里用Homestead做為開發環境。
homestead.yaml配置:
atom ~/.homestead/Homestead.yaml
--- ip: "192.168.10.10" memory: 2048 cpus: 1 provider: virtualbox authorize: ~/.ssh/id_rsa.pub keys: - ~/.ssh/id_rsa folders: - map: ~/Code to: /home/vagrant/Code sites: - map: framework.app # <--- 這里,第五個項目,框架學習開發 to: /home/vagrant/Code/php-framework # <--- 這里 databases: - php-framework variables: - key: APP_ENV value: local # blackfire: # - id: foo # token: bar # client-id: foo # client-token: bar # ports: # - send: 50000 # to: 5000 # - send: 7777 # to: 777 # protocol: udp
重啟vagrant
修改完 Homestead.yaml 文件后,需要重新加載配置文件信息才能生效。
? ~ cd Homestead ? Homestead git:(7924ab4) vagrant reload --provision
修改hosts配置文件
Hosts配置域名在mac的位置: /etc/hosts
192.168.10.10 digtime.app2、開發工具
我們可以選擇 Sublime,Atom,PHPStorm 這些IDE。
二、第一版-實現最基本的功能現在,我們先創建一個簡單的框架,實現MySQLPDO的連接,查詢,創建引導文件,創建項目的配置文件(包括連接數據庫的用戶名和密碼等)
第一版本GitHub地址
三、第二版本-單一入口和mvc架構我們對目錄進行重構,按照MVC功能劃分:
├── index.php ├── config.php ├── controllers ├── core │ ├── bootstrap.php │ └── database │ ├── Connection.php │ └── QueryBuilder.php ├── models │ └── Task.php └── views
現在我們再來添加兩張頁面about.php和contact.php, 按照之前我們說的邏輯層和視圖層分離的原則,我們還需要建立about.view.php和contact.view.php, 并在about.php和contact.php中引入它們的視圖文件。然后我們可以通過http://framework.app/about.php 或 http://framework.app/contact.php 之類的 uri 來訪問這些頁面, 像這種方式我們稱為多入口方式,這種方式對于小型項目還能管理,項目過大了,管理起來就會比較麻煩了。
現在的框架基本都是采用單一入口的模式,什么是單一入口,其實就是整個站點只有 index.php 這一個入口,我們訪問的任何 uri 都是先經過 index.php 頁面,然后在index.php中根據輸入的 uri 找到對應的文件或者代碼運行,然后返回數據。
單一入口思路:
1.訪問http://framework.app/about.php這條路徑時,先進入到 index.php 中
2.然后在 index.php 中會通過一些方法去找到與這條路由對應需要執行的文件,一般我們會把這些文件放到控制器中。
3、執行控制器文件中的邏輯代碼,最終將數據通過對應的視圖層顯示出來。
事實上,我們訪問 http://framework.app/about.php 這個路由時,它真正的路由是 http://framework.app/index.ph...然后通過Apache或者是Nginx做路由跳轉,就可以實現成類式 http://framework.app/about.php 這樣的路由了。
重寫Nginx服務器路由(Homestead 下重寫):
nginx配置url重寫
// Homestead 對每個域名都分配不同的配置
我們對framework.app的Nginx配置進行路由重寫:
cd /etc/nginx/sites-available vagrant@homestead:/etc/nginx/sites-available$ sudo vim framework.app
重寫:
server { listen 80; listen 443 ssl http2; server_name framework.app; root "/home/vagrant/Code/php-framework"; ## 重寫路由 rewrite ^(.*) /index.php?action=$1 last; index index.html index.htm index.php; charset utf-8; location / { try_files $uri $uri/ /index.php?$query_string; } location = /favicon.ico { access_log off; log_not_found off; } location = /robots.txt { access_log off; log_not_found off; } access_log off; error_log /var/log/nginx/framework.app-error.log error; sendfile off; client_max_body_size 100m; location ~ .php$ { fastcgi_split_path_info ^(.+.php)(/.+)$; fastcgi_pass unix:/var/run/php/php7.0-fpm.sock; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_intercept_errors off; fastcgi_buffer_size 16k; fastcgi_buffers 4 16k; fastcgi_connect_timeout 300; fastcgi_send_timeout 300; fastcgi_read_timeout 300; } location ~ /.ht { deny all; } ssl_certificate /etc/nginx/ssl/framework.app.crt; ssl_certificate_key /etc/nginx/ssl/framework.app.key; }
重啟服務器:
sudo service nginx restart;
重寫路由地址后,我們可以直接用 http://framework.app/about 來訪問了:
Nginx 服務器會將訪問的路徑http://framework.app/about 重寫為:http://framework.app/index.php?action=about
如果你的服務器是Apache,則可以在根目錄下增加.htaccess 文件即可:
編寫路由類 RouterRewriteEngine On #如果文件存在就直接訪問目錄不進行RewriteRule RewriteCond %{REQUEST_FILENAME} !-f #如果目錄存在就直接訪問目錄不進行RewriteRule RewriteCond %{REQUEST_FILENAME} !-d #將所有其他URL重寫到 index.php/URL RewriteRule ^(.*)$ index.php?action=$1 [PT,L]
Router.php
[], "POST" => [] ]; public function get($uri, $controller) { $this->routes["GET"][$uri] = $controller; } // 當定義POST路由時候,把對應的$uri和$controller以健值對的形式保存在$this->routes["POST"]數組中 public function post($uri, $controller) { $this->routes["POST"][$uri] = $controller; } /** * 賦值路由關聯數組 * @param $routes */ public function define($routes) { $this->routes = $routes; } /** * 分配控制器路徑 * 通過用戶輸入的 uri 返回對應的控制器類的路徑 * @param $uri * 這里的 $requestType 是請求方式,GET 或者是 POST * 通過請求方式和 $uri 查詢對應請求方式的數組中是否定義了路由 * 如果定義了,則返回對應的值,沒有定義則拋出異常。 * @return mixed * @throws Exception */ public function direct($uri, $requestType) { if(array_key_exists($uri, $this->routes[$requestType])) { return $this->routes[$requestType][$uri]; } // 不存在,拋出異常,以后關于異常的可以自己定義一些,比如404異常,可以使用NotFoundException throw new Exception("No route defined for this URI"); } public static function load($file) { $router = new static; // 調用 $router->define([]); require ROOT . DS . $file; // 注意這里,靜態方法中沒有 $this 變量,不能 return $this; return $router; } }
routes.php 路由文件
get("", "controllers/index.php"); $router->get("about", "controllers/about.php"); $router->get("contact", "controllers/contact.php"); $router->post("tasks", "controllers/add-task.php");
index.php 入口文件
direct(Request::uri(), Request::method());
我們來看一下入口文件index.php,先加載路由文件routes.php,該文件是不是和我們Laravel的一樣呢,根據請求類型進行控制器分配,先把所有請求的路徑根據類型劃分到不同的請求類型屬性(GET,POST)中,然后,再根據請求的路徑來加載對應的控制器。
加載過程詳解:
http://framework.app/about通過GET請求訪問頁面:
1: Router::load("routes.php"),加載所有路由
routes.php
$router->get("", "controllers/index.php"); $router->get("about", "controllers/about.php"); $router->get("contact", "controllers/contact.php"); $router->post("tasks", "controllers/add-task.php");
路由類Router.php
public static function load($file) { $router = new static; // 調用 $router->define([]); require ROOT . DS . $file; // 注意這里,靜態方法中沒有 $this 變量,不能 return $this; return $router; } 此方法等價于: public static function load($file) { $router = new static; // 調用 $router->define([]); // require ROOT . DS . $file; // 這里調用get,post方法進行$routes屬性賦值 $router->get("", "controllers/index.php"); $router->get("about", "controllers/about.php"); $router->get("contact", "controllers/contact.php"); $router->post("tasks", "controllers/add-task.php"); // 注意這里,靜態方法中沒有 $this 變量,不能 return $this; return $router; }
加載路由文件routes.php之后Router.php的$routes屬性結果為:
protected $routes = [ "GET" => [ "" => "controllers/index.php", "about" => "controllers/about.php", "contact" => "controllers/contact.php", ], "POST" => ["tasks" => "controllers/add-task.php"] ];
然后再根據 direct($uri, $requestType)方法獲取對應路徑的控制器路徑,然后 require controllers/about.php.
四、使用composer進行類自動加載我們現在的項目中使用了一堆的require語句, 這樣的方式對項目管理并不是很好,現在有人為 php 開發了一個叫做 composer 的依賴包管理工具,非常好用,我們將其集成進來,composer 官方地址 https://getcomposer.org/ 按照提示進行全局安裝即可。
我們先將 bootstrap.php 中的下面4句類引入代碼注銷
// require "core/Router.php"; // require "core/Request.php"; // require "core/database/Connection.php"; // require "core/database/QueryBuilder.php";
然后在根目錄下建立 coomposer.json 的配置文件,輸入以下內容:
{ "autoload": { "classmap": [ "./" ] } }
上面的意思是將根目錄下的所有的類文件都加載進來, 在命令行執行 composer install 后,在根目錄會生成出一個vendor的文件夾,我們以后通過 composer 安裝的任何第三方代碼都會被生成在這里。
下面在bootstrap.php添加require "vendor/autoload.php"; 即可。我們可以在vendor/composer/autoload_classmap.php文件中查看生成的文件對應關系。
$baseDir . "/core/database/Connection.php", "QueryBuilder" => $baseDir . "/core/database/QueryBuilder.php", "Request" => $baseDir . "/core/Request.php", "Router" => $baseDir . "/core/Router.php", "Task" => $baseDir . "/models/Task.php", );
這里的核心思想是使用了一個 spl_autoload_register() 函數,進行類按需加載,懶加載,即創建對象,然后再加載對象所需要的類文件,而不是之前那種將所有的類文件全部引入,具體請看 詳解spl_autoload_register()函數。
如果新添加了類文件,我們需要運行下面命令進行類自動重新加載:
composer dump-autoload
五、實現依賴注入容器 DI Container注意:以上方法只能將類文件自動加載,其他文件不會進行引入的,如 function.php不會被引入,如果需要,則仍需要使用手動 require 引入。
什么是依賴注入容器 DI Container? 一個聽上去非常高大上的東西,先不要去糾結字面的意思,你可以這么想,把我們的 APP 想象成一個很大的盒子,把我們所寫的一些功能,比如說配置,數據庫操作等都扔到這個盒子里,在扔進去的時候你要給它們貼一個標簽,以后可以通過這個標簽把它們取出來用。大體就是這個意思。
我們來看bootstrap.php 中的代碼, 其實 $app 這個數組就可以看成是一個容器,我們把配置文件扔到數組中,貼上config的標簽(也就是健),把QueryBuilder也扔進去了,貼上標簽database。之后我們可以通過$app["config"]這樣拿出我們需要的值。
我們為何不把$app數組做成一個對象呢! 這樣我們以后可以為其添加很多的屬性和方法,會方便很多,需要對象就必須要有類,我們馬上就可以在core文件夾內建立一個 App.php 的文件,當中包含App類。
下面看看我們需要哪些方法,先看 $app["config"] = require "config.php"; 這一句是把config.php放進到App的容器中,現在常用的說法是 注冊config 到App, 或者是綁定config 到App, 那我們需要的方法可能是這樣的。
$app->bind("config", require "config.php"); // 或者 $app->register("config", require "config.php"); // 或者 App::bind(config", require "config.php"); // 或者 App::register("config", require "config.php");
在我們寫類的時候,可能不知道怎么動手,可以先嘗試著調用假定存在的方法,再回頭去完善類,之前我們也都是這么做的,這樣相對會容易些,上面的幾種方法個人感覺App::bind(config", require "config.php");更好些,然后要取出config可以使用 App::get("config") 方法,下面去實現這兩個方法。在core/App.php 中
class App { protected static $registries = []; public static function bind($key, $value) { static::$registries[$key] = $value; } public static function get($key) { if (! array_key_exists($key, static::$registries)) { throw new Exception("No {$key} is bound in the container."); } return static::$registries[$key]; } }
bootstrap.php 中目前代碼如下:
require "vendor/autoload.php"; App::bind("config", require "config.php"); App::bind("database", new QueryBuilder( Connection::make(App::get("config")["database"]) ));
將所有使用到$app["config"]和$app["database"]的地方全部用App::get("config")和App::get("database")替換過來,毫無疑問的會提示“找不到APP的錯誤”,原因是在我們的autoload_classmap.php文件中并沒有導入App.php文件,我們需要在命令行執行 composer dump-autoload 來重新生成autoload_classmap.php文件。
六、重構控制器 1.新建控制器類現在我們的控制器中的代碼還都是一些面條式的代碼, 并沒有使用面向對象的方式去開發,我們來重構下,我們需要編寫控制器類,然后讓路由指向到對應的控制器的方法,這樣在我們以后的工作流中就會方便很多。
我們在controllers文件夾下建立 PagesController.php 的文件, 編寫以下的代碼,將之前控制器中的文件中的代碼都以方法的形式寫在這個類中
class PagesController { public function home() { $tasks = App::get("database")->selectAll("tasks", "Task"); require "views/index.view.php"; } public function about() { require "views/about.view.php"; } public function contact() { require "views/contact.view.php"; } }
現在可以將controllers文件夾下的index.php, about.php, contact.php都刪除了,將路由文件中的代碼改成下面這樣:
2.更改路由文件$router->get("", "PagesController@home"); $router->get("about", "PagesController@about"); $router->get("contact", "PagesController@contact");3.初次修改 direct() 方法
現在我的意圖是這樣的,以about路由舉例,當我們訪問about, 就會調用PagesController類的about方法, 在about方法中直接運行邏輯代碼。所以我們需要修改Router.php中的direct()方法。
目前direct()是根據相對路徑返回對應控制器類的路徑,然后在入口頁面將其引入進來執行,現在我們只需要通過實例化控制器類,然后調用對應的方法即可。 那direct()的核心代碼應該是類式這樣的:(new PagesController)->about(); 我們暫且把這個功能命名為 callAction() 方法,先將定已經有了這個方法, 我們先去 direct()方法中調用它, 如下:
public function direct($uri, $requestType) { if (array_key_exists($uri, $this->routes[$requestType])) { return $this->callAction("這里應該有參數"); } throw new Exception("No route defined for this URI"); }4.實現私有方法 callAction()
下面考慮下 Router 類中的 callAction() 方法該怎么實現,剛才說了這個方法的核心是 (new Controller)->action(); 不多考慮,我們給這個方法兩個參數,$controller 和 $action, 代碼如下:
private function callAction($controller, $action) { $controllerObj = new $controller; if (! method_exists($controllerObj, $action)) { throw new Exception( "{$controller} does not respond to the {$action} action." ); } return $controllerObj->$action(); }5. ... 運算符和 explode() 函數用法
上面的 method_exists($obj, $action) 方法是判斷一個對象中是否某個方法,那在 direct() 中調用callAction()的參數我們該如何獲取呢? 我們現在的 $this->routes$requestType的值是類式于 PagesController@about 這樣的字符串,我們只需將該值拆分為 ["PagesController", "about"] 這樣的數組,然后使用 php5.6 之后出現的 ...運算符,將其作為參數傳遞,關于拆分字符串為數組,php 也給我們提供了一個這樣的函數,叫做 explode(), 我們先看下這個函數的用法,
打開終端,輸入 php --interactive 進入命令行交互模式
好了,現在就可以修改下direct() 這個方法了,如下:
public function direct($uri, $requestType) { if (array_key_exists($uri, $this->routes[$requestType])) { return $this->callAction( ...explode("@", $this->routes[$requestType][$uri]) ); } throw new Exception("No route defined for this URI"); }
關于...explode("@", $this->routes$requestType) 這里的 ... 操作符, 它會把一維數組中的第一個元素作為參數1, 第二個元素作為參數2,以此類推,這是 php5.6 后新出的語法,可以自己查閱文檔。
6.修改入口頁面的代碼ok, 現在將入口頁面的這句代碼require Router::load("routes.php")->direct(Request::uri(), Request::method());的 require 去掉吧。再測試之前不要忘記了在命令行運行 composer dump-autoload 來重新加載文件。
七、全局函數 view()下面更改下 PagesController 的 require "views/about.view.php"; 這句代碼,我們改成 return view("about"); 這樣,可讀性會好很多。同時在 psr標準中 也有這樣的規定,在聲明一個類的文件中是不能存在 require 代碼的。
我們在core下創建一個functions.php的文件,把所有的全局函數都放在這里,準確來說幫助函數的文件不應該放在這里,它并不屬于核心文件,但是為了我們這里寫的幫助函數基本都是給我們的框架使用的,不設計業務開發,所以暫時還是先放這里。view()函數很簡單,如下:
function view($name) { $name = trim($name, "/"); return require "views/{$name}.view.php"; }
在PagesController的home 方法當中有$tasks對象集合, 我們怎么傳遞它到view()函數中呢? 我們需要給view()設置第二個數組形式的參數,調用view()的時候,將數據以數組的形式傳遞給view()即可,如下:
return view("index", ["tasks" => $tasks]);
現在在view()函數中會出現問題了,我們傳入的數據是一個數組,而在index.view.php中使用的是$tasks這樣的變量,怎么轉化?使用PHP提供的extract()函數可以做到這點,它可以將數組中的元素以變量的形式導入到當前的符號表,這句話不好懂,我們來演示下就明白了,還是進入 php 的命令行交互模式, 如下:
使用了extract()函數就會自動幫我們定義好與數組 key 同名的變量,并將 key 對應的 value 賦值給了該變量,好了,下面我們把view()方法完善下,如下:
function view($name, $data =[]) { extract($data); return require "views/{$name}.view.php"; }八、通過 composer 加載不是類的文件
下面自己把控制器中與view()相關的代碼都更改過來,然后運行composer dump-autoload,它還是會提示找不到view()函數,原因在于我們的composer.json中的配置,我們需要將配置改成下面這樣:
{ "autoload": { "classmap": [ "./" ], "files": [ "core/functions.php" ] } }
上面的classmap只會加載類文件,要加載普通的文件需要使用 "files": [],好了,最后別忘記了composer dump-autoload.
九、控制器和路由的一些命名規范及命名空間控制器和路由我們可以按照Laravel的風格:
// tasks 的列表頁 $router->get("tasks", "TasksController@index"); // TasksController.php class TasksController { public function index() { $tasks = App::get("database")->selectAll("tasks", "Task"); return view("index", compact("tasks")); } public function store() { App::get("database")->create("tasks", [ "description" => $_POST["description"], "completed" => 0 ]); return redirect("/"); } }
從 PHP5.3 開始就支持命名空間了,關于命名空間的介紹看官方文檔: http://php.net/manual/zh/lang... 。其實也很簡單,你把命名空間想象層文件夾就行
本項目Github地址:php-framework
參考文章:論PHP框架是如何誕生的?
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/30590.html
摘要:通過跟蹤請求的處理過程,來對應用系統在前后端處理服務端調用的性能消耗進行跟蹤,關于的介紹可以看這個鏈接,大規模分布式系統的跟蹤系統作者刀把五鏈接來源知乎著作權歸作者所有。 手把手教你搭APM之Skywalking 前言 什么是APM?全稱:Application Performance Management 可以參考這里: 現代APM體系,基本都是參考Google的Dapper(大規模...
摘要:前言一直以來,因為標準應用方式是配合或使用,而被認為不適合做服務化后端。下面我就介紹如何用來搭建一個高性能的服務化后端框架,并且實現一個客戶端調用例子。服務端我使用的框架叫,地址在這里。 前言 一直以來,PHP 因為標準應用方式是配合 php-fpm 或 apache mod 使用,而被認為不適合做服務化后端。但是隨著 Workerman 和 Swoole 這些常駐進程模塊的出現,PH...
摘要:畢竟,我們還將在接下來的開發之旅中使用其他框架開發者編寫的輔助包。缺乏行業標準必然意味著,框架中的這些組件高度耦合。如果你嘗試對這個類進行單元測試,會發現根本不可行。在做單元測試的時候,我們可以很好地模擬數據庫連接,并將其傳入使用。 showImg(https://segmentfault.com/img/remote/1460000014180802); 我為你們準備了一個富有挑戰性...
摘要:一步一步教你基于搭建自己的個人博客,作為成熟的框架,美觀,方便,插件多,更新頻繁,非常適合個人博客與網站的搭建,適合新手,無需太多的代碼基礎。原文鏈接手把手教你搭建自己的網站購買購買云服務器為了搭建個人網站,首先肯定需要一個云服務器。 一步一步教你基于WordPress搭建自己的個人博客,WordPress作為成熟的CMS框架,美觀,方便,插件多,更新頻繁,非常適合個人博客與網站的搭建...
摘要:菜鳥教程框架中文手冊入門目標使用搭建通過對數據增刪查改沒了純粹占行用的拜 后端API入門學習指北 了解一下一下概念. RESTful API標準] 所有的API都遵循[RESTful API標準]. 建議大家都簡單了解一下HTTP協議和RESTful API相關資料. 阮一峰:理解RESTful架構 阮一峰:RESTful API 設計指南 RESTful API指南 依賴注入 D...
閱讀 2593·2023-04-25 20:50
閱讀 3946·2023-04-25 18:45
閱讀 2220·2021-11-17 17:00
閱讀 3330·2021-10-08 10:05
閱讀 3080·2019-08-30 15:55
閱讀 3495·2019-08-30 15:44
閱讀 2360·2019-08-29 13:51
閱讀 1118·2019-08-29 12:47