寫在前面
本人開發(fā)的框架在2021年年初開發(fā)完成,后面沒有再做過任何維護(hù)和修改。是僅供大家參考交流的學(xué)習(xí)項(xiàng)目,請勿使用在生產(chǎn)環(huán)境,也勿用作商業(yè)用途。
框架地址:
https://github.com/yijiebaiyi/fast_framework
整體思路
開發(fā)一款web框架,首先要考慮這個(gè)框架的整體運(yùn)行架構(gòu),然后具體到那些功能的擴(kuò)展。那么我開發(fā)框架的時(shí)候想的是,精簡為主,實(shí)用為主。主要功能需要包括入口文件、路由解析、異常處理、日志記錄、ORM、緩存、類依賴注入。
入口文件
入口文件需要定義全局變量,主要是核心框架文件的所在路徑,然后,通過include_once引入框架核心類文件,初始化框架進(jìn)行初始化操作。
<?php
define("FAST_PATH", $_SERVER["DOCUMENT_ROOT"] . DIRECTORY_SEPARATOR . "fast");
// 初始化
include_once FAST_PATH . DIRECTORY_SEPARATOR . "App.php";
(new \fast\App())->init();
應(yīng)用核心類
應(yīng)用核心類主要是用來注冊類的自動加載、加載環(huán)境變量文件、注冊錯(cuò)誤異常以及注冊路由。下面是應(yīng)用初始化init方法。
public function init()
{
if (false === $this->isInit) {
define("DOCUMENT_ROOT", $_SERVER["DOCUMENT_ROOT"]);
define("ROOT_PATH", $_SERVER["DOCUMENT_ROOT"]);
define("RUNTIME_PATH", $_SERVER["DOCUMENT_ROOT"] . DIRECTORY_SEPARATOR . "runtime");
define("APP_PATH", $_SERVER["DOCUMENT_ROOT"]);
// 注冊自動加載
require_once FAST_PATH . DIRECTORY_SEPARATOR . "Autoload.php";
(new Autoload())->init();
// 注冊配置
(new Config())->init();
// 加載env
(new Env())->init();
// 注冊錯(cuò)誤和異常
(new Exception())->init();
(new Error())->init();
(new Shutdown())->init();
// 檢驗(yàn)運(yùn)行環(huán)境
$this->validateEnv();
// 注冊路由
(new Route())->init();
$this->isInit = true;
}
}
上面初始化的方法中,我們需要先判斷框架是否已經(jīng)初始化,如果已經(jīng)初始化則不需要再進(jìn)行操作了。init方法中所涉及到的類都在框架核心文件根目錄下面,需要注意的是,一定要先注冊自動加載,不然使用new 關(guān)鍵字生成對象就會報(bào)錯(cuò)。下面是自動加載類的自動加載方法。
public function init()
{
if (false === $this->isInit) {
spl_autoload_register(array($this, 'autoload'));
$this->isInit = true;
}
}
/**
* @var array 類加載次
*/
private static array $loadedClassNum = [];
/**
* 自動加載
* @param $name
* @throws Exception
*/
public static function autoload($name): void
{
if (trim($name) == '') {
throw new Exception("No class for loading");
}
$file = self::formatClassName($name);
if (isset(self::$loadedClassNum[$file])) {
self::$loadedClassNum[$file]++;
return;
}
if (!$file || !is_file($file)) {
return;
}
// 導(dǎo)入文件
include $file;
if (empty(self::$loadedClassNum[$file])) {
self::$loadedClassNum[$file] = 1;
}
}
/**
* 返回全路徑
* @param $className
* @return string
*/
private static function formatClassName($className): string
{
return $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . $className . '.php';
}
使用PHP提供的spl_autoload_register自動加載器函數(shù),注冊autoload方法實(shí)現(xiàn)自動加載,可以看到我們自動加載的類必須都在項(xiàng)目根目錄下才可以實(shí)現(xiàn)。這是一個(gè)簡單的約定。
加載配置
我們知道php使用include 導(dǎo)入文件是可以獲取到文件的返回值的(如果有的話),所以使用php文件返回一個(gè)數(shù)組來實(shí)現(xiàn)項(xiàng)目的配置文件,框架里面支持默認(rèn)的config.php文件,以及額外用戶可以自定義的配置:extra.php。這個(gè)也是我們約定好的。
配置文件示例代碼config.php:
<?php
return [
"Cache" => [
"default" => "redis",
"redis" => [
"master" => [
"pconnect" => false,
"host" => "localhost",
"port" => 6379,
"timeout" => 0,
],
],
],
"Log" => [
"default" => "file",
"file" => [
"path" => RUNTIME_PATH
],
]
];
引入配置文件的關(guān)鍵代碼:
/**
* 加載配置
* @param $filename
*/
private static function addConfig($filename): void
{
$configArr = include_once($filename);
if (is_array($configArr)) {
self::$configs = Arr::arrayMergeRecursiveUnique(self::$configs, $configArr);
}
}
/**
* 導(dǎo)入配置
* @param $paths
*/
private static function importConfig($paths): void
{
foreach ($paths as $path) {
self::addConfig($path);
}
}
加載環(huán)境變量
環(huán)境變量文件,我們默認(rèn)的就是項(xiàng)目根目錄的.env文件。.env文件配置項(xiàng)是標(biāo)準(zhǔn)的*.ini類型配置文件的書寫方式,且.env文件里面的配置項(xiàng)不區(qū)分大小寫,小寫配置項(xiàng)最終會被轉(zhuǎn)化成大寫。.env文件的加載使用php的函數(shù)parse_ini_file來實(shí)現(xiàn):
/**
* 加載環(huán)境變量定義文件
* @param string $file 環(huán)境變量定義文件
* @return void
*/
public static function load(string $file): void
{
$env = parse_ini_file($file, true) ?: [];
static::set($env);
}
框架支持環(huán)境變量的寫入、讀取和檢測。
錯(cuò)誤和異常
異常信息抓取到之后,我們將他格式化處理,主要記錄異常碼、異常文件和所在行號。然后將異常寫入日志。(注意,如果是生產(chǎn)模式,需要關(guān)閉錯(cuò)誤顯示)
public static function handler($exception)
{
// 設(shè)置http狀態(tài)碼,發(fā)送header
if (in_array($exception->getCode(), array_keys(Http::$httpStatus))) {
self::$httpCode = $exception->getCode();
} else {
self::$httpCode = 500;
}
Http::sendHeader(self::$httpCode);
// 異常信息格式化輸出
$echoExceptionString = "<b>message</b>: {$exception->getMessage()}<br/>" .
"<b>code</b>: {$exception->getCode()}<br/>" .
"<b>file</b>: {$exception->getFile()}<br/>" .
"<b>line</b>: {$exception->getLine()}<br/>";
$serverVarDump = Str::dump(false, $_SERVER);
$postVarDump = Str::dump(false, $_POST);
$filesVarDump = Str::dump(false, $_FILES);
$cookieVarDump = Str::dump(false, $_COOKIE);
$logExceptionString = "message: {$exception->getMessage()}" . PHP_EOL .
"code: {$exception->getCode()}" . PHP_EOL .
"file: {$exception->getFile()}" . PHP_EOL .
"line: {$exception->getLine()}" . PHP_EOL .
"\$_SERVER: {$serverVarDump}" . PHP_EOL .
"\$_POST: {$postVarDump}" . PHP_EOL .
"\$_COOKIE: {$cookieVarDump}" . PHP_EOL .
"\$_FILES: {$filesVarDump}";
Log::write($logExceptionString, Log::ERROR);
// debug模式將錯(cuò)誤輸出
if (static::isDebugging()) {
if (self::$isJson) {
echo Json::encode(["message" => $exception->getMessage(), "code" => 0]);
App::_end();
} else {
echo $echoExceptionString;
}
}
}
路由分發(fā)
路由的實(shí)現(xiàn)思路是:我們根據(jù)請求的地址,截取到請求的路徑信息(根據(jù)PHP全局變量$_SERVER[‘PATH_INFO’]獲取),根據(jù)路徑信息的格式,定位到某個(gè)控制器類的某個(gè)方法,然后將其觸發(fā)。實(shí)現(xiàn)代碼:
public function distribute()
{
// 解析path_info
if (isset($_SERVER['PATH_INFO'])) {
$url = explode('/', trim($_SERVER['PATH_INFO'], "/"));
if (count($url) < 3) {
$url = array_pad($url, 3, "index");
}
} else {
$url = array_pad([], 3, "index");
}
// 獲取類名和方法名
$className = self::formatClassName($url);
$actionName = self::formatActionName($url);
if (!class_exists($className)) {
throw new Exception("the controller is not exist: {$className}", 404);
}
$class = new $className();
if (!is_callable([$class, $actionName])) {
throw new Exception("the action is not exist: {$className} -> {$actionName}", 404);
}
if (!$class instanceof Controller) {
throw new Exception("the controller not belongs to fast\\Controller: {$className}", 403);
}
// 將請求分發(fā)
$class->$actionName();
}
實(shí)現(xiàn)緩存
框架中的緩存、日志、ORM都是使用適配器模式。即定義一個(gè)抽象類,抽象類中定義若干抽象方法。這樣的話,繼承了抽象類的方法必須要實(shí)現(xiàn)這些抽象方法。我們就可以通過統(tǒng)一的入口去根據(jù)配置去調(diào)用對應(yīng)的適配器類了。
其中緩存適配了Redis、Memcache以及Memcached三種。開發(fā)者可以在config.php配置文件中自行配置。
緩存主要實(shí)現(xiàn)了將數(shù)據(jù)寫入緩存和獲取緩存數(shù)據(jù)兩個(gè)方法,我們以redis為例,redis緩存主要是使用redis字符串存儲結(jié)構(gòu),使用set和get方法來實(shí)現(xiàn)。
public function get($key, &$time = null, &$expire = null)
{
$_key = $this->makeKey($key);
$res = $this->slaveObj->get($_key);
if (is_null($res) || false === $res) {
return null;
}
$res = unserialize($res);
if ($res && isset($res['value'])) {
$time = $res['time'];
$expire = $res['expire'];
return $res['value'];
}
return null;
}
public function set($key, $value = null, $expire = 3600): bool
{
return $this->masterObj->set($this->makeKey($key), serialize($this->makeValue($value, $expire)), $expire);
}
前面的代碼只是適配器的實(shí)現(xiàn),那么我們怎么調(diào)用適配器類中的方法呢。我這邊想到的是,在框架核心代碼根目錄創(chuàng)建一個(gè)緩存文件類,實(shí)現(xiàn)一個(gè)單例,通過配置來讀取我們要使用什么類型的緩存(即使用哪個(gè)適配器類),配置中配置項(xiàng)是緩存適配器類的類名稱,讀取到了我們就加載他。具體實(shí)現(xiàn)代碼:
public static function instance($type = "default"): CacheDriver
{
if ($type === "default") {
$_type = Config::get("Cache.default");
} else {
$_type = $type;
}
if (!$_type) {
throw new Exception("The type can not be set to empty!");
}
if (!isset(self::$_instance[$_type])) {
$conf = Config::get("Cache.{$_type}");
if (empty($conf)) {
throw new Exception("The '{$_type}' type cache config does not exists!");
}
$class = self::getNamespace() . "\\" . ucfirst($_type);
$obj = new $class();
if (!$obj instanceof CacheDriver) {
throw new Exception("The '{$class}' not instanceof CacheDriver!");
}
$obj->init($conf);
self::$_instance[$_type] = $obj;
} else {
$obj = self::$_instance[$_type];
}
return $obj;
}
注:日志以及ORM的實(shí)現(xiàn)方法和緩存的實(shí)現(xiàn)類似,也是通過實(shí)現(xiàn)一個(gè)適配器,然后通過加載配置中定義的適配器類來加載。
實(shí)現(xiàn)完了之后我們測試一下:
設(shè)置:
$cacheObj = Cache::instance('redis');
$setRes = $cacheObj->setModuleName("user")->set(["id" => 1], ["name" => "ZhangSan"], 1000);
if ($setRes) {
echo "設(shè)置成功";
} else {
echo "設(shè)置失敗";
}
獲?。?/p>
$cacheObj = Cache::instance('redis');
$res = $cacheObj->setModuleName("user")->get(["id" => 1], $time, $expire);
var_dump($res, $time, $expire);
實(shí)現(xiàn)日志
日志的實(shí)現(xiàn)比較簡單,主要值實(shí)現(xiàn)了日志的寫入功能,通過php函數(shù)file_put_contents實(shí)現(xiàn)寫入文件。當(dāng)然也可以使用別的方法來實(shí)現(xiàn)。
相關(guān)代碼:
public function write(string $message, string $type)
{
if (empty($message)) {
trigger_error('$message dose not empty! ');
return false;
}
if (empty($type)) {
trigger_error('$type dose not empty! ');
return false;
}
$path = APP_PATH . DIRECTORY_SEPARATOR . 'runtime' . DIRECTORY_SEPARATOR . 'logs' . DIRECTORY_SEPARATOR . $type . '/' . date('Ym/d') . '.log';
$mark = "\n\n===========================================================================\n";
$mark .= 'time:' . date('Y/m/d H:i:s') . "\n";
return \fast\util\File::write($mark . $message, $path, (FILE_APPEND | LOCK_EX));
}
public static function write($content, $path, $flags = 0)
{
$path = trim($path);
if (empty($path)) {
trigger_error('$path must to be set!');
return false;
}
$dir = dirname($path);
if (!self::exists($dir)) {
if (false == self::mkdir($dir)) {
trigger_error('filesystem is not writable: ' . $dir);
return false;
}
}
$path = str_replace("http://", "/", $path);
return file_put_contents($path, $content, ((empty($flags)) ? (LOCK_EX) : $flags));
}
應(yīng)用層調(diào)用:
Log::write("這是一條info類型的log", Log::INFO);
實(shí)現(xiàn)操作數(shù)據(jù)庫
數(shù)據(jù)庫目前只實(shí)現(xiàn)了Mysql,如果需要支持別的數(shù)據(jù)庫,只需要新增適配器即可。區(qū)別于緩存的實(shí)現(xiàn),數(shù)據(jù)庫使用接口interface作為適配器的約定。
mysql的實(shí)現(xiàn)主要依賴mysqli庫,它對mysql庫做了優(yōu)化,防注入更完善一些。CURD的具體實(shí)現(xiàn)思路是,先獲取要處理的數(shù)據(jù),最終拼接成sql來執(zhí)行。
注:鏈?zhǔn)秸{(diào)用通過方法返回$this來實(shí)現(xiàn)
簡單看一下select查詢的實(shí)現(xiàn):
public function select()
{
$this->checkMysqlOperate("table_empty");
empty($this->_fields) && $this->_fields = "*";
$sql = "SELECT {$this->_fields} FROM {$this->_table}";
!empty($this->_where) && $sql .= " WHERE {$this->_where}";
!empty($this->_order) && $sql .= " ORDER BY {$this->_order}";
!empty($this->_group) && $sql .= " GROUP BY {$this->_group}";
!empty($this->_limit) && $sql .= " LIMIT {$this->_offset}, {$this->_limit}";
$this->_sql = $sql;
$mysqliResult = mysqli_query($this->_connection, $this->_sql);
if (false === $mysqliResult) {
$this->_error = mysqli_error($this->_connection);
return false;
}
return mysqli_fetch_all($mysqliResult, MYSQLI_ASSOC);
}
我們在應(yīng)用層調(diào)用一下select:
$dbInstance = Db::getInstance();
$result = $dbInstance->table('student')->where('SId in (01, 02, 13)')->order("SId DESC")->select();
update:
$dbInstance = Db::getInstance();
$dbInstance->table('student');
$dbInstance->where(['Sid' => '01']);
$result = $dbInstance->update($data);
數(shù)據(jù)驗(yàn)證器
數(shù)據(jù)驗(yàn)證器主要是用來驗(yàn)證數(shù)據(jù)是否符合我們的規(guī)范,可以用來驗(yàn)證表單數(shù)據(jù),也可以用來驗(yàn)證業(yè)務(wù)數(shù)據(jù)。
主要實(shí)現(xiàn)是列舉所有的驗(yàn)證規(guī)則依次校驗(yàn),主要有這些規(guī)則校驗(yàn):必傳校驗(yàn)、類型校驗(yàn)、字符校驗(yàn)、數(shù)字校驗(yàn)、正則校驗(yàn)。
主要實(shí)現(xiàn)代碼:
public function check(array $data, array $rules): self
{
foreach ($rules as $rule => $message) {
$dataRule = explode(".", $rule);
if (count($dataRule) < 2) {
continue;
}
// 必傳校驗(yàn)
if ($dataRule[1] == "required" && !isset($data[$dataRule[0]])) {
array_push($this->errors, $message);
continue;
}
if (!isset($data[$dataRule[0]])) {
continue;
}
// 類型校驗(yàn)
if (in_array($dataRule[1], $this->typeCheckName)) {
if (false === self::typeCheck(strval($dataRule[1]), $data[$dataRule[0]])) {
array_push($this->errors, $message);
continue;
}
}
// 字符校驗(yàn)
if (in_array($dataRule[1], $this->stringCheckName) && isset($dataRule[2])) {
if (false === self::stringCheck(strval($dataRule[1]), $dataRule[2], $data[$dataRule[0]])) {
array_push($this->errors, $message);
continue;
}
}
// 數(shù)字校驗(yàn)
if (in_array($dataRule[1], $this->operatorCheckName) && isset($dataRule[2])) {
if (false === self::operatorCheck(strval($dataRule[1]), $dataRule[2], $data[$dataRule[0]])) {
array_push($this->errors, $message);
continue;
}
}
// 正則校驗(yàn)
if (in_array($dataRule[1], array_keys($this->pregCheckRules))) {
if (false === self::pregCheck(strval($dataRule[1]), $data[$dataRule[0]])) {
array_push($this->errors, $message);
continue;
}
}
}
return $this;
}
字符傳校驗(yàn)部分代碼:
public function stringCheck(string $rule, $value, $dataValue): bool
{
$flag = true;
switch ($rule) {
case "max":
strlen($dataValue) > $value && $flag = false;
break;
case "min":
strlen($dataValue) < $value && $flag = false;
break;
case "length":
strlen($dataValue) != $value && $flag = false;
break;
case "in":
$value = explode(",", $value);
!in_array($dataValue, $value) && $flag = false;
break;
case "notIn":
$value = explode(",", $value);
in_array($dataValue, $value) && $flag = false;
break;
}
return $flag;
}
業(yè)務(wù)層這樣調(diào)用:
public function testValidate()
{
$validate = new ValidateData();
$data = [
"age" => 17,
"weight" => "50公斤",
"name" => "ZhangSan",
"country" => "這里是中國abc",
"sex" => "未知",
"mobile" => "11098186452",
];
$rules = [
"age.required" => "請輸入年齡",
"email.required" => "請輸入郵箱",
"age.gt.18" => "年齡必須大于18",
"weight.float" => "體重必須為浮點(diǎn)數(shù)",
"name.max.6" => "姓名最大長度為6",
"country.alphaNum" => "國家必須為數(shù)字或者字母",
"sex.in.男,女" => "性別必須是男或者女",
"mobile.mobile" => "手機(jī)號碼不合法",
];
$validate->check($data, $rules);
var_dump($validate->getErrors());
}
實(shí)現(xiàn)容器依賴注入
首先我們先了解概念??蚣苤械娜萜髦傅氖鞘裁??什么是依賴注入?
容器(當(dāng)前所指)是一個(gè)用于管理和存儲應(yīng)用程序中各種對象的工具。它允許你注冊、創(chuàng)建和解析對象,以及管理它們之間的依賴關(guān)系。當(dāng)前框架中的容器通常使用關(guān)聯(lián)數(shù)組來存儲對象和服務(wù)。
依賴注入是一種設(shè)計(jì)模式,它允許你將一個(gè)對象的依賴關(guān)系傳遞給它,而不是在對象內(nèi)部直接創(chuàng)建或管理依賴關(guān)系。
這可以使代碼更加可測試、可維護(hù)和可擴(kuò)展,因?yàn)樗鼘ο蟮囊蕾囆越怦?,并使它們更容易替換和修改。
依賴注入通常通過構(gòu)造函數(shù)注入、方法注入或?qū)傩宰⑷雭韺?shí)現(xiàn)。
在當(dāng)前框架中,依賴注入和容器一起使用,容器負(fù)責(zé)實(shí)例化和解析對象,并自動注入它們的依賴關(guān)系。
那么如何實(shí)現(xiàn)呢?通過php的反射,來獲取類的相關(guān)信息來解決依賴。
我們從容器中拿一個(gè)服務(wù)對象,如果沒有拿到,則需要創(chuàng)建。創(chuàng)建的時(shí)候通過下面幾步我們來解決依賴。
- 根據(jù)類名獲取目標(biāo)類(實(shí)際是反射類)
$reflection = new \ReflectionClass($className)
- 進(jìn)一步獲取目標(biāo)類的構(gòu)造方法(實(shí)際是構(gòu)造方法類)
$reflection->getConstructor()
- 獲取構(gòu)造方法所需參數(shù)類(是一個(gè)數(shù)組)
$constructorParameters = $constructor->getParameters()
- 循環(huán)所需參數(shù),如果參數(shù)沒有默認(rèn)值,則是一個(gè)服務(wù)對象,我們繼續(xù)從容器中獲取,直到解決所有的依賴。
foreach ($constructorParameters as $param) {
if (version_compare(PHP_VERSION, '5.6.0', '>=') && $param->isVariadic()) {
break;
} elseif ($param->isDefaultValueAvailable()) {
$dependencies[] = $param->getDefaultValue();
} else {
$c = $param->getClass();
$dependencies[] = $this->get($c->getName(), $this->_params[$c->getName()] ?? []);
}
}
注:請避免出現(xiàn)循環(huán)嵌套,否則會出現(xiàn)未知問題
創(chuàng)建的完整代碼:
public function build(string $className, array $params = []): ?object
{
if (isset($this->_reflections[$className])) {
$reflection = $this->_reflections[$className];
} else {
try {
$reflection = new \ReflectionClass($className);
} catch (ReflectionException $exception) {
throw new Exception("Failed to reflect class " . $className . ", error: " . $exception->getMessage());
}
$this->_reflections[$className] = $reflection;
}
if (!$reflection->isInstantiable()) {
throw new Exception("Is not instantiable:" . $reflection->name);
}
$dependencies = [];
$constructor = $reflection->getConstructor();
if ($constructor !== null) {
$constructorParameters = $constructor->getParameters();
foreach ($constructorParameters as $param) {
if (version_compare(PHP_VERSION, '5.6.0', '>=') && $param->isVariadic()) {
break;
} elseif ($param->isDefaultValueAvailable()) {
$dependencies[] = $param->getDefaultValue();
} else {
$c = $param->getClass();
$dependencies[] = $this->get($c->getName(), $this->_params[$c->getName()] ?? []);
}
}
}
$this->_dependencies[$className] = Arr::arrayMergeBase($dependencies, $params);
$object = $reflection->newInstanceArgs($this->_dependencies[$className]);
$this->_objects[$className] = $object;
return $object;
}
}
解決完依賴,我們就把改服務(wù)存入容器中。
業(yè)務(wù)層調(diào)用:
$container = new Container();
$container->set("app\service\Group", [123]);
$container->set("app\service\User");
$container->set("app\service\UserList");
$group = $container->get("app\service\Group");
$userList = $container->get("app\service\UserList");
$group->getA();
$userList->getUserList();
Group.php:
<?php
namespace app\service;
class Group
{
public static $a = 0;
function __construct($a =1)
{
static::$a = $a;
}
public function getA()
{
echo self::$a;
}
}
User.php:
<?php
namespace app\service;
class User
{
public function __construct(Group $group)
{
}
function user()
{
}
}
UserList.php:
<?php
namespace app\service;
class UserList
{
public function __construct(User $user)
{
}
public function getUserList()
{
echo "this is the user-list";
}
}
尾聲
至此,這款簡易的php框架的實(shí)現(xiàn)過程就介紹完了。更多詳細(xì)的內(nèi)容請異步:
https://github.com/yijiebaiyi/fast_framework文章來源:http://www.zghlxwxcb.cn/news/detail-705226.html
這里有詳細(xì)的代碼示例和完整的實(shí)現(xiàn)過程。文章來源地址http://www.zghlxwxcb.cn/news/detail-705226.html
到了這里,關(guān)于怎么從0到1實(shí)現(xiàn)一個(gè)PHP框架?的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!