不同框架的设计思路是不一样的,但末了核心都会落在如何把全部须要利用到的类、工具、资源更好地组织起来,在性能上达到最优,在易用性上达到最高。理解框架是如何运行的,不仅能帮助我们理清框架的设计思路,还能让我们编写更能符合框架制订的标准和规范的代码,乃至在恰当的时候提升我们专注框架或者自主设计微架构的能力。在与开拓工程师互换过程中,我创造还是有很多同学对付这一块险些没什么认识,这让我想起木兰诗里的那句“同行十二年,不知木兰是女郎。”
如果利用框架开拓了多年项目,却不知道框架内部是如何运行的话,我以为同样是有点可悲的。以是,我以为有必要在这里大略分享一下。
4.5.1 多种调用办法
PHP是一门动态阐明性脚本措辞,它真的很动态,很灵巧,并且它是弱类型的。你看,对付一个字符串变量$var = "abc",你可以把它赋值为整型,接着又可以把它设置为一个数组,还可以把它变为布尔值,乃至还可以改为类工具实例。这都没任何问题!

根据我的理解,框架所做的事情,概括起来便是:对付将要访问的链接或功能做事,先按路由规则进行解析,提取待实行的类名称和方法名称。然后对待实行的操作进行调用,在实行前后还须要将过滤、预处理、回调、事宜侦听等环节串联起来。末了,把实行结果以得当的办法返回给客户端,可以是页面输出,也可以是接口数据返回。当然,还要有非常处理的机制。
这里,重点讲如何调用待实行的操作。即给定一个类名和一个类的方法名,如何对其进行动态调用。
假设,我们已经有这样一个BookController类,通过getHotList()方法可以获取一些热门的书本。为专注于如何调用,而非实现,以是这里大略仿照了一些数据。同时为简化,此类方法的结果将通过接口要求返回数据给客户端,而不是返回输出一个页面。BookController类代码如下:
<?phpclass 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()钩子函数。
<?phpnamespace 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中,提炼后的HttpKernel::handleRaw()代码如下。通过getController()获取待调用的掌握器,通过getArguments()获取实际参数列表,末了通过call_user_func_array()来进行调用。末了将结果$response通过得当的办法返回给客户端。
<?phpnamespace 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,Container:: invokeFunction($function, $vars = [])内利用了ReflectionFunction反射来获取回调函数的参数信息,然后通过bindParams()与实际参数进行绑定并产生$args,末了通过call_user_func_array()进行调用实行。如果方法不存在,则会通过ReflectionException非常抛出。
<?php 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。回调类型可以用于动态实行,还可以作为注册的事宜先存储起来,在适当的机遇再触发实行。
对匿名函数的回调
回调类型是一个很趣的类型,下面我们一起来逐一学习下。
首先,是匿名函数,类似这样:
<?php$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 () {returnnew\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框架中,事宜分发的干系代码片段,就能更好地理解了。
<?phpnamespace 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) { / 略 / }publicfunctionnotifyException(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 <==<?phpif (!class_exists('Helper')) { require_once dirname(__FILE__) . '/Helper.php';}$helper = new Helper();if (!function_exists('foo')) { require_once dirname(__FILE__) . '/foo.php';}==> foo.php <==<?phpif (!function_exists('foo')) { function foo() { }}==> Helper.php <==<?phpif (!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规范。大略来说,类的全称格式如下:
<?phpnamespace 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(); }}
个中,<NamespaceName>为顶级命名空间;<SubNamespaceNames>为子命名空间,可以有多层;<ClassName>为类名。
Composer已经帮我们实现了统一的自动加载机制,剩下要做的事便是按照它的规范命名即可。但对付初次利用composer和初次打仗PSR-4的同学,以下事变须要特殊把稳,否则随意马虎导致误解、误用、误导。
1、在当前命名空间利用其他命名空间的类时,应先use再利用,或者利用完全的、最前面带反斜杠的类名。2、在定义类时,当前命名空间应置于第一行,且当存在多级命名空间时,应填写完全。3、命名空间和类,该当与文件路径保持同等,并严格区分大小写。例如,以PhalApi框架内编写接口类Site为例:
更多关于PSR-4的规范解释,可以参考:
PSR-4: Autoloader,https://www.php-fig.org/psr/psr-4/