laravel框架容器管理的一些要点

laravel(源码笔记) 路由注册,加载,寻址,参数绑定过程 this−routes[this−allRoutes。 阅读详情

本文面向php语言的laravel框架的用户,介绍一些laravel框架里面容器管理方面的使用要点。文章很长,但是内容应该很有用,希望有需要的朋友能看到。php经验有限,不到位的地方,欢迎帮忙指正。

1. laravel容器基本认识

laravel框架是有一个容器框架,框架应用程序的实例就是一个超大的容器,这个实例在bootstrap/app.php内进行初始化:

image

这个文件在每一次请求到达laravel框架都会执行,所创建的$app即是laravel框架的应用程序实例,它在整个请求生命周期都是唯一的。laravel提供了很多服务,包括认证,数据库,缓存,消息队列等等,$app作为一个容器管理工具,负责几乎所有服务组件的实例化以及实例的生命周期管理。这种方式能够很好地对代码进行解耦,使得应用程序的业务代码不必操心服务组件的对象从何而来,当需要一个服务类来完成某个功能的时候,仅需要通过容器解析出该类型的一个实例即可。从最终的使用方式来看,laravel容器对服务实例的管理主要包括以下几个方面:

  • 服务的绑定与解析
  • 服务提供者的管理
  • 别名的作用
  • 依赖注入

弄清这几个方面的思想, 以及laravel容器的实现机制,就能熟练掌握laravel容器的管理。

2. 如何在代码中获取到容器实例

laravel容器实例在整个请求生命周期中都是唯一的,且管理着所有的服务组件实例。那么有哪些方式能够拿到laravel容器的实例呢?常用的有以下几种方式:

1) 通过app这个help函数:

$app = app();

app这个辅助函数定义在 
image 
文件里面,这个文件定义了很多help函数,并且会通过composer自动加载到项目中。所以,在参与http请求处理的任何代码位置都能够访问其中的函数,比如app()。

2)通过App这个Facade

<?php

Route::get('/', function () {
    dd(App::basePath());
    return '';
});

通过App这个Facade拿容器实例的方式,跟上面不同的是,不能把App先赋给一个变量,然后通过变量来调用容器的方法。这是因为App相当于只是一个类名,我们不能把一个类名复制一个变量。$app = App;不是一个合法的可执行的语句,而$app = app();却是一个合法的可执行的语句,因为它后面有app(),表示函数调用。App::basePath();也是一个合法的语句,它就是在调用类的静态方法。

再补充2点:

第一点: Facade是laravel框架里面比较特殊的一个特性,每个Facade都会与容器里面的一个实例对象关联,我们可以直接通过Facade类静态方法调用的形式来调用它关联的实例对象的方法。比如App这个Facade,调用App::basePath()的时候,实际相当于app()->basePath()。这个底层机制也是依赖于php语言的特性才能实现的,需要在每一个Facade里面,设定一个静态成员并关联到一个服务的实例对象,当调用Facade类的静态方法的时候,解析出调用的方法名,再去调用关联的服务实例的同名方法,最后把结果返回。我认为理解Facade能起到什么作用就够了,不一定要深究到它底层去了解实现的细节,毕竟在实际的开发中,不用Facade,也完全不影响laravel框架的使用。另外在实际编码中,要自定义一个Facade也非常容易,只要继承laravel封装的Facade基类即可:

<?php

namespace ThirdProviders\CasServer\Facades;

use Illuminate\Support\Facades\Facade;
use ThirdProviders\CasServer\CasServerManager;

class CasServer extends Facade
{
    protected static function getFacadeAccessor()
    {
        return CasServerManager::class;
    }
}

实现Facade基类的getFacadeAccessor方法,laravel框架就知道这个Facade类该与哪个服务实例关联起来了。实际上这个getFacadeAccess方法,返回的名称就是后面要介绍的服务绑定名称。在laravel容器里面,一个服务实例,都会有一个固定的绑定名称,通过这个名称就能找到这个实例。所以为啥Facade类只要返回服务绑定名称即可。

我们可以看看App这个Facade类的代码:

<?php

namespace Illuminate\Support\Facades;

/**
 * @see \Illuminate\Foundation\Application
 */
class App extends Facade
{
    /**
     * Get the registered name of the component.
     *
     * @return string
     */
    protected static function getFacadeAccessor()
    {
        return 'app';
    }
}

它的getFacadeAccessor返回的就是一个字符串“app”,这个app就是laravel容器自己绑定自己时用的名称。

第二点: 从上一点最后App这个Facade的源码可以看出,App这个Facade的全类名其实是:Illuminate\Support\Facades\App,那为什么我们在代码里面能够直接通过App这个简短的名称就能访问到呢:

<?php

Route::get('/', function () {
    dd(App::basePath());
    return '';
});

你看以上代码完全没有用到use或者完全限定的方式来使用Illuminate\Support\Facades\App。实际上App跟Illuminate\Support\Facades\App是完全等价的,只不过App比Illuminate\Support\Facades\App要简短很多,而且不需要use,所以用起来方便,那么它是怎么实现的?这跟laravel容器配置的别名有关系,在config/app.php中,有一节aliases专门用来配置一些类型的别名:

'aliases' => [

    'App' => Illuminate\Support\Facades\App::class,
    'Artisan' => Illuminate\Support\Facades\Artisan::class,
    'Auth' => Illuminate\Support\Facades\Auth::class,
    'Blade' => Illuminate\Support\Facades\Blade::class,
    'Bus' => Illuminate\Support\Facades\Bus::class,
    'Cache' => Illuminate\Support\Facades\Cache::class,
    'Config' => Illuminate\Support\Facades\Config::class,
    'Cookie' => Illuminate\Support\Facades\Cookie::class,
    'Crypt' => Illuminate\Support\Facades\Crypt::class,
    'DB' => Illuminate\Support\Facades\DB::class,
    'Eloquent' => Illuminate\Database\Eloquent\Model::class,
    'Event' => Illuminate\Support\Facades\Event::class,
    'File' => Illuminate\Support\Facades\File::class,
    'Gate' => Illuminate\Support\Facades\Gate::class,
    'Hash' => Illuminate\Support\Facades\Hash::class,
    'Lang' => Illuminate\Support\Facades\Lang::class,
    'Log' => Illuminate\Support\Facades\Log::class,
    'Mail' => Illuminate\Support\Facades\Mail::class,
    'Notification' => Illuminate\Support\Facades\Notification::class,
    'Password' => Illuminate\Support\Facades\Password::class,
    'Queue' => Illuminate\Support\Facades\Queue::class,
    'Redirect' => Illuminate\Support\Facades\Redirect::class,
    'Redis' => Illuminate\Support\Facades\Redis::class,
    'Request' => Illuminate\Support\Facades\Request::class,
    'Response' => Illuminate\Support\Facades\Response::class,
    'Route' => Illuminate\Support\Facades\Route::class,
    'Schema' => Illuminate\Support\Facades\Schema::class,
    'Session' => Illuminate\Support\Facades\Session::class,
    'Storage' => Illuminate\Support\Facades\Storage::class,
    'URL' => Illuminate\Support\Facades\URL::class,
    'Validator' => Illuminate\Support\Facades\Validator::class,
    'View' => Illuminate\Support\Facades\View::class
],

然后在laravel框架处理请求过程中,会通过Illuminate\Foundation\Bootstrap\RegisterFacades这个类来注册这些别名到全局环境里面:

<?php

namespace Illuminate\Foundation\Bootstrap;

use Illuminate\Support\Facades\Facade;
use Illuminate\Foundation\AliasLoader;
use Illuminate\Contracts\Foundation\Application;

class RegisterFacades
{
    /**
     * Bootstrap the given application.
     *
     * @param  \Illuminate\Contracts\Foundation\Application  $app
     * @return void
     */
    public function bootstrap(Application $app)
    {
        Facade::clearResolvedInstances();

        Facade::setFacadeApplication($app);

        AliasLoader::getInstance($app->make('config')->get('app.aliases', []))->register();
    }
}

所以我们才能直接通过别名,代替完整的类型名做同样的访问功能。如果你自己写了一些类,名称很长,并且在代码里面用的特别多,也可以考虑配置到config/app.php别名里面去,laravel会帮我们注册。

3)另外一种方式拿到laravel容器实例就是在服务提供者里面直接使用$this->app

服务提供者后面还会介绍,现在只是引入。因为服务提供者类都是由laravel容器实例化的,这些类都继承自Illuminate\Support\ServiceProvider,它定义了一个实例属性$app:

image

laravel在实例化服务提供者的时候,会把laravel容器实例注入到这个$app上面。所以我们在服务提供者里面,始终能通过$this->$app访问到laravel容器实例,而不需要再使用app()函数或者App Facade了。

3. 直观的认识laravel容器

一直在说容器,既然它是用来存取实例对象的时候,那么它里面应该至少有一个数组充当容器存储功能的角色才行,所以我们可以通过打印的方式来直观地看下laravel容器实例的结构:

<?php

Route::get('/', function () {
    dd(app());
    return '';
});

结果如下: 
image 
从这个结构可以看出,laravel容器实例上包含了很多的数组,其中红框部分的数组,从名字也可以猜测出它们跟后面要介绍的服务,服务提供者与服务别名之间的联系。理清这几个数组的存储结构,自然就明白了laravel容器如何管理服务。

4. 如何理解服务绑定与解析

浅义层面理解,容器既然用来存储对象,那么就要有一个对象存入跟对象取出的过程。这个对象存入跟对象取出的过程在laravel里面称为服务的绑定与解析。

先来看服务绑定,在laravel里面,服务绑定到容器,有多种形式:

app()->singleton('service', 'this is service1');

app()->singleton('service2', [
    'hi' => function(){
        //say hi
    }
]);

class Service {

}

app()->singleton('service3', function(){
    return new Service();
});

singleton是laravel服务绑定的方法之一,详细作用后面会介绍,目前只是用它来展现服务绑定的形式。笼统的说容器的时候,我们说容器管理的是服务对象,但是laravel的容器可以管理不仅仅是对象,它能够管理的是任意类型的数据,包括基本数据类型和对象。所以在服务绑定的时候,我们也可以绑定任意的数据,正如以上代码展示的那样。在绑定的时候,我们可以直接绑定已经初始化好的数据(基本类型、数组、对象实例),还可以用匿名函数来绑定。用匿名函数的好处在于,这个服务绑定到容器以后,并不会立即产生服务最终的对象,只有在这个服务解析的时候,匿名函数才会执行,此时才会产生这个服务对应的服务实例。

实际上,当我们使用singleton,bind方法以及数组形式,(这三个方法是后面要介绍的绑定的方法),进行服务绑定的时候,如果绑定的服务形式,不是一个匿名函数,也会在laravel内部用一个匿名函数包装起来,这样的话, 不轮绑定什么内容,都能做到前面介绍的懒初始化的功能,这对于容器的性能是有好处的。这个可以从bind的源码中看到一些细节:

image

服务绑定时的第一个参数就是服务的绑定名称。服务绑定完成后,容器会把这个服务的绑定记录存储到实例属性bindings里面:

image

这个bindings里面的每条记录,代表一个服务绑定。它的key值就是服务的绑定名称,value值也是一个数组,这个数组的concrete属性就是服务绑定时产生的匿名函数,也就是闭包;另外一个参数表示这个服务在多次解析的时候,是否只返回第一次解析得到的对象。这个参数在介绍服务绑定方法时会再继续介绍。

接下来看看服务绑定的几种方法及区别:

a. 通过bind方法

app()->bind('service', function(){
    return new Service();
},true);

bind是laravel服务绑定的底层方法,它的签名是:

image

第一个参数服务绑定名称,第二个参数服务绑定的结果,第三个参数就表示这个服务是否在多次解析的时候,始终返回第一次解析出的实例。它的默认值是false,意味着这样的服务在每次解析的时候都会返回一个新的实例。它的值与bindings里面服务绑定记录value数组里面的share属性是对应的。

b. 通过singleton方法

举例略。它跟bind的区别在于,它始终是以shared=true的形式进行服务绑定,这是因为它的源码是这样的:

image

c. 通过数组的形式

app()['service'] = function(){
    return new Service();
};

为什么可以直接把容器实例直接当成数组来用呢,这是因为容器实现了php的ArrayAccess接口:

/**
 * Set the value at a given offset.
 *
 * @param  string  $key
 * @param  mixed   $value
 * @return void
 */
public function offsetSet($key, $value)
{
    // If the value is not a Closure, we will make it one. This simply gives
    // more "drop-in" replacement functionality for the Pimple which this
    // container's simplest functions are base modeled and built after.
    if (! $value instanceof Closure) {
        $value = function () use ($value) {
            return $value;
        };
    }

    $this->bind($key, $value);
}

所以实际上以上这种数组形式的绑定实际上相当于没有第三个参数的bind方法。

再来看服务的解析。上面的内容都是在说明把如何获取服务实例的方式绑定到容器,那么如何从容器获取到需要的服务实例呢?这个过程就是服务解析,在laravel里面通过make方法来完成服务的解析:

$service= app()->make('service');

这个方法接收两个参数,第一个是服务的绑定名称和服务绑定名称的别名,如果是别名,那么就会根据服务绑定名称的别名配置,找到最终的服务绑定名称,然后进行解析;第二个参数是一个数组,最终会传递给服务绑定产生的闭包。

我们可以通过make的源码理解服务解析的逻辑,这个是Illuminate\Container\Container类中的make方法源码,laravel的容器实例是Illuminate\Foundation\Application类的对象,这个类继承了Illuminate\Container\Container,这里暂时只展示Illuminate\Container\Container类中的make方法的代码,先不涉及Illuminate\Foundation\Application类的make方法,因为后者覆盖了Illuminate\Container\Container类中的make方法,加了一些服务提供者的逻辑,所以这里先不介绍它。其实前面的很多源码也都是从Illuminate\Container\Container中拿出来的,不过那些代码Application没有覆盖,不影响内容的介绍。

public function make($abstract, array $parameters = [])
{
    $abstract = $this->getAlias($this->normalize($abstract));

    // If an instance of the type is currently being managed as a singleton we'll
    // just return an existing instance instead of instantiating new instances
    // so the developer can keep using the same objects instance every time.
    if (isset($this->instances[$abstract])) {
        return $this->instances[$abstract];
    }

    $concrete = $this->getConcrete($abstract);

    // We're ready to instantiate an instance of the concrete type registered for
    // the binding. This will instantiate the types, as well as resolve any of
    // its "nested" dependencies recursively until all have gotten resolved.
    if ($this->isBuildable($concrete, $abstract)) {
        $object = $this->build($concrete, $parameters);
    } else {
        $object = $this->make($concrete, $parameters);
    }

    // If we defined any extenders for this type, we'll need to spin through them
    // and apply them to the object being built. This allows for the extension
    // of services, such as changing configuration or decorating the object.
    foreach ($this->getExtenders($abstract) as $extender) {
        $object = $extender($object, $this);
    }

    // If the requested type is registered as a singleton we'll want to cache off
    // the instances in "memory" so we can return it later without creating an
    // entirely new instance of an object on each subsequent request for it.
    if ($this->isShared($abstract)) {
        $this->instances[$abstract] = $object;
    }

    $this->fireResolvingCallbacks($abstract, $object);

    $this->resolved[$abstract] = true;

    return $object;
}

从这个源码可以看到:

a. 在解析一个服务的时候,它会先尝试把别名转换成有效的服务绑定名称;

b. 如果这个服务是一个shared为true的服务绑定,且之前已经做过解析的话,就会直接返回之前已经解析好的对象;

c. 如果这个服务是一个shared为true的服务绑定,并且是第一次解析的话,就会把已解析的对象存入到instances这个容器属性里面去,也就是说只有shared为true的服务绑定,在解析的时候才会往instances属性里面存入记录,否则不会存入;

d. 解析完毕,还会在容器的resolved属性里面存入一条记录,表示这个服务绑定解析过;

e. resolved,instances数组的key值跟bindings数组的key值一样,都是服务绑定名称;

f. 服务绑定的shared属性在整个服务绑定生命周期内都是不能更改的。

服务的解析也有多种形式,常用的有:

a. make方法

b. 数组形式

app()['service'];

这个的原理还是跟容器实现了ArrayAccess的接口有关系:

public function offsetGet($key)
{
    return $this->make($key);
}

所以数组形式的访问跟不使用第二个参数的make方法形式是一样的。

c. app($service)的形式

app('service');

看了app这个help函数的源码就明白了:

function app($make = null, $parameters = [])
{
    if (is_null($make)) {
        return Container::getInstance();
    }

    return Container::getInstance()->make($make, $parameters);
}

原来app这个函数在第一个参数为空的时候,返回的是容器实例本身。在有参数的时候等价于调用容器实例的make方法。

以上就是服务绑定与解析的主要内容,涉及的要点较多,希望描述的比较清楚。

5. 服务提供者的作用与使用

前面介绍了服务的绑定。那么服务的绑定应该在哪个位置处理呢?虽然说,能够拿到容器实例的地方,就都能进行服务的绑定;但是我们使用服务的绑定的目的,是为了在合适的位置解析出服务实例并使用,如果服务绑定的位置过于随意,那么就很难保证在解析的位置能够准确的解析出服务实例。因为服务能够解析的前提是服务绑定的代码先与服务解析的代码执行;所以,服务绑定通常会在应用程序初始化的时候进行,这样才能保证业务代码中(通常是router和controller里面)一定能解析出服务实例。这个最佳的位置就是服务提供者。

服务提供者,在laravel里面,其实就是一个工厂类。它最大的作用就是用来进行服务绑定。当我们需要绑定一个或多个服务的时候,可以自定义一个服务提供者,然后把服务绑定的逻辑都放在该类的实现中。在larave里面,要自定一个服务提供者非常容易,只要继承Illuminate\Support\ServiceProvider这个类即可。下面通过一个简单的自定义服务提供者来说明服务提供者的一些要点:

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    protected $defer = true;

    public function boot()
    {
        //
    }

    public function register()
    {
        $this->app->singleton('service1', function(){
            return 'service1';
        });
        $this->app->singleton('service2', function(){
            return 'service2';
        });
        $this->app->singleton('service3', function(){
            return 'service3';
        });
    }

    public  function provides()
    {
        return ['service1','service2','service3'];
    }
}

1). 首先,自定义的服务提供者都是放在下面这个目录的: 
image 
其实你放在哪都可以,不过得告诉laravel你的服务提供者在哪,laravel才会帮你注册。怎么告诉它,后面还有介绍。

2)在这个举例里面,可以看到有一个register方法,这个方法是ServiceProvider里面定义的。自定义的时候,需要重写它。这个方法就是用来绑定服务的。你可以在这个服务里面,根据需要加入任意数量的服务绑定。前面要介绍过,在服务提供者里面,始终能通过$this->app拿到容器实例,所以上面的举例中,我们直接用这种方式来完成服务绑定。这个方法是怎么完成服务绑定的呢?因为当laravel找到这个服务提供者的类以后,就会初始化这个服务提供者类,得到一个服务提供者的对象,然后调用它的register方法,自然它里面的所有服务绑定代码就都会执行了。

laravel初始化自定义服务提供者的源码是:

public function registerConfiguredProviders()
    {
        $manifestPath = $this->getCachedServicesPath();

        (new ProviderRepository($this, new Filesystem, $manifestPath))
                    ->load($this->config['app.providers']);
    }

这个代码是在Illuminate\Foundation\Application的源码里面拿出来的,从中你能看到laravel会把所有的自定义服务提供者都注册进来。这个注册的过程其实就是前面说的实例化服务提供者的类,并调用register方法的过程。

3). 从上一步的源码也能看到,laravel加载自定义服务提供者的时候,实际是从config/app.php这个配置文件里面的providers配置节找到所有要注册的服务提供者的。

'providers' => [

    /*
     * Laravel Framework Service Providers...
     */
    Illuminate\Auth\AuthServiceProvider::class,
    Illuminate\Broadcasting\BroadcastServiceProvider::class,
    Illuminate\Bus\BusServiceProvider::class,
    Illuminate\Cache\CacheServiceProvider::class,
    Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
    Illuminate\Cookie\CookieServiceProvider::class,
    Illuminate\Database\DatabaseServiceProvider::class,
    Illuminate\Encryption\EncryptionServiceProvider::class,
    Illuminate\Filesystem\FilesystemServiceProvider::class,
    Illuminate\Foundation\Providers\FoundationServiceProvider::class,
    Illuminate\Hashing\HashServiceProvider::class,
    Illuminate\Mail\MailServiceProvider::class,
    Illuminate\Notifications\NotificationServiceProvider::class,
    Illuminate\Pagination\PaginationServiceProvider::class,
    Illuminate\Pipeline\PipelineServiceProvider::class,
    Illuminate\Queue\QueueServiceProvider::class,
    Illuminate\Redis\RedisServiceProvider::class,
    Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
    Illuminate\Session\SessionServiceProvider::class,
    Illuminate\Translation\TranslationServiceProvider::class,
    Illuminate\Validation\ValidationServiceProvider::class,
    Illuminate\View\ViewServiceProvider::class,

    /*
     * Package Service Providers...
     */

    //

    /*
     * Application Service Providers...
     */
    App\Providers\AppServiceProvider::class,
    App\Providers\AuthServiceProvider::class,
    // App\Providers\BroadcastServiceProvider::class,
    App\Providers\EventServiceProvider::class,
    App\Providers\RouteServiceProvider::class,
    Xavrsl\Cas\CasServiceProvider::class,
    ThirdProviders\CasServer\CasServerProvider::class
],

所以你如果自己写了一个服务提供者,那么只要配置到这里面,laravel就会自动帮你注册它了。

4)除了register方法,服务提供者里面还有一个boot方法,这个boot方法,会在所有的服务提供者都注册完成之后才会执行,所以当你想在服务绑定完成之后,通过容器解析出其它服务,做一些初始化工作的时候,那么就可以这些逻辑写在boot方法里面。因为boot方法执行的时候,所有服务提供者都已经被注册完毕了,所以在boot方法里面能够确保其它服务都能被解析出来。

5)前面说的服务提供者的情况,在laravel应用程序初始化的时候,就会去注册服务提供者,调用register方法。但是还有一种需求,你可能需要在真正用到这个服务提供者绑定的服务的时候,才会去注册这个服务提供者,以减少不必要的注册处理,提高性能。这也是延迟处理的一种方式。那么这种服务提供者该怎么定义呢?

其实最前面的这个举例已经告诉你了,只要定义一个$defer的实例属性,并把这个实例属性设置为true,然后添加一个provides的实例方法即可。这两个成员都是ServiceProvider基类里面定义好的,自定义的时候,只是覆盖而已。

在基类中,$defer的默认值是false,表示这个服务提供者不需要延迟注册。provides方法,只要简单的返回这个服务提供register方法里面,注册的所有服务绑定名称即可。

延迟注册的服务提供者的机制是:

  • 当laravel初始化服务提供者的实例后,如果发现这个服务提供者的$defer属性为true,那么就不会去调用它的register方法
  • 当laravel解析一个服务的时候,如果发现这个服务是由一个延迟服务提供的(它怎么知道这个服务是延迟服务提供的,是provides方法告诉它的),那么就会先把这个延迟服务提供者先注册,再去解析。这个可以看看Illuminate\Foundation\Application的make方法就清楚了: 
    public function make($abstract, array $parameters = [])
    {
        $abstract = $this->getAlias($abstract);
    
        if (isset($this->deferredServices[$abstract])) {
            $this->loadDeferredProvider($abstract);
        }
    
        return parent::make($abstract, $parameters);
    }

6)还记得容器实例结构上几个带有providers名称的属性数组吧:

image

在了解以上provider的机制后,这几个数组的作用也就比较清晰了。其中serviceProviders用来存放所有已经注册完毕的服务提供者:

image

loadedProviders跟serviceProviders的作用类似,只是存储的记录形式不同:

image

deferredProviders用来存储所有的延迟注册的服务提供者:

image

跟前面两个不同的是,deferredProviders存储的记录的key值并不是服务提供者的类型名称,而是服务提供者的provides返回数组里面的名称。并且如果一个服务提供者的provides里面返回了多个服务绑定名称的话,那么deferredProviders里面就会存多条记录:

image

这样是方便根据服务绑定名称,找到对应的服务提供者,并完成注册。当服务的解析的时候,会先完成延迟类型的服务提供者的注册,注册完毕,这个服务绑定名称在deferredProviders对应的那条记录就会删除掉。不过如果一个服务提供者provides了多个服务绑定名称,解析其中一个服务的时候,只移除该名称对应的deferredProviders记录,而不是所有。

7)服务提供者还有一个小问题值的注意,由于php是一门基本语言,在处理请求的时候,都会从入口文件把所有php都执行一遍。为了性能考虑,laravel会在第一次初始化的时候,把所有的服务提供者都缓存到bootstrap/cache/services.php文件里面,所以有时候当你改了一个服务提供者的代码以后,再刷新不一定能看到期望的效果,这有可能就是因为缓存所致。这时把services.php删掉就能看到你要的效果了。

6. 服务绑定名称的别名

前面介绍的别名是在config/app.php的aliases配置节里面定义的,那个别名的作用仅仅是简化类名的时候,laravel帮你把长的类型名注册成为简短的名称,然后在全局环境了里面都能使用。laravel还存在另外一个别名,就是服务绑定名称的别名。通过服务绑定的别名,在解析服务的时候,跟不使用别的效果一致。别名的作用也是为了同时支持全类型的服务绑定名称以及简短的服务绑定名称考虑的。

1)如何指定和使用服务绑定名称的别名

假如有一个服务做如下绑定:

app()->singleton('service1', function(){
    new CasServerManager();
});

那么可以通过容器方法alias方法指定别名:

app()->alias('service1', 'alias_a');

这个方法的第一个参数是服务绑定名称,第二个参数是别名。这个方法调用后,就会在容器实例属性aliases数组里面存入一条记录:

image 
image

你看刚才举例中的别名就已经添加到这个数组里面。这个数组里面每条记录的key值都是别名。但是value有可能是服务绑定名称,也有可能是另外一个别名。这是因为别名是可以递归的。

2)别名支持递归

也就是说,可以对别名再指定别名:

app()->alias('alias_a', 'alias_b');
app()->alias('alias_b', 'alias_c');

image

3)别名如何应用于服务解析

在解析服务的时候,会先确定这个服务名称是否为一个别名(只要看看在aliases数组里是否存在记录即可),如果不是别名,直接用这个服务名称进行解析。如果这个服务名称是一个别名,那么就会通过调用的方式,找到最终的服务名称:

image

如下所有的服务解析都是等价的:

app('alias_c');
app('alias_b');
app('alias_a');
app('service1');

4)另外一种指定别名的方式

可以在服务绑定的时候,进行别名的指定。只要按照如下的方式进行绑定即可:

app()->singleton(['service1' => 'alias'], function(){
    new CasServerManager();
});

也就是把服务绑定名称换成数组形式而已。数组记录的key值就是服务名称,value值就是别名。

7. 依赖注入的机制

<?php

class Service{
    protected $app;

    public function __construct(\Illuminate\Contracts\Foundation\Application $app)
    {
        $this->app = $app;
    }
}

app()->singleton(Service::class);

Route::get('/', function () {
    dd(app(Service::class));
    return '';
});

在这个举例中,定义了一个Service类,这个类有一个实例成员$app,它需要一个实现了\Illuminate\Contracts\Foundation\Application 接口的实例对象,也就是容器实例。然后通过直接使用类型名称的方式把这个类快速地绑定到了容器。app()->singleton(Service::class),等价于app()->singleton(Service::class,Service:class)。这种通过类名形式的绑定,laravel在解析的时候会调用这个类型的构造函数来实例化服务。并且在调用构造函数的时候,会通过反射获得这个构造函数的参数类型,然后从容器已有的绑定中,解析出对应参数类型的服务实例,传入构造函数完成实例化。这个过程就是所谓的依赖注入。

在以上代码中,完全没有手写的new Service(app())代码,就能正确地解析到service实例,这就是依赖注入的好处:

image

当一个类需要某个服务类型的实例时,不需要自己去创造这个服务的实例,只要告诉容器,它需要的实例类型即可,然后容器会根据这个类型, 解析出满足该类型的服务。如何根据参数类型解析出该参数类型的服务实例呢?其实就是根据参数类型的类型名称进行解析得到的,所以依赖注入能够成功的前提是根据参数类型的名称,能够成功地解析到一个服务对象。以上之所以能够通过Illuminate\Contracts\Foundation\Application 这个名称解析到服务,那是因为在容器实例aliases数组里面有一条Illuminate\Contracts\Foundation\Application 的别名记录:

image

也就是说Illuminate\Contracts\Foundation\Application 实际上是app这个服务绑定名称的一个别名,所以laravel在解析Illuminate\Contracts\Foundation\Application的时候,就能得到对应的服务实例了。

这些别名属于laravel容器核心的别名,在laravel初始化的时候会被注册:

public function registerCoreContainerAliases()
{
    $aliases = [
        'app'                  => ['Illuminate\Foundation\Application', 'Illuminate\Contracts\Container\Container', 'Illuminate\Contracts\Foundation\Application'],
        'auth'                 => ['Illuminate\Auth\AuthManager', 'Illuminate\Contracts\Auth\Factory'],
        'auth.driver'          => ['Illuminate\Contracts\Auth\Guard'],
        'blade.compiler'       => ['Illuminate\View\Compilers\BladeCompiler'],
        'cache'                => ['Illuminate\Cache\CacheManager', 'Illuminate\Contracts\Cache\Factory'],
        'cache.store'          => ['Illuminate\Cache\Repository', 'Illuminate\Contracts\Cache\Repository'],
        'config'               => ['Illuminate\Config\Repository', 'Illuminate\Contracts\Config\Repository'],
        'cookie'               => ['Illuminate\Cookie\CookieJar', 'Illuminate\Contracts\Cookie\Factory', 'Illuminate\Contracts\Cookie\QueueingFactory'],
        'encrypter'            => ['Illuminate\Encryption\Encrypter', 'Illuminate\Contracts\Encryption\Encrypter'],
        'db'                   => ['Illuminate\Database\DatabaseManager'],
        'db.connection'        => ['Illuminate\Database\Connection', 'Illuminate\Database\ConnectionInterface'],
        'events'               => ['Illuminate\Events\Dispatcher', 'Illuminate\Contracts\Events\Dispatcher'],
        'files'                => ['Illuminate\Filesystem\Filesystem'],
        'filesystem'           => ['Illuminate\Filesystem\FilesystemManager', 'Illuminate\Contracts\Filesystem\Factory'],
        'filesystem.disk'      => ['Illuminate\Contracts\Filesystem\Filesystem'],
        'filesystem.cloud'     => ['Illuminate\Contracts\Filesystem\Cloud'],
        'hash'                 => ['Illuminate\Contracts\Hashing\Hasher'],
        'translator'           => ['Illuminate\Translation\Translator', 'Symfony\Component\Translation\TranslatorInterface'],
        'log'                  => ['Illuminate\Log\Writer', 'Illuminate\Contracts\Logging\Log', 'Psr\Log\LoggerInterface'],
        'mailer'               => ['Illuminate\Mail\Mailer', 'Illuminate\Contracts\Mail\Mailer', 'Illuminate\Contracts\Mail\MailQueue'],
        'auth.password'        => ['Illuminate\Auth\Passwords\PasswordBrokerManager', 'Illuminate\Contracts\Auth\PasswordBrokerFactory'],
        'auth.password.broker' => ['Illuminate\Auth\Passwords\PasswordBroker', 'Illuminate\Contracts\Auth\PasswordBroker'],
        'queue'                => ['Illuminate\Queue\QueueManager', 'Illuminate\Contracts\Queue\Factory', 'Illuminate\Contracts\Queue\Monitor'],
        'queue.connection'     => ['Illuminate\Contracts\Queue\Queue'],
        'queue.failer'         => ['Illuminate\Queue\Failed\FailedJobProviderInterface'],
        'redirect'             => ['Illuminate\Routing\Redirector'],
        'redis'                => ['Illuminate\Redis\Database', 'Illuminate\Contracts\Redis\Database'],
        'request'              => ['Illuminate\Http\Request', 'Symfony\Component\HttpFoundation\Request'],
        'router'               => ['Illuminate\Routing\Router', 'Illuminate\Contracts\Routing\Registrar'],
        'session'              => ['Illuminate\Session\SessionManager'],
        'session.store'        => ['Illuminate\Session\Store', 'Symfony\Component\HttpFoundation\Session\SessionInterface'],
        'url'                  => ['Illuminate\Routing\UrlGenerator', 'Illuminate\Contracts\Routing\UrlGenerator'],
        'validator'            => ['Illuminate\Validation\Factory', 'Illuminate\Contracts\Validation\Factory'],
        'view'                 => ['Illuminate\View\Factory', 'Illuminate\Contracts\View\Factory'],
    ];

    foreach ($aliases as $key => $aliases) {
        foreach ($aliases as $alias) {
            $this->alias($key, $alias);
        }
    }
}

依赖注入更多地用在接口编程当中,就像上面的举例类似。再看一个自定义的例子:

<?php

interface Inter{
    public function method();
}

class InterImpl implements Inter{
    public function method(){
        //
    }
}

class Service{
    protected $inter;

    public function __construct(Inter $inter)
    {
        $this->inter = $inter;
    }
}

app()->singleton(Inter::class,InterImpl::class);
app()->singleton(Service::class);

Route::get('/', function () {
    dd(app(Service::class));
    return '';
});

按接口进行编程,像Service这种业务类,只需要声明自己需要一个Inter类型的实例即可。接口的好处在于解耦,将来要更换一种Inter的实现,不需要改Service的代码,只需要在实例化Service的时候,传入另外一个Inter的实例即可。有了依赖注入以后,也不用改Service实例化的代码,只要把Inter这个服务类型,重新做一个绑定,绑定到另外一个实现即可。

app()->singleton(Inter::class,InterImpl2::class);

8. 其它

还有两个小点,也值的介绍一下。

1) 容器实例的instance方法

这个方法其实也是完成绑定的作用,但是它跟前面介绍的三种绑定方法不同,它是把一个已经存在的实例,绑定到容器:

$service = new Service();
app()->instance('service',$service);

这是它的源码:

public function instance($abstract, $instance)
    {
        $abstract = $this->normalize($abstract);

        // First, we will extract the alias from the abstract if it is an array so we
        // are using the correct name when binding the type. If we get an alias it
        // will be registered with the container so we can resolve it out later.
        if (is_array($abstract)) {
            list($abstract, $alias) = $this->extractAlias($abstract);

            $this->alias($abstract, $alias);
        }

        unset($this->aliases[$abstract]);

        // We'll check to determine if this type has been bound before, and if it has
        // we will fire the rebound callbacks registered with the container and it
        // can be updated with consuming classes that have gotten resolved here.
        $bound = $this->bound($abstract);

        $this->instances[$abstract] = $instance;

        if ($bound) {
            $this->rebound($abstract);
        }
    }

从这个代码可以看到,instance方法,会直接把外部实例化好的对象,直接存储到容器的instances里面。如果这个服务绑定名称存在bindings记录,那么还会做一下重新绑定的操作。也就是说,通过intance方法绑定,是直接绑定服务实例,而原来的bind方法其实只是绑定了一个闭包函数,服务实例要到解析的时候才会创建。

2) 容器实例的share方法

容器实例的singleton方法,绑定的服务在解析的时候,始终返回第一次解析的对象。还有一个方式也能做到这个效果,那就是使用share方法包装服务绑定的匿名函数:

$this->app['cas'] = $this->app->share(function()
{
    $config = $this->app['config']->get('cas');
    return new CasManager($config);
});

当我们使用app('cas')解析的时候,始终拿到的都是第一次解析创建的那个CasManager对象。这个跟share方法的实现有关系:

image

从源码看出,share方法把服务绑定的闭包再包装了一下,返回一个新的闭包,并且在这个闭包里面,加了一个静态$object变量,它会存储原始闭包第一次解析调用后的结果,并在后续解析中直接返回,从而保证这个服务的实例只有一个。


laravel源码解析之ServiceProvider机制 文档:Service Providers - Laravel - The PHP Framework For Web Artisans 1.简介 Illuminate\Support\ServiceProvider是所有 Laravel 应用程序引导的中心位置。 应用程序,以及 Laravel 的所有核心服务,都是通过服务提供者引导的。 2.Application::register 这个方法是所有ServiceProvider的注册入口 public function 阅读详情

相关推荐

二手房买卖合同范本改造:条款联动、日期校验与风险排错

在房产交易中,合同文本本质上是多个时间节点、付款条件与权属义务之间的约束网络。房屋买卖合同模板若只做静态填空,容易产生条款冲突;通过把解押、贷款审批、过户、交房等节点串成依赖链,并对税费承担、资金监管、违约责任做交叉校验,可显著降低履约争议。对二手房买卖双方和中介法务,一份可编辑的WORD范本更适合作为底稿,结合字段自检、日期试算和一致性排错,将模板改造成可执行合同。这也自然指向“最新房屋买卖合同二手房买卖合同WORD格式范本”的改造与落地。

weixin_33979203的博客 259

Laravel禁止对Alias自动加表前缀

前言 使用DB::table('menu as m')->get()生成的SQL语句是select * from menu as prefix_m,可见Laravel自动给alias加了表前缀,可以通过预先注册Grammar实现的方式修改。 缺点:不同数据库引擎需要分别编写不同的实现 优点:简单可靠方便 使用后,SQL语句变成了select * from menu as m 适用版本:Laravel5.x、Laravel7.x (6.x未测试) 代码 <?php namespace App\

wang805447391的博客 2053

浅谈laravel aliases别名的原理

在laravel发现有些类可以直接use 类名,就能使用了,例如use DB;就可以使用DB类了,问题是DB这个类并不在根命名空间,这里面实际就是用到了别名。 先通过如下例子来分析基本原理 建立如下文件upload.php,内容为 <?php namespace test\test2; class upload{ public function test(){ return 123; } } 2 建立文件index.php,内容为 <?php namespace b; require('upload.php'); class_alias ( '\test\test2\upload'

Laravel源码解析之服务提供者

官方文档中关于服务提供者的这篇文档,主要是说明如何编写自定义的服务提供者。本篇注解,就来说说啥是服务提供者,然后再总结一下如何编写服务提供者。服务提供者想要理解,首先需要理解什么是服务容器,可以移步服务容器 进行了解。在有了服务容器的概念后,就可以很容易的理解什么是服务提供者了。服务容器是盛放服务的容器,有绑定和解析两个主要操作。绑定就是将服务注册到容器中。而服务提供者,就是完成绑定服务到容器任务的单元。看名字就知道,是用来提供服务的单位。简言之,服务容器中绑定的服务,就是由服务提供者绑定进去的。...

u010602865的博客 732

【GD32篇】驱动AD7616完成数据采集

GD32F10X驱动AD7616完成数据采集

we_long的博客 1万+

使用 Java + PaddleOCR 实现表格识别,支持导出 HTML 和 Excel(附源码)

使用Java+PaddleOCR快速搭建表格识别系统,无需处理底层模型细节。系统支持表格结构识别和内容识别,可将图片转换为可编辑表格格式。文章提供了完整的实现流程,包括Maven依赖引入、模型下载(包含检测、识别、分类和表格结构模型)及参数配置说明。SmartJavaAI支持多种高性能模型选择,适用于不同应用场景,帮助开发者快速实现表格识别功能。

deng775747758的博客 1474

[php]laravel框架容器管理的一些要点

本文面向php语言的laravel框架的用户,介绍一些laravel框架里面容器管理方面的使用要点。文章很长,但是内容应该很有用,希望有需要的朋友能看到。php经验有限,不到位的地方,欢迎帮忙指正。 1. laravel容器基本认识 laravel框架是有一个容器框架,框架应用程序的实例就是一个超大的容器,这个实例在bootstrap/app.php内进行初始化: 这个文件在每一次请求到...

weixin_34001430的博客 226

医学影像AI的幻觉问题与CCD解决方案

医学影像AI在辅助诊断中面临的核心挑战是幻觉现象,即模型生成不符合实际影像的虚假诊断。这种现象源于多模态大语言模型(MLLMs)在视觉-语言对齐上的技术局限,可能导致假阳性或假阴性的临床风险。通过临床对比解码(CCD)框架,结合症状锚定和专家引导的双阶段校准机制,可有效提升模型输出的可靠性。该技术在CheXpert和MIMIC-CXR等标准数据集上验证,显著改善了肺不张、胸腔积液等关键指标的检测准确率。对于医疗AI落地场景,这种即插即用的解决方案既规避了数据隐私合规问题,又降低了基层医院的部署门槛,为AI辅

weixin_36303807的博客 322

laravel框架简单总结

01 — 什么是 laravel Laravel 是 Taylor Otwell 开发的一款基于 PHP 语言的 Web 开源框架,采用了 MVC 的架构模式。 由于 Laravel 具备 Rails 敏捷开发等优秀特质,深度集成 PHP 强大的扩展包(Composer)生态,让 Laravel 在发布之后的短短几年时间得到了极其迅猛的发展。 下面 我分享一张 图片,此图是goole提供,lara...

dong的博客 1017

一文讲透什么是ADC/DAC中的DDC、DUC、采样率以及数据率等概念

摘要:ADC/DAC中的采样率与数据率概念不同,采样率是转换器的时钟速率,而数据率是数字输出的实际速率。抽取通过滤波降低数据速率,提升信噪比但牺牲带宽;内插则通过插零和滤波提高采样率,将镜像信号移远但增加延迟和功耗。JESD204B标准的高速转换器需平衡通道数与串行速率,内插/抽取技术是关键。文章还解析了DDC、DUC的应用及优缺点,为高速数据转换设计提供参考。(150字)

weixin_49761275的博客 4268

如何快速部署Opus团队知识库?5步实战教程

Opus是一款专为技术团队设计的开源知识库应用,能够集中管理文档、促进知识共享与协作。本文将提供一套完整的Opus部署实战指南,帮助开发团队在5个步骤内完成环境配置、容器部署和系统初始化,快速搭建企业级知识管理平台。 ## 环境准备与配置技巧 在开始部署之前,需要确保系统满足以下基础要求: **系统要求:** - Docker 20.10+ 和 Docker Compose 2.0+ - G

gitblog_00942的博客 442

还在纠结二层交换机和三层交换机的区别?这篇文章帮你讲明白!

你好,这里是网络技术联盟站,我是瑞哥。公众号后台有小伙伴想要写一篇文章具体介绍一下二层交换机和三层交换机,此类的文章,之前写过几篇,今天瑞哥再写一篇,希望对大家有所帮助!

网络技术联盟站 4441

Laravel框架的服务容器

Laravel 服务容器是一个用于管理类依赖以及实现依赖注入的强有力工具。依赖注入这个名词表面看起来花哨,实质上是指:通过构造函数,或者某些情况下通过「setter」方法将类依赖「注入」到类中。

爱吃五花肉EMin的博客 888

YOLOv12创新改进 | 全网独家创新篇、小目标检测改进 | AAAI 2025 | 引入HS-FPN中的HFP和SDP创新点,从频域增强小目标特征,淘汰FPN进行升级,助力YOLOv12有效涨点

本文提出了HS-FPN(高频和空间感知特征金字塔网络),通过HFP(高频感知)模块增强小目标特征,SDP(空间依赖感知)模块捕获像素间空间依赖关系。实验在AI-TOD等小目标数据集上验证了该方法的有效性,相比FPN显著提升检测性能。文章详细介绍了HS-FPN的网络结构、核心代码实现及在YOLOv12中的应用方法,并提供多种改进版本的yaml配置文件,为小目标检测任务提供了有效的解决方案。

Ai缝合怪、助力小伙伴高效发论文 338

可控异常的契约化设计:在 ABAP 中用 CX_STATIC_CHECK 把可预期错误变成接口的一部分

文章摘要: 本文探讨了ABAP系统中异常处理的工程化设计原则,强调将可预期的业务异常作为接口契约的一部分。核心观点是:对于调用方能够合理处理的错误情况,应使用继承自CX_STATIC_CHECK的受检异常,通过方法签名强制调用方处理或显式转发。文章对比了ABAP三大异常根类的适用场景,提出"三问法"判断异常是否适合受检处理,并给出了包含上下文信息的异常类实现示例。通过Fiori文件上传案例,展示了如何在分层架构中合理传递和转换异常,最终在OData服务层将业务异常映射为标准错误响应。这种

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 3064

Laravel:服务容器

我们已经了解了依赖注入及其使用,接下来咱们一起来探索控制反转容器(IoC)。我们前面已经说过,通过 IoC 容器可以帮助我们更方便地管理类依赖,而且 Laravel 提供了一个功能强大的 IoC 容器。这个 IoC 容器在 Laravel 中被称作服务容器,是整个 Laravel 框架最核心的部分,在它的调度下,框架各个组件可以很好的组合在一起工作。实际上,Laravel 的Application...

谢谢你,慌乱了我的年华 3696

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

深入了解Laravel框架之容器技术

一、前言 在 laravel 框架中,如果问什么是设计最巧妙也是最需要掌握的内容,毫无疑问的就是服务容器。laravel 框架之所以能够具备低耦合、易扩展和可重用的优秀特性,也正是因为有了容器。如果将整个 laravel 框架比喻成一个人,那么服务容器就是相当于人的大脑,其重要性不言而喻。 下文就对 laravel 框架中的容器相关源代码进行解析,原文较长,但是我相信看完必然会有所收获。 二、源码解析 在 laravel 框架中,服务容器是通过 Illuminate\Container\Container

qq_32019789的博客 710

IMSL CNL 7.0.0 (x86-64)Crack

IMSL CNL 7.0.0 (x86-64)Crack intel的算法库破解文件,这是C++版本的破解文件

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

上一篇: ThinkPHP结合laypage实现ajax分页
下一篇: ThinkPHP 3.2.2 实现持久登录 ( 记住我 )
code301
博客等级 码龄9年 87粉丝 78原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值