4.5 框架是如何運行的
基本很多項目的開發都是基於PHP開源框架的,或者至少都是基於框架的,不管這個框架是內部的,還是自己個人編寫的,還是來自開源社區的。理解框架是如何運行是很有幫助的,注意這裏說的是理解,而不是瞭解。說白了,就是你不單要知道它是怎麼樣的,還要明白爲什麼會這樣。
不同框架的設計思路是不一樣的,但最後核心都會落在如何把全部需要使用到的類、對象、資源更好地組織起來,在性能上達到最優,在易用性上達到最高。理解框架是如何運行的,不僅能幫助我們理清框架的設計思路,還能讓我們編寫更能符合框架制定的標準和規範的代碼,甚至在恰當的時候提升我們專注框架或者自主設計微架構的能力。在與開發工程師交流過程中,我發現還是有很多同學對於這一塊幾乎沒什麼認識,這讓我想起木蘭詩裏的那句“同行十二年,不知木蘭是女郎。”
如果使用框架開發了多年項目,卻不知道框架內部是如何運行的話,我覺得同樣是有點可悲的。所以,我覺得有必要在這裏簡單分享一下。
4.5.1 多種調用方式
PHP是一門動態解釋性腳本語言,它真的很動態,很靈活,並且它是弱類型的。你看,對於一個字符串變量$var = "abc",你可以把它賦值爲整型,接着又可以把它設置爲一個數組,還可以把它變爲布爾值,甚至還可以改爲類對象實例。這都沒任何問題!
根據我的理解,框架所做的事情,概括起來就是:對於將要訪問的鏈接或功能服務,先按路由規則進行解析,提取待執行的類名稱和方法名稱。然後對待執行的操作進行調用,在執行前後還需要將過濾、預處理、回調、事件偵聽等環節串聯起來。最後,把執行結果以合適的方式返回給客戶端,可以是頁面輸出,也可以是接口數據返回。當然,還要有異常處理的機制。
這裏,重點講如何調用待執行的操作。即給定一個類名和一個類的方法名,如何對其進行動態調用。
假設,我們已經有這樣一個BookController類,通過getHotList()方法可以獲取一些熱門的書籍。爲專注於如何調用,而非實現,所以這裏簡單模擬了一些數據。同時爲簡化,此類方法的結果將通過接口請求返回數據給客戶端,而不是返回輸出一個頁面。BookController類代碼如下:
class BookController {
public function getHotList() {
return array(
array('name' => '重構:改善既有代碼的設計'),
array('name' => '逆流而上:PHP企業級系統開發'),
);
}
}
下面來看下多種調用方式的實現與差異。
通過硬編碼方式調用
首先,是硬編碼的方式。硬編碼就是把要實例化的類名,將要執行的方法名,都是固定寫死的。這種方式最爲常見,也最簡單。
$book = new BookController();
$rs = $book->getHotList();
print_r($rs);
很多已經流行的開源框架,都是多年前提出來,並且是在當時的時代背景下設計、迭代出來的。那時,網站建設還很流行,PC端的流量就像一塊處女地,到處攻城掠地。而如今,天平的砝碼開始傾斜到移動端。基於前後端分離的思想,更多的開發工作從原來的網站頁面開發轉變成對接口服務的開發。由於以前老的開源框架專注於網站頁面的開發,所以對接口微服務開發這一領域支持度不夠友好。漸而行之,我們就能慢慢發現,身邊的項目充斥着很多下面這樣的代碼,以滿足AJAX請求的接口能在服務端對應的被請求和響應。
$action = $_GET['action'];if ($action == 'getHotList') {
$book = new BookController();
$rs = $book->getHotList();} else if ($action == 'getDetail') {
$book = new BookController();
$rs = $book->getDetail();} else if ($action == 'updateDetail') {
$book = new BookController();
$rs = $book->updateDetail();} else if ($action == 'xxx') {
// ……}
$apiRs = array('code' => 200, 'msg' => '', 'data' => $rs);
echo json_encode($apiRs);
這裏用的就是硬編碼的方式來調用。可以看到,會存在很多重複性的代碼,有一定有代碼異味。最重要的是,每次新增一個接口或者頁面,都要同步修改這裏的入口控制代碼。雖然某種程序上符合開放-封閉原則,但是增加了維護成本。
通過動態變量方式調用
另外一種方式,可能會比硬編碼的方式好一點,那就是通過動態變量的方式來調用。把待執行的類名和方法名,先存在變量中,然後再根據類名動態類實例對象,再根據方法名動態執行。這對於一直習慣於靜態編程語言的同學來說,可能會覺得有點不可思議。但它這就樣真實發生了。
$className = 'BookController';
$actionName = 'getHotList';
$book = new $className();
$rs = $book->$actionName();
print_r($rs);
這種方式能節省很多重複的代碼,並且可以支持動態執行新增擴展的接口或者頁面,減少額外的維護成本。但還不是最好的做法,並且你也基本找不到主流的開源框架會採用這種方式來執行。爲什麼呢?
因爲,首先,這種做法看起來很粗魯,難登大雅之堂(我個人的看法)。其次,更重要的是,如果需要傳遞參數該怎麼辦,尤其當參數的個數、位置、簽名各有不同時?最後,缺少對基本錯誤的判斷檢測和預處理。例如,如果方法是不存在的或者不可調用的話,框架執行到這裏就會出現500錯誤,而開發人員完全不知道是怎麼回事。更別說在線上生產環境上,用戶不小心訪問了某個不存在的鏈接,結果系統給用戶一個空白的頁面,這就像Windows的應用程序時不時會彈窗提示你“程序崩潰,錯誤代碼:0XXXXXX”一樣粗暴。
那有沒有更好的方式呢?繼續看一下節。
通過call_user_func_array()調用
調用一個回調函數,可以使用call_user_func()或者call_user_func_array()來進行調用。兩者的區別在於兩者對於參數列表的傳遞方式,前者是通過參數列表方式來傳遞多個不定參數,後者是通過一個數組來傳遞。
我們先來看一下簡單的實現版本,再來逐步迭代優化。在第一版中,我們先快速使用call_user_func_array()實現動態執行。
// call_user_func_array()調用 - 第一版
$className = 'BookController';
$actionName = 'getHotList';
$book = new $className();
$params = array();
$rs = call_user_func_array(array($book, $actionName), $params);
print_r($rs);
call_user_func_array()函數的第一個參數是待調用的回調函數,即callback類型,對應的值是array($book, $actionName)。關於回調類型,在下一節會繼續詳細講解,這裏暫時不展開。$params是要被傳入回調函數的數組,即待調用函數的實際參數。這裏暫時也沒有額外的參數,但後面會對此強化。
回到上面的問題,我們怎麼提前判斷一個回調函數是否可以正常執行呢?答案是使用is_callable()函數,使用它可以增加我們系統的健壯性和容錯性。只需要這樣即可:
// call_user_func_array()調用 - 第二版
$book = new $className();
$params = array();
$callback = array($book, $actionName);// 判斷是否可被調用if (is_callable($callback)) {
$rs = call_user_func_array($callback, $params);}
print_r($rs);
但是,在有些開源框架裏,例如Somfony的控制器的操作方法是可以有參數的,例如下面的LuckyController::number($max)中,就有一個參數$max,它們又是如何做到實際參數傳遞的呢?
// src/Controller/LuckyController.phpnamespace App\Controller;use Symfony\Component\HttpFoundation\Response;use Symfony\Component\Routing\Annotation\Route;class LuckyController{
/**
* @Route("/lucky/number/{max}", name="app_lucky_number")
*/
public function number($max)
{
$number = mt_rand(0, $max);
return new Response(
'<html><body>Lucky number: '.$number.'</body></html>'
);
}
}
具體實現起來也不難,我們已經知道通過call_user_func_array()的第二個數組參數,可以動態傳遞多個不定實際參數給待執行的回調函數。剩下的難點,就是如果找到回調函數需要哪些形參,以及如何在請求的參數中找到對應的實際參數。先來看,怎麼知道控制器的操作需要哪些形式參數。
我們也來爲獲取熱門書籍列表的接口增加一個參數$max,也用來表示需要獲取的最大條目數量。以此爲例,再來探討如何具體實現。增加$max參數,並且重新調整實現的代碼如下:
class BookController {
public function getHotList($max = 2) {
$all = array(
array('name' => '重構:改善既有代碼的設計'),
array('name' => '逆流而上:PHP企業級系統開發'),
);
return array_slice($all, 0, $max);
}
}
如果想獲取形式參數列表,包括有幾個參數、參數名字是什麼、有沒默認值(有的話是什麼),這時需要用到反射Reflection裏面的ReflectionMethod和ReflectionParameter。繼續我們第三版迭代,在爲了嘗試獲取形式參數列表的名稱以及默認值而添加新的代碼如下:
// call_user_func_array()調用 - 第三版(上)// 獲取形式參數和參數實際
$reflection = new ReflectionMethod($className, $actionName);foreach ($reflection->getParameters() as $arg) {
$argName = $arg->name;
$argDefaultValue = $arg->getDefaultValue();
var_dump($argName, $argDefaultValue);
}
作爲臨時調試的代碼,可以看到結果中有輸出前面的$max參數,以及它對應的默認值2。
string(3) "max"int(2)
但第三版到這裏只完成了一半,因爲我們還要找到實際中對應的參數值。這一點就好辦了,有了具體的參數名字以及它的默認值,稍微制定一下規則就可以輕鬆找到客戶端傳遞過來的具體參數值了。例如,就以形參名字作爲客戶端的參數名,如果客戶端沒傳,就使用默認值,如果沒有默認值則賦爲NULL。即最終參數的值的優先級依次是:
1、優化使用客戶端傳遞的參數值
2、如果沒傳,則使用形參的默認值
3、如果沒有默認值,就賦爲NULL
根據這些規則,再來完善第三版,最終代碼是:
// call_user_func_array()調用 - 第三版(下)// 獲取形式參數和參數實際
$reflection = new ReflectionMethod($className, $actionName);
foreach ($reflection->getParameters() as $arg) {
$argName = $arg->name;
$argDefaultValue = $arg->isOptional() ? $arg->getDefaultValue() : NULL;
// var_dump($argName, $argDefaultValue);
// 獲取參數並構建實際參數列表
$params[$argName] = isset($_REQUEST[$argName]) ? $_REQUEST[$argName] : $argDefaultValue;
}
至此,經過多次迭代,我們對於通過call_user_func_array()調用回調函數的方案設計,就可以暫告一段落了。
作爲最後的總結和回顧,我們來觀察下幾個開源框架對於這一塊的做法,並簡單分析一下。
Yii框架 2.0
在Yii 2.0中,Action::runWithParams($params)裏,可以看到對控制器Controller的Action操作執行前的相關處理。這裏使用了method_exists()函數來判斷方法是否存在,通過bindActionParams()操作來綁定實際參數併產生參數列表$args。最後在執行前觸發beforeRun()鉤子函數,通過call_user_func_array()函數來執行回調函數[$this, 'run'],實際參數就是剛剛產生的$args,執行完畢後再觸發afterRun()鉤子函數。
namespace yii\base;// ……class Action extends Component{
/**
* Runs this action with the specified parameters.
* This method is mainly invoked by the controller.
*/
public function runWithParams($params)
{
if (!method_exists($this, 'run')) {
throw new InvalidConfigException(get_class($this) . ' must define a "run()" method.');
}
$args = $this->controller->bindActionParams($this, $params);
Yii::debug('Running action: ' . get_class($this) . '::run()', __METHOD__);
if (Yii::$app->requestedParams === null) {
Yii::$app->requestedParams = $args;
}
if ($this->beforeRun()) {
$result = call_user_func_array([$this, 'run'], $args);
$this->afterRun();
return $result;
}
return null;
}
// ……
Symfony框架 4.0
在Symfony 4.0中,提煉後的HttpKernel::handleRaw()代碼如下。通過getController()獲取待調用的控制器,通過getArguments()獲取實際參數列表,最後通過call_user_func_array()來進行調用。最後將結果$response通過合適的方式返回給客戶端。
namespace Symfony\Component\HttpKernel;// ……class HttpKernel implements HttpKernelInterface, TerminableInterface{
/**
* Handles a request to convert it to a response.
*/
private function handleRaw(Request $request, int $type = self::MASTER_REQUEST)
{
$this->requestStack->push($request);
// ……
$controller = $event->getController();
$arguments = $event->getArguments();
// call controller
$response = \call_user_func_array($controller, $arguments);
// ……
return $this->filterResponse($response, $request, $type);
}
// ……
ThinkPHP框架 5.1
在 ThinkPHP 5.1,Container:: invokeFunction($function, $vars = [])內使用了ReflectionFunction反射來獲取回調函數的參數信息,然後通過bindParams()與實際參數進行綁定併產生$args,最後通過call_user_func_array()進行調用執行。如果方法不存在,則會通過ReflectionException異常拋出。
namespace think;// ……class Container implements \ArrayAccess
{
/**
* 執行函數或者閉包方法 支持參數調用
*/
public function invokeFunction($function, $vars = [])
{
try {
$reflect = new ReflectionFunction($function);
$args = $this->bindParams($reflect, $vars);
return call_user_func_array($function, $args);
} catch (ReflectionException $e) {
throw new Exception('function not exists: ' . $function . '()');
}
}
// ……
可以發現,不同開源框架在處理動態執行這一塊是大同小異的,都是使用反射來獲取形式參數,然後綁定到實際參數。準備好待調用的控制器或回調函數後,通過call_user_func_array()函數進行回調,並傳遞實際的參數列表。在這執行前後、處理過程中,再結合鉤子函數或者偵聽事件豐富更多擴展的操作。
4.5.2 Callback / Callable 回調類型
在前面剛剛結束的這一節中,有討論到回調類型。在使用call_user_func_array()進行回調時,它的第一個參數是回調類型,類型關鍵字是Callback / Callable。回調類型可以用於動態執行,還可以作爲註冊的事件先存儲起來,在適當的時機再觸發執行。
對匿名函數的回調
回調類型是一個很趣的類型,下面我們一起來逐一學習下。
首先,是匿名函數,類似這樣:
$func = function() {
return '我在匿名函數內';
};// 這樣調用
var_dump($func());// 或這樣調用
var_dump(call_user_func($func));
匿名函數與數組系統的函數結合使用較多,例如:array_walk(),和前面提到的usort()、array_map()、array_filter()。在提供了DI容器的開源框架中,也會使用匿名函數來延遲加載,從而提升性能。例如在PhalApi 2.x中的di.php文件內,對於緩存的註冊就使用了匿名函數。因爲並不是全部的接口請求都需要使用到緩存,所以可以延遲加載,直到有需要時纔去初始化。
// 緩存 - Memcache/Memcached
$di->cache = function () {
return new \PhalApi\Cache\MemcacheCache(\PhalApi\DI()->config->get('sys.mc'));};
這時,匿名函數可直接作爲回調類型。
對普通函數的回調
接下來,就是帶名稱的函數。PHP官方本身就有很多這樣的函數,例如:strtoupper()、md5()、intval()等。你也可以自己編寫一個函數。例如將全部數組的元素轉成大寫:
$arr = array('dogstar', 'aevit', 'yoyo');
$arrUpper = array_map('strtoupper', $arr);// print_r($arrUpper);
在這裏,只需要用函數的名稱,就可以表示成回調類型了。
對類實例方法的回調
前面說的都是面向過程編程中的函數,下面來講講面向對象編程中的類。對於類的成員函數方法,如果需要進行回調的話,表示方式是:array(類實例, 方法名)。關於這種用法,前面在講框架是如何運行的一節中已有很多案例,這裏不再贅述。
例如:
$book = new BookController();
$actionName = 'getHotList';
$params = array();
$rs = call_user_func_array(array($book, $actionName), $params);
對類靜態方法的回調
最後,還有一種是對類靜態方法的回調。因爲類的靜態方法不需要實例化就能調用,因此它的回調類型用字符串來表示,格式是:類名::方法名。例如,我們有一個Foo類,裏面有一個靜態方法doSth(),則可以這樣進行回調:
class Foo {
public static function doSth() {
return '我在類的靜態方法內';
}}
var_dump(call_user_func('Foo::doSth'));
此外,也可以使用數組的形式來表示,第一個位置表示類名,第二個位置表示方法名。例如:
var_dump(call_user_func(array('Tool', 'doSth')));
也可以達到同樣的效果。
在討論完以上四類回調類型的表示方式後,再來看下在Symfony框架中,事件分發的相關代碼片段,就能更好地理解了。
namespace Symfony\Component\EventDispatcher;// ……class EventDispatcher implements EventDispatcherInterface{
/**
* Triggers the listeners of an event.
*/
protected function doDispatch($listeners, $eventName, Event $event)
{
foreach ($listeners as $listener) {
if ($event->isPropagationStopped()) {
break;
}
\call_user_func($listener, $event, $eventName, $this);
}
}
// ……
上面是Symfony底層處理事件分發的核心代碼,很簡潔。其實就是循環每一個偵聽事件註冊的回調函數進行調用,然後把相應的上下文信息傳遞過去。
緊接着,再來聯繫一下客戶端的使用,看看客戶端是如何註冊偵聽事件以及實現事件回調處理的話,就更加清晰明朗了。下面是從Symfony官方摘錄的代碼版本,講的是如何創建一個事件訂閱者。
// src/EventSubscriber/ExceptionSubscriber.phpnamespace App\EventSubscriber;class ExceptionSubscriber implements EventSubscriberInterface{
public static function getSubscribedEvents()
{
// return the subscribed events, their methods and priorities
return array(
KernelEvents::EXCEPTION => array(
array('processException', 10),
array('logException', 0),
array('notifyException', -10),
)
);
}
public function processException(GetResponseForExceptionEvent $event) { /** 略 **/ }
public function logException(GetResponseForExceptionEvent $event) { /** 略 **/ }
public function notifyException(GetResponseForExceptionEvent $event) { /** 略 **/ }
}
這些都有回調類型的身影,雖然它並不是那麼明顯,但通過getSubscribedEvents()返回的配置,再結合當前具體的實現類,就不難推導出底層是如何組裝回調類型的了。
4.5.3 自動加載
文件的自動加載在任何一文件語言中,都有其處理的特色。在PHP中,則有一套靈活的機制來動態加載所需要的PHP文件。下面,從原始的手動加載,到簡單實現自動加載,再到社區推薦和統一的PSR-4命名規範,分別依次講解。
原始的手動加載
直的很難理解,爲什麼到了科技如此發達的21世紀,居然還會有PHP項目使用手動加載的方式來引入文件。
在手動引入的項目中,可以說歷史原因是多種多樣,但令人費解的是他們可以一直這樣保持着並忍受手動引入的痛苦。要麼就是因爲缺少引入的文件出現“Class not found”的錯誤,要麼就是因爲重複加載而提示“Cannot redeclare class”。
例如,在入口文件index.php中,需要用到存放類Helper類的文件Helper.php,以及存放函數foo()的文件foo.php。如果你的項目中使用的也是手動加載的方式,那麼以下代碼很可能就是你項目的縮影。
==> index.php <==
if (!class_exists('Helper')) {
require_once dirname(__FILE__) . '/Helper.php';}
$helper = new Helper();if (!function_exists('foo')) {
require_once dirname(__FILE__) . '/foo.php';}==> foo.php <==
if (!function_exists('foo')) {
function foo() {
}}==> Helper.php <==
if (!class_exists('Helper')) {
class Helper {
}}
在客戶端調用時,需要先判斷要實例化的類是否已經存在,沒有話就手動引入。同樣,爲了防止“Class not found”錯誤,客戶端在調用函數之前,要判斷函數是否存在,沒有的話就手動引入。每次都這樣,顯得很重複累贅。不僅如此,爲了避免出現“Cannot redeclare class”錯誤,在聲明類和聲明函數時,也要添加多一層判斷。
沿用手動加載的方式,原因可能有兩個,一點是出於性能的考慮,但我覺得並不成立。第二點是項目沒有使用框架,或者分層設計得不明顯,代碼放置得錯落無序,沒有統一的規則能根據類名找到代碼文件的位置。
我覺得手動加載的方式,簡直是在浪費程序員的生命,因爲每次都要忍受最原始方式的折磨。就好比如,現在要點個火,花一塊錢買個打火機,然後一按就有火了,既方便攜帶又可以長時間保存火種,經濟又實惠。但如果換成,每次生個火都要你拿兩個火石在碰撞,或者使用放大鏡通過凸點聚焦方式來燃燒,不是很麻煩,很浪費時間,很不值得嗎?
那,有沒更好的解決方案?有,當然有!正如你看到的,沒有哪個開源框架是還需要你手動引入文件的。接下來,我們來重複造個輪子,以便深刻理解PHP是如何實現自動加載的。
簡單實現自動加載
當調用一個不存在的對象方法時,會觸發魔法方法。那當使用一個不存在的類時,會觸發什麼方法,或者會發生什麼事情呢?
PHP提供了兩種方式,可用來註冊自己的自動加載機制,分別是:
__autoload()
嘗試加載未定義的類spl_autoload_register()註冊自定義加載類的方式,註冊給定的函數作爲
__autoload
的實現
__autoload()
只能定義一次,它的參數只有一個,就是未定義的類名稱。
推薦的方式是使用spl_autoload_register() ,因爲它更靈活。它需要的第一個參數是回調類型,即欲註冊的自動裝載函數。
註冊很簡單,基本的代碼骨架是:
spl_autoload_register(array(new MyAutoLoader(), 'load'));class MyAutoLoader {
public function load($classname) {
// ……
}}
剩下的事情,就是如何根據類名找到對應PHP文件的藝術了。
說它是藝術,是因爲項目代碼的目錄結構,以及命名規則,以及放置的位置,都是可以由我們自己來制定的。只要類名以及文件路徑之間,存在唯一映射關係,再來實現自動加載就不難了。例如常用的PEAR命名規範,就是其中一種。又或者使用後綴來區分不同的目錄位置,比如DemoController表示在控制器Controller目錄內,DemoModel表示在模型Model目錄內,DemoHelper表示在輔助類Helper目錄內。這些規則都是可參考既有的方式,也可以自己根據情況來設計。這裏不過多展開。
但這裏要重點說明,在實現自定義自動加載時,要特別注意以下幾個事項。
避免重複加載
在最終引入PHP文件時,可以使用require_once的方式來引入,避免重複加載。嚴格區分大小寫
需要嚴格區分大小寫,包括類名和文件路徑。因爲經常發生的事情是,明明在本地的Windows系統開發和調試是正常的,但一發布到線上環境就會出500錯誤。是因爲線上的Linux操作系統是嚴格區分大小寫的,從而會導致PHP文件找不到。與操作系統環境有不可移植性的除了大小寫敏感外,還有就是文件路徑的分割符號。Windows系統是反斜槓,而Linux系統是斜槓。這一點也要留意區分。注意命名空間
還要注意如果類名是帶有命名空間的話,要怎麼處理。關鍵點在於命名空間之間的連接符。與其他加載機制的共存
最後,如果自定義的加載機制無法找到對應的類文件,也不要輕易終止或拋出異常。應該把機會留給其他自動加載機制,除非確認這是一個閉包生態圈。
其他還有一些零散的知識點,例如可以用file_exists()來判斷文件是否存在,引入後還可以使用class_exists()來判斷類是否真的存在。
綜合這些知識點,基本的加載骨架和注意事項,我們就可以實現自己的自動加載機制了。但有沒有更省心的做法,就是我連自動加載都不用實現,就能實現類文件的自動加載?有!下面會繼續介紹。
遵循PSR-4命名規範
在PHP開源社區裏,Composer的方式逐漸成爲了主流。很多開源框架都紛紛升級轉爲這種組件化的方式。Compose主要使用的是PSR-4規範。簡單來說,類的全稱格式如下:
\<NamespaceName>(\<SubNamespaceNames>)*\<ClassName>
其中,<NamespaceName>
爲頂級命名空間;<SubNamespaceNames>
爲子命名空間,可以有多層;<ClassName>
爲類名。
Composer已經幫我們實現了統一的自動加載機制,剩下要做的事就是按照它的規範命名即可。但對於初次使用composer和初次接觸PSR-4的同學,以下事項需要特別注意,否則容易導致誤解、誤用、誤導。
1、在當前命名空間使用其他命名空間的類時,應先use再使用,或者使用完整的、最前面帶反斜槓的類名。
2、在定義類時,當前命名空間應置於第一行,且當存在多級命名空間時,應填寫完整。
3、命名空間和類,應該與文件路徑保持一致,並嚴格區分大小寫。
例如,以PhalApi框架內編寫接口類Site爲例:
namespace App\Api;use PhalApi\Api;class Site extends Api {
public function test() {
// 錯誤!會提示 App\Api\DI()函數不存在!
DI()->logger->debug('測試函數調用');
// 正確!調用PhalApi官方函數要用絕對命名空間路徑
\PhalApi\DI()->logger->debug('測試函數調用');
}
public function testMyFun() {
// 錯誤!會提示 App\Api\my_fun()函數不存在!
//(假設在./src/app/functions.php有此函數)
my_fun();
// 正確!調用前要加上用絕對命名空間路徑
\App\my_fun();
}}
更多關於PSR-4的規範說明,可以參考:
PSR-4: Autoloader,https://www.php-fig.org/psr/psr-4/
本文分享自微信公衆號 - 小白開放平臺(yesapi)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。