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 的管理与使用)

你可能感兴趣的:(Laravel 源码分析---ServiceProvider)