Laravel 源码分析---ServiceProvider

标签: laravel 源码分析 ServiceProvider


ServiceProvider 是 laravel 框架中很重要的一个概念,理解 ServiceProvider 的在框架中的作用并阅读其源码对于我们理解框架的设计思想和用好框架很有作用。今天我们就来看一下 ServiceProvider 的功能及其源码。

ServiceProvider 功能概述

在框架中 ServiceProvider 扮演着沟通 laravel 框架核心和独立模块桥梁的作用。laravel 框架大部分的核心模块、第三方模块、自己开发的业务模块都通过 ServiceProvider 整合进框架。在 app 配置文件中配置着所有整合进框架的模块,如下代码所示:

'providers' => [

        /*
         * Laravel Framework Service Providers...
         */
        Illuminate\Auth\AuthServiceProvider::class,
        Illuminate\Broadcasting\BroadcastServiceProvider::class,
        Illuminate\Bus\BusServiceProvider::class,
        ...
        Illuminate\View\ViewServiceProvider::class,

        /*
         * Application Service Providers...
         */
        App\Providers\AppServiceProvider::class,
        App\Providers\AuthServiceProvider::class,
        App\Providers\EventServiceProvider::class,
        App\Providers\RouteServiceProvider::class,
    ],

我们看到,框架的视图(VIEW)、数据库(DB)、缓存(Cache)等我们用到的大部分功能都通过 ServiceProvider 将其对应的模块注册到框架。一般一个 ServiceProvider 文件只注册其相应的模块信息。
ServiceProvider 是如何将独立模块整合进框架呢,或者说 ServiceProvider 提供了什么样的接口供用户整和独立模块进框架呢?首先 laravel 框架提供了一个抽象类 Illuminate\Support\ServiceProvider 作为所有 ServiceProvider 的父类,其中有一个 register 抽象方法,由用户在其子类中实现这个方法,在其中进行第三方模块的注册。并且可在子类中实现 boot 方法,标识模块在框架启动过程中需要进行的操作。
总体来说,我们在 ServiceProvider中,主要通过进行如下操作来注册独立模块:

  1. 注册独立模块的核心类到框架容器(在register方法中实现)
  2. 添加独立模块的配置、视图、翻译数据到框架的相应模块(在boot方法中实现)
  3. 添加独立模块要发布到项目的资源的数据(在boot方法中实现)

ServiceProvider 信息配置好之后,框架中 ServiceProvider 类的实例化以及相应方法的调用都由框架中的核心类 Application 来管理。

ServiceProvider 源码分析

ServiceProvider 类结构

我们先来看一下 ServiceProvider 类的主要结构

namespace Illuminate\Support;

abstract class ServiceProvider
{
    /**
     * The application instance.
     * 
     * @var \Illuminate\Contracts\Foundation\Application
     */
    protected $app;

    /**
     * Indicates if loading of the provider is deferred.
     * 标注是否延迟注册模块。如果非延迟注册模块,则无论模块在请求中是否用到,都会在框架初始化的时候注册和启动模块。否则模块只会在请求中模块使用的时候自动注册并启动
     * @var bool
     */
    protected $defer = false;

    /**
     * The paths that should be published.
     * 这个模块要向项目中发布的资源文件的位置。通过静态方法,统一管理所有的子类相关的数据
     * @var array
     */
    protected static $publishes = [];

    /**
     * The paths that should be published by group.
     * 对 ServiceProvider 中要发布的资源文件的分组。通过静态方法,统一管理所有的子类相关的数据
     * @var array
     */
    protected static $publishGroups = [];

    /**
     * Register the service provider.
     * 子类实现这个方法,注册模块的代码在此方法中实现
     * @return void
     */
    abstract public function register();

    /**
     * Merge the given configuration with the existing configuration.
     * 将注册模块需要的配置文件合并入框架配置系统
     * @param  string  $path
     * @param  string  $key
     * @return void
     */
    protected function mergeConfigFrom($path, $key){}

    /**
     * Register a view file namespace.
     * 注册模块命名空间和对应的视图路径到框架视图系统
     * @param  string  $path
     * @param  string  $namespace
     * @return void
     */
    protected function loadViewsFrom($path, $namespace){}

    /**
     * Register a translation file namespace.
     * 注册模块命名空间到框架翻译系统
     * @param  string  $path
     * @param  string  $namespace
     * @return void
     */
    protected function loadTranslationsFrom($path, $namespace){}

    /**
     * Register paths to be published by the publish command.
     * 注册模块要发布到项目中的的资源路径
     * @param  array  $paths
     * @param  string  $group
     * @return void
     */
    protected function publishes(array $paths, $group = null){}

    /**
     * Get the paths to publish.
     * 根据 ServiceProvider 和分组返回发布的资源路径
     * @param  string  $provider
     * @param  string  $group
     * @return array
     */
    public static function pathsToPublish($provider = null, $group = null)
    {}


    /**
     * Get the services provided by the provider.
     * 返回注册到框架中的模块的类
     * @return array
     */
    public function provides(){}

    /**
     * Get the events that trigger this service provider to register.
     * 返回能够触发注册此模块的事件
     * @return array
     */
    public function when(){}
}

添加模块视图、配置、翻译信息源码分析

接下来我们来看添加模块视图、配置、翻译信息的源码

namespace Illuminate\Support;

abstract class ServiceProvider
{
    /**
     * Merge the given configuration with the existing configuration.
     * 将注册模块需要的配置文件合并入框架配置系统
     * @param  string  $path
     * @param  string  $key
     * @return void
     */
    protected function mergeConfigFrom($path, $key)
    {
        $config = $this->app['config']->get($key, []);

        $this->app['config']->set($key, array_merge(require $path, $config));
    }

    /**
     * Register a view file namespace.
     * 注册模块命名空间和对应的视图路径到框架视图系统。我们看到在视图系统中,一个命名空间可以对应多个路径,程序会对路径进行扩展,在传入的命名空间下添加两个路径
     * @param  string  $path
     * @param  string  $namespace
     * @return void
     */
    protected function loadViewsFrom($path, $namespace)
    {
        if (is_dir($appPath = $this->app->basePath().'/resources/views/vendor/'.$namespace)) {
            $this->app['view']->addNamespace($namespace, $appPath);
        }

        $this->app['view']->addNamespace($namespace, $path);
    }

    /**
     * Register a translation file namespace.
     * 注册模块命名空间到框架翻译系统
     * @param  string  $path
     * @param  string  $namespace
     * @return void
     */
    protected function loadTranslationsFrom($path, $namespace)
    {
        $this->app['translator']->addNamespace($namespace, $path);
    }
}

模块资源发布管理相关源码分析

接下来我们看和模块资源发布管理相关的源码

namespace Illuminate\Support;

abstract class ServiceProvider
{
    /**
     * Register paths to be published by the publish command.
     * 注册模块要发布到项目中的的资源路径。实际就是根据子类的名字和传进来的分组参数设置静态属性 static::$publishes 和 static::$publishGroups
     * @param  array  $paths
     * @param  string  $group
     * @return void
     */
    protected function publishes(array $paths, $group = null)
    {
        $class = static::class;

        if (! array_key_exists($class, static::$publishes)) {
            static::$publishes[$class] = [];
        }

        static::$publishes[$class] = array_merge(static::$publishes[$class], $paths);

        if ($group) {
            if (! array_key_exists($group, static::$publishGroups)) {
                static::$publishGroups[$group] = [];
            }

            static::$publishGroups[$group] = array_merge(static::$publishGroups[$group], $paths);
        }
    }

    /**
     * Get the paths to publish.
     * 根据 ServiceProvider 和分组返回发布的资源路径
     * @param  string  $provider
     * @param  string  $group
     * @return array
     */
    public static function pathsToPublish($provider = null, $group = null)
    {
        if ($provider && $group) {
            if (empty(static::$publishes[$provider]) || empty(static::$publishGroups[$group])) {
                return [];
            }

            return array_intersect_key(static::$publishes[$provider], static::$publishGroups[$group]);
        }

        if ($group && array_key_exists($group, static::$publishGroups)) {
            return static::$publishGroups[$group];
        }

        if ($provider && array_key_exists($provider, static::$publishes)) {
            return static::$publishes[$provider];
        }

        if ($group || $provider) {
            return [];
        }

        $paths = [];

        foreach (static::$publishes as $class => $publish) {
            $paths = array_merge($paths, $publish);
        }

        return $paths;
    }
}

模块延迟加载相关源码分析

最后我们来看一下和模块延迟加载相关的源码

namespace Illuminate\Support;

abstract class ServiceProvider
{
 /**
     * Get the services provided by the provider.
     * 返回注册到框架中的模块的类。如果设置模块是异步加载的话,框架容器调用其中其中的类的时候,运行 register 方法。
     * @return array
     */
    public function provides()
    {
        return [];
    }

    /**
     * Get the events that trigger this service provider to register.
     * 返回能够触发注册此模块的事件。如果设置模块是异步加载的话,当这些事件触发的时候,框架调用 register 方法注册模块
     * @return array
     */
    public function when()
    {
        return [];
    }
}

通过上面的源码我们可以知道,ServiceProvider 主要提供了 mergeConfigFromloadViewsFromloadTranslationsFrom 三个方法提供了将注册模块的配置、视图、翻译等信息添加到框架里面;提供了 publishes 方法标识模块要发布资源文件的路径,通过命令行可以发布资源到项目的对应位置;提供了 provideswhen 方法标识了对于延迟加载的模块的加载时机。
比如通过以下方法添加模块的视图信息:

/**
 * Perform post-registration booting of services.
 *
 * @return void
 */
public function boot(){
    $this->loadViewsFrom(__DIR__.'/path/to/views', 'courier');
}

则我们就可以通过下面方法调用模块里面的视图

Route::get('admin', function () {
    return view('courier::admin');
});

View 模块相关源码分析

通过上面的代码分析我们可以看到,ServiceProvider 可以向框架的视图系统添加命令空间和视图路径的对应关系,我们来一下视图系统的相关代码,看一下相应功能是如何实现的。

框架视图系统位于命名空间 Illuminate\View 下,添加命名空间和路径对应的功能主要通过 Illuminate\View\FileViewFinder 类实现和应用。FileViewFinder 类的主要作用就是根据视图的命名找到其对应的文件的路径,主要有两种查找方式,一种在在配置的默认路径下查找,一种是在命名空间下的路径查找。接下来我们来看相关代码的实现。

FileViewFinder 类结构

我们先来看一下类的具体结构

namespace Illuminate\View;

use InvalidArgumentException;
use Illuminate\Filesystem\Filesystem;

class FileViewFinder implements ViewFinderInterface
{
    /**
     * Hint path delimiter value.
     * 模块命名空间(包名)与其下视图路径之间的分隔符
     * @var string
     */
    const HINT_PATH_DELIMITER = '::';
    
    /**
     * The array of active view paths.
     * 储存视图文件的路径。在寻找视图文件时,代码会遍历数组中的所有路径查找视图文件,当文件存在时,则返回其完整路径
     * @var array
     */
    protected $paths;

    /**
     * The array of views that have been located.
     * 储存的已经定位过的视图位置
     * @var array
     */
    protected $views = [];

    /**
     * The namespace to file path hints.
     * 命名空间(包名)与其对应的视图路径。
     * @var array
     */
    protected $hints = [];

    /**
     * Register a view extension with the finder.
     * 视图文件支持的扩展名
     * @var array
     */
    protected $extensions = ['blade.php', 'php'];

    /**
     * Get the fully qualified location of the view.
     * 根据视图的名字,返回视图文件的完整路径
     * @param  string  $name
     * @return string
     */
    public function find($name){}

    /**
     * Get the path to a template with a named path.
     * 在命名空间下查找视图完整路径
     * @param  string  $name
     * @return string
     */
    protected function findNamedPathView($name){}

    /**
     * Find the given view in the list of paths.
     * 在给定的一组路径下查找视图
     * @param  string  $name
     * @param  array   $paths
     * @return string
     *
     * @throws \InvalidArgumentException
     */
    protected function findInPaths($name, $paths){}

    /**
     * Add a location to the finder.
     * 后面追加一个视图的搜索路径
     * @param  string  $location
     * @return void
     */
    public function addLocation($location){}

    /**
     * Prepend a location to the finder.
     * 前面添加一个视图的搜索路径
     * @param  string  $location
     * @return void
     */
    public function prependLocation($location){}

    /**
     * Add a namespace hint to the finder.
     * 追加一个命令空间和路径的对应关系。一个命名空间可以对应多个路径,新添加的在后面
     * @param  string  $namespace
     * @param  string|array  $hints
     * @return void
     */
    public function addNamespace($namespace, $hints){}

    /**
     * Prepend a namespace hint to the finder.
     * 添加一个命令空间和路径的对应关系。一个命名空间可以对应多个路径,新添加的在放在前面
     * @param  string  $namespace
     * @param  string|array  $hints
     * @return void
     */
    public function prependNamespace($namespace, $hints){}
}

视图命名空间管理相关代码

我们来看和视图命名空间管理相关的代码:

class FileViewFinder implements ViewFinderInterface
{
 /**
     * Add a namespace hint to the finder.
     * 追加一个命令空间和路径的对应关系。一个命名空间可以对应多个路径,新添加的在后面
     * @param  string  $namespace
     * @param  string|array  $hints
     * @return void
     */
    public function addNamespace($namespace, $hints)
    {
        $hints = (array) $hints;

        if (isset($this->hints[$namespace])) {
            $hints = array_merge($this->hints[$namespace], $hints);
        }

        $this->hints[$namespace] = $hints;
    }

    /**
     * Prepend a namespace hint to the finder.
     * 添加一个命令空间和路径的对应关系。一个命名空间可以对应多个路径,新添加的在放在前面
     * @param  string  $namespace
     * @param  string|array  $hints
     * @return void
     */
    public function prependNamespace($namespace, $hints)
    {
        $hints = (array) $hints;

        if (isset($this->hints[$namespace])) {
            $hints = array_merge($hints, $this->hints[$namespace]);
        }

        $this->hints[$namespace] = $hints;
    }
}

视图默认路径管理相关代码

我们来看和视图路径管理相关的代码:

class FileViewFinder implements ViewFinderInterface
{
 /**
     * Add a location to the finder.
     * 后面追加一个视图的搜索路径
     * @param  string  $location
     * @return void
     */
    public function addLocation($location)
    {
        $this->paths[] = $location;
    }

    /**
     * Prepend a location to the finder.
     * 前面添加一个视图的搜索路径
     * @param  string  $location
     * @return void
     */
    public function prependLocation($location)
    {
        array_unshift($this->paths, $location);
    }
}

视图路径查找代码

最后我们来看,代码是如何根据视图名称找到视图路径的。

class FileViewFinder implements ViewFinderInterface
{
    /**
     * Get the fully qualified location of the view.
     * 根据视图的名字,返回视图文件的完整路径
     * @param  string  $name
     * @return string
     */
    public function find($name)
    {    
        if (isset($this->views[$name])) {
            return $this->views[$name];
        }
        //如果视图名字里面有命名空间和视图路径之间的分隔符,则在命名空间的路径下查找视图
        if ($this->hasHintInformation($name = trim($name))) { 
            return $this->views[$name] = $this->findNamedPathView($name);
        }
        
        //在 $this->paths 变量定义的路径下查找视图路径。$this->paths 变量在类实例化的时候会初始化为配饰文件 view.paths 定义的值。
        return $this->views[$name] = $this->findInPaths($name, $this->paths);
    }

    /**
     * Get the path to a template with a named path.
     * 在命名空间下查找视图完整路径
     * @param  string  $name
     * @return string
     */
    protected function findNamedPathView($name)
    {
        //根据视图名字,分割出命名空间和视图路径
        list($namespace, $view) = $this->getNamespaceSegments($name);
        
        //在命名空间对应的路径下查找视图
        return $this->findInPaths($view, $this->hints[$namespace]);
    }

    /**
     * Get the segments of a template with a named path.
     *
     * @param  string  $name
     * @return array
     *
     * @throws \InvalidArgumentException
     */
    protected function getNamespaceSegments($name)
    {
        $segments = explode(static::HINT_PATH_DELIMITER, $name);

        if (count($segments) != 2) {
            throw new InvalidArgumentException("View [$name] has an invalid name.");
        }

        if (! isset($this->hints[$segments[0]])) {
            throw new InvalidArgumentException("No hint path defined for [{$segments[0]}].");
        }

        return $segments;
    }

    /**
     * Find the given view in the list of paths.
     * 在给定的一组路径下查找视图
     * @param  string  $name
     * @param  array   $paths
     * @return string
     *
     * @throws \InvalidArgumentException
     */
    protected function findInPaths($name, $paths)
    {
        foreach ((array) $paths as $path) {
            foreach ($this->getPossibleViewFiles($name) as $file) {
                if ($this->files->exists($viewPath = $path.'/'.$file)) {
                    return $viewPath;
                }
            }
        }

        throw new InvalidArgumentException("View [$name] not found.");
    }

    /**
     * Get an array of possible view files.
     * 根据文件扩展名返回所有可能的视图路径名称
     * @param  string  $name
     * @return array
     */
    protected function getPossibleViewFiles($name)
    {
        return array_map(function ($extension) use ($name) {
            return str_replace('.', '/', $name).'.'.$extension;
        }, $this->extensions);
    }
    
    /**
     * Returns whether or not the view name has any hint information.
     * 返回字符串里面是否有命名空间与视图名的分割符
     * @param  string  $name
     * @return bool
     */
    public function hasHintInformation($name)
    {
        return strpos($name, static::HINT_PATH_DELIMITER) > 0;
    }
}

至此我们知道了视图系统如何根据视图名称查找对应的视图文件。通过其提供的 addNamespace 方法,ServiceProvider 可以向视图系统中注册命名空间(包名)和路径的对应关系,在我们创建视图对象的时候,可以非常方便的引用到某个命名空间(包)下的视图。

总结

至此,我们看了 ServiceProvider 抽象类的相关源码,了解了其在框架中的作用,以及实现这些功能提供的方法。ServiceProvider 是框架非常重要的概念和模块之一,其对我们了解框架原理和在框架上开发功能都非常重要。
接下来我们来看 Application 对 ServiceProvider 的管理与使用(见文章Laravel 源码分析---Application 对 ServiceProvider 的管理与使用

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,067评论 25 707
  • 原文链接 必备品 文档:Documentation API:API Reference 视频:Laracasts ...
    layjoy阅读 8,607评论 0 121
  • 有人说 看到这幅画想哭 我哭着告诉ta 放心 几百年后 ta们会在同一个星球相遇的 ta们说 你好残忍 是啊 我是...
    一凡SU阅读 122评论 2 1
  • 三月不减肥,六月徒伤悲!为什么从三月开始减肥呢?时间!减肥是没有捷径可走的,健身的效果是按月呈现的。有的人每天...
    健身顾问陈晨阅读 281评论 0 0
  • 十字街口 行人匆匆 在眼前交叉穿行 你在那里 找不到方向了 我张大嘴 喊不出声 夜色摁着咽喉 你走时 风在树梢上鸣...
    春笺素心阅读 114评论 0 1