当前位置: 首页 > 后端技术 > PHP

LaravelframeworkfacadeFacade源码解析

时间:2023-03-29 15:27:11 PHP

前言开始之前请关注我自己的博客:www.leoyang90.cn本篇文章开始说说laravel框架中的facade。什么是门面?官方文档:Facades(发音:/f??s?d/)为应用程序服务容器中可用的类提供一个“静态”接口。Laravel带有许多门面,可以用来访问Laravel中几乎所有的服务。Laravel外观实际上是服务容器中底层类的“静态代理”。与传统的静态方法相比,门面不仅提供了简洁丰富的语法,还带来了更好的可测试性和可扩展性。这意味着什么?首先我们要知道laravel框架的核心是一个Ioc容器作为服务容器。它的功能类似于工厂模型,是工厂的高级版本。laravel的其他功能如路由、缓存、日志、数据库,其实都类似于插件或者部件,叫做服务。Ioc容器的主要作用是生产各种零件,提供各种服务。在laravel中,如果我们想使用某个服务,应该怎么办呢?最简单的方法就是调用服务容器的make函数,或者使用依赖注入,或者我们今天要说的门面。与其他方法相比,门面最大的特点就是简单。比如我们经常使用的Router,如果使用服务容器的make:App::make('router')->get('/',function(){returnview('welcome');});如果你使用外观:Route::get('/',function(){returnview('welcome');});可以看到代码更简洁了。其实下面我们会介绍门面最后调用的函数也是服务容器的make函数。Facade的原理下面以Route为例,讲解一下Facade的原理和实现。我们先来看Route的门面类:classRouteextendsFacade{protectedstaticfunctiongetFacadeAccessor(){return'router';}}很简单,对吧?其实每个门面类只需要重新定义getFacadeAccessor函数即可,该函数返回服务的唯一名称:router。需要注意的是确保可以使用服务容器的make函数(App::make('router'))成功创建此名称,原因我们稍后会看到。那么当我们写一个像Route::get()这样的语句时到底发生了什么?秘诀在于基类Facade。公共静态函数__callStatic($method,$args){$instance=static::getFacadeRoot();if(!$instance){thrownewRuntimeException('Afacaderoothasnotbeenset.');}return$instance->$method(...$args);}运行Route::get()时,发现门面Route没有staticget()函数,PHP会调用这个魔术函数__callStatic。我们看到这个魔法函数做了两件事:获取对象实例,并用对象调用get()函数。先看如何获取对象实例:publicstaticfunctiongetFacadeRoot(){returnstatic::resolveFacadeInstance(static::getFacadeAccessor());}protectedstaticfunctiongetFacadeAccessor(){thrownewRuntimeException('Facade没有实现getFacadeAccessor方法.');}protectedstaticfunctionresolveFacadeInstance($name){if(is_object($name)){return$name;}if(isset(static::$resolvedInstance[$name])){returnstatic::$resolvedInstance[$name];}returnstatic::$resolvedInstance[$name]=static::$app[$name];}我们看到基类getFacadeRoot()调用了getFacadeAccessor(),这是我们的服务重载函数,如果基类的getFacadeAccessor类被调用,会抛出异常。在我们的示例中,getFacadeAccessor()返回“router”,然后getFacadeRoot()调用resolveFacadeInstance()。这个函数的关键点是returnstatic::$resolvedInstance[$name]=static::$app[$name];我们可以看到“路由器”是使用app创建的,也就是服务容器。创建成功后,将importresolvedInstance作为缓存,方便后面快速加载。好了,Facade的原理讲到这里就说完了,但是这里我们有一个疑惑,为什么在代码中写Route的时候可以调用IlluminateSupportFacadesRoute呢?这就是别名的目的。很多门面都有自己的别名,所以我们不用在代码中写useIlluminateSupportFacadesRoute,直接使用Route即可。AliasesAliases为什么不用useIlluminateSupportFacadesRoute就可以在larval中全局使用Route?其实,其中的奥秘就在于一个PHP函数:class_alias,它可以为任何类创建一个别名。larval启动时,会为每个门面类调用class_alias函数,所以不用直接使用类名,直接使用别名即可。facade和类名之间的映射存储在config文件夹中的app文件中:'aliases'=>['App'=>Illuminate\Support\Facades\App::class,'Artisan'=>Illuminate\Support\Facades\Artisan::class,'Auth'=>Illuminate\Support\Facades\Auth::class,...]让我们看看laravel如何为外观类创建别名。启动aliasAliases服务说到启动larval,我们离不开index.php:require__DIR__.'/../bootstrap/autoload.php';$app=require_once__DIR__.'/../bootstrap/app.php';$kernel=$app->make(Illuminate\Contracts\Http\Kernel::class);$response=$kernel->handle($request=Illuminate\Http\Request::capture());...section前一句是我们上一篇博客中提到的composer的自动加载,下一句是获取laravel的核心Ioc容器,第三句“manufactures”Http请求的核心,而第四句是这里的重点,涉及到很多Big,laravel中所有功能服务的注册和加载,甚至是Http请求的构建和传递,都归功于这一句。$request=Illuminate\Http\Request::capture()是laravel通过全局_SERVER数组构造Http请求,然后调用Http内核函数句柄的语句:publicfunctionhandle($request){try{$request->enableHttpMethodParameterOverride();$response=$this->sendRequestThroughRouter($request);}catch(Exception$e){$this->reportException($e);$response=$this->renderException($request,$e);}catch(Throwable$e){$this->reportException($e=newFatalThrowableError($e));$response=$this->renderException($request,$e);}事件(新事件\RequestHandled($request,$response));return$response;}在handle函数方法中,允许enableHttpMethodParameterOverride函数在表单中使用delete、put等类型的请求。让我们看看sendRequestThroughRouter:protectedfunctionsendRequestThroughRouter($request){$this->app->instance('request',$request);Facade::clearResolvedInstance('request');$this->bootstrap();return(newPipeline($this->app))->send($request)->through($this->app->shouldSkipMiddleware()?[]:$this->middleware)->then($this->dispatchToRouter());}前两句是在larval的Ioc容器中设置request请求的对象实例,在Facade中清除request的缓存实例。bootstrap:publicfunctionbootstrapper(){if(!$this->app->hasBeenBootstrapped()){$this->app->bootstrapWith($this->bootstrappers());}}protected$bootstrappers=[\Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables::class,\Illuminate\Foundation\Bootstrap\LoadConfiguration::class,\Illuminate\Foundation\Bootstrap\HandleExceptions::class,\Illuminate\Foundation\Bootstrap\RegisterFacades::class,\Illuminate\Foundation\Bootstrap\RegisterProviders::class,\Illuminate\Foundation\Bootstrap\BootProviders::class,];$bootstrappers是Http内核中专门用于启动的组件,bootstrap函数调用Ioc容器的bootstrapWith函数创建这些组件,并使用Components启动服务。app->bootstrapperWith:publicfunctionbootstrapWith(数组$bootstrappers){$this->hasBeenBootstrapped=true;foreach($bootstrappersas$bootstrapper){$this['events']->fire('bootstrapping:'.$bootstrapper,[$this]);}$this->make($bootstrapper)->bootstrap($this);$this['events']->fire('bootstrapped:'.$bootstrapper,[$this]);可以看到bootstrapWith函数使用Ioc容器创建了各个启动服务的实例,然后回调启动自己的函数bootstrap。这里只看我们的Facade启动组件\Illuminate\Foundation\Bootstrap\RegisterFacades::classRegisterFacades的bootstrap函数:Facade::setFacadeApplication($app);AliasLoader::getInstance($app->make('config')->get('app.aliases',[]))->register();可以看出bootstrap做了几件事:清除了Facade中的缓存设置,Facade的Ioc容器获取了我们前面提到的config文件夹中的app文件别名使用aliases来实例化别名映射数组初始化AliasLoader并调用AliasLoader->register()publicfunctionregister(){if(!$this->registered){$this->prependToLoaderStack();$this->registered=true;}}protectedfunctionprependToLoaderStack(){spl_autoload_register([$this,'load'],true,true);}我们可以看到启动别名服务的关键就是这个spl_autoload_register,这个函数我们应该很熟悉,这个函数用来解析autoloading中的命名空间,这里是用来解析别名的真正的类名别名Aliasesservice先来看注册到spl_autoload_register的函数,load:publicfunctionload($alias){if(static::$facadeNamespace&&strpos($alias,static::$facadeNamespace)===0){$this->loadFacade($alias);返回真;}if(isset($this->aliases[$alias])){returnclass_alias($this->aliases[$alias],$alias);}}这个函数的底层很好理解,就是class_alias使用别名映射数组将别名映射到真正的门面类,但是上面是什么?其实这是laravel5.4的一个新功能,叫做实时门面服务。实时门面服务其实门面功能很简单。我们只需要定义一个类来继承Facade,但是laravel5.4打算更进一步——自动生成facade的子类,这就是实时的facade。如何使用实时门面?看下面的例子:namespaceApp\Services;classPaymentGateway{protected$tax;公共函数__construct(TaxCalculator$tax){$this->tax=$tax;}}这是一个自定义类,如果我们要提供这个类定义一个门面。在laravel5.4中我们可以这样做:使用Facades\{App\Services\PaymentGateway};Route::get('/pay/{amount}',function($amount){PaymentGateway::pay($amount);});那么这样做的原理是什么呢?我们看源码:protectedstatic$facadeNamespace='Facades\\';if(static::$facadeNamespace&&strpos($alias,static::$facadeNamespace)===0){$this->loadFacade($别名);returntrue;}如果namespace以Facades\开头,那么会调用实时facade函数,调用loadFacade函数:protectedfunctionloadFacade($alias){tap($this->ensureFacadeExists($alias),function($path){require$path;});}tap是laravel的全局帮助函数,ensureFacadeExists函数负责自动生成门面类,loadFacade负责加载门面类:protectedfunctionensureFacadeExists($alias){if(file_exists($path=storage_path('framework/cache/facade-'.sha1($alias).'.php'))){返回$路径;}file_put_contents($path,$this->formatFacadeStub($alias,file_get_contents(__DIR__.'/stubs/facade.stub')));return$path;}可以看出laravel框架生成的facade类会放在stroge/framework/cache/文件夹下,名字以facade开头,以命名空间的hash结尾如果文件存在则返回,否则使用file_put_contents生成,formatFacadeStub:protectedfunctionformatFacadeStub($alias,$stub){$replacements=[str_replace('/','\\',dirname(str_replace('\\','/',$alias))),class_basename($alias),substr($alias,strlen(static::$facadeNamespace)),];returnstr_replace(['DummyNamespace','DummyClass','DummyTarget'],$replacements,$stub);}简单来说,对于Facades\App\Services\PaymentGateway,replacements的第一项就是facade命名空间,将Facades\App\Services\PaymentGateway到Facades/App/Services/PaymentGateway,取前面的Facades/App/Services/,然后转成命名空间Facades\App\Services\;第二项是外观类的名称,PaymentGateway;第三项是门面类的服务对象,App\Services\PaymentGateway,使用这些替换门面模板文件: