2

说明:本文主要学习Laravel的Filesystem模块的源码逻辑,把自己的一点点研究心得分享出来,希望对别人有所帮助。总的来说,Filesystem模块的源码也比较简单,Laravel的Illuminate\Filesystem模块主要依赖于League\Flysystem这个Filesystem Abstractor Layer,类似于是League\Flysystem的Laravel Bridge。而不同的Filesystem SDK有着各自的具体增删改查逻辑,如AWS S3 SDK,Dropbox SDK,这些SDK都是通过Adapter Pattern装载入这个Filesystem Abstractor Layer。Filesystem模块的整体架构如下两张图:

图片描述

图片描述

开发环境:Laravel5.2+MAMP+PHP7+MySQL5.6

1. Illuminate\Filesystem\FilesystemServiceProvider

Laravel中每一个Service模块都有对应的ServiceProvider,主要帮助把该Service注册到Container中,方便在应用程序中利用Facade调用该Service。同样,Filesystem Service有对应的FilesystemServiceProvider,帮助注册filesfilesystem等Service:

        // Illuminate\Filesystem
        $this->app->singleton('files', function () {
            return new Filesystem;
        });
        
        $this->app->singleton('filesystem', function () {
            return new FilesystemManager($this->app);
        });

使用Container的singleton单例注册,同时还注册了filesystem.disk(config/filesystems.php的default配置选项)和filesystem.cloud(config/filesystems.php的cloud配置选项)。其中,files的Facade为IlluminateSupportFacadesFilefilesystem的Facade为IlluminateSupportFacadesFilesystem

2. Illuminate\Filesystem\FilesystemManager

Laravel官网上有类似这样代码:

// Recursively List下AWS S3上路径为dir/to的所有文件,迭代所有的文件和文件夹下的文件
$s3AllFiles = Storage::disk('s3')->allFiles('dir/to');
// Check S3 上dir/to/filesystem.png该文件是否存在
$s3AllFiles = Storage::disk('s3')->exists('dir/to/filesystem.png');

那这样的代码内部实现逻辑是怎样的呢?

翻一下Illuminate\Filesystem\FilesystemManager代码就很容易知道了。首先Storage::disk()是利用了Facade模式,Storage是名为filesystem的Facade,而filesystem从上文知道实际是FilesystemManager的对象,所以可以看做(new FilesystemManager)->disk(),看disk()方法源码:

    // Illuminate\Filesystem\FilesystemManager
    /**
     * Get a filesystem instance.
     *
     * @param  string  $name
     * @return \Illuminate\Contracts\Filesystem\Filesystem
     */
    public function disk($name = null)
    {
        // 如果不传参,就默认filesystems.default的配置
        $name = $name ?: $this->getDefaultDriver();
        // 这里传s3,$this->get('s3')取S3 driver
        return $this->disks[$name] = $this->get($name);
    }
    
    /**
     * Get the default driver name.
     *
     * @return string
     */
    public function getDefaultDriver()
    {
        return $this->app['config']['filesystems.default'];
    }
    
    /**
     * Attempt to get the disk from the local cache.
     *
     * @param  string  $name
     * @return \Illuminate\Contracts\Filesystem\Filesystem
     */
    protected function get($name)
    {
        // PHP7里可以这样简洁的写 $this->disks[$name] ?? $this->resolve($name);
        return isset($this->disks[$name]) ? $this->disks[$name] : $this->resolve($name);
    }
    
    /**
     * Resolve the given disk.
     *
     * @param  string  $name
     * @return \Illuminate\Contracts\Filesystem\Filesystem
     *
     * @throws \InvalidArgumentException
     */
    protected function resolve($name)
    {
        // 取出S3的配置
        $config = $this->getConfig($name);
        // 检查自定义驱动中是否已经提前定义了,自定义是通过extend($driver, Closure $callback)定制化driver,
        // 如果已经定义则取出定制化driver,下文介绍
        if (isset($this->customCreators[$config['driver']])) {
            return $this->callCustomCreator($config);
        }
        // 这里有个巧妙的技巧,检查Illuminate\Filesystem\FilesystemManager中是否有createS3Driver这个方法,
        // 有的话代入$config参数执行该方法,看createS3Driver()方法
        $driverMethod = 'create'.ucfirst($config['driver']).'Driver';

        if (method_exists($this, $driverMethod)) {
            return $this->{$driverMethod}($config);
        } else {
            throw new InvalidArgumentException("Driver [{$config['driver']}] is not supported.");
        }
    }
    
    /**
     * Get the filesystem connection configuration.
     *
     * @param  string  $name
     * @return array
     */
    protected function getConfig($name)
    {
        return $this->app['config']["filesystems.disks.{$name}"];
    }
    
    /**
     * Create an instance of the Amazon S3 driver.
     *
     * @param  array  $config
     * @return \Illuminate\Contracts\Filesystem\Cloud
     */
    public function createS3Driver(array $config)
    {
        $s3Config = $this->formatS3Config($config);

        $root = isset($s3Config['root']) ? $s3Config['root'] : null;

        $options = isset($config['options']) ? $config['options'] : [];

        // use League\Flysystem\AwsS3v3\AwsS3Adapter as S3Adapter,这里用了League\Flysystem\Filesystem,
        // 上文说过Laravel的Filesystem只是个Filesystem Bridge,实际上用的是League\Flysystem这个依赖。
        // League\Flysystem源码解析会在下篇中讲述,
        // 主要使用了Adapter Pattern把各个Filesystem SDK 整合到一个\League\Flysystem\FilesystemInterface实例中,
        // 有几个核心概念:Adapters, Relative Path, Files First, Plugin, MountManager(File Shortcut), Cache。
        // 下面代码类似于
        //  (new \Illuminate\Filesystem\FilesystemAdapter(
        //    new \League\Flysystem\Filesystem(
        //        new S3Adapter(new S3Client(), $options), $config)
        //    )
        //  ))
        return $this->adapt($this->createFlysystem(
            new S3Adapter(new S3Client($s3Config), $s3Config['bucket'], $root, $options), $config
        ));
    }
    
    /**
     * Create a Flysystem instance with the given adapter.
     *
     * @param  \League\Flysystem\AdapterInterface  $adapter
     * @param  array  $config
     * @return \League\Flysystem\FlysystemInterface
     */
    protected function createFlysystem(AdapterInterface $adapter, array $config)
    {
        $config = Arr::only($config, ['visibility', 'disable_asserts']);

        // use League\Flysystem\Filesystem as Flysystem
        return new Flysystem($adapter, count($config) > 0 ? $config : null);
    }
    
    /**
     * Adapt the filesystem implementation.
     *
     * @param  \League\Flysystem\FilesystemInterface  $filesystem
     * @return \Illuminate\Contracts\Filesystem\Filesystem
     */
    protected function adapt(FilesystemInterface $filesystem)
    {
        return new FilesystemAdapter($filesystem);
    }

通过代码里注释,可以看出Storage::disk('s3')实际上返回的是这样一段类似代码:

(new \Illuminate\Filesystem\FilesystemAdapter(new \League\Flysystem\Filesystem(new S3Adapter(new S3Client(), $options), $config))))

所以,Storage::disk('s3')->allFiles($parameters)或者Storage::disk('s3')->exists($parameters),实际上调用的是IlluminateFilesystemFilesystemAdapter这个对象的allFiles($parameters)和exists($parameters)方法。

3. Illuminate\Filesystem\FilesystemAdapter

查看FilesystemAdapter的源码,提供了关于filesystem的增删改查的一系列方法:

    /**
     * Determine if a file exists.
     *
     * @param  string  $path
     * @return bool
     */
    public function exists($path)
    {
        // 实际上又是调用的driver的has()方法,$driver又是\League\Flysystem\Filesystem对象,
        // 查看\League\Flysystem\Filesystem对象的has()方法,
        // 实际上是通过League\Flysystem\AwsS3v3\AwsS3Adapter的has()方法,
        // 当然最后调用的是AWS S3 SDK包的(new S3Client())->doesObjectExist($parameters)检查S3上该文件是否存在
        return $this->driver->has($path);
    }
    
    /**
     * Get all of the files from the given directory (recursive).
     *
     * @param  string|null  $directory
     * @return array
     */
    public function allFiles($directory = null)
    {
        return $this->files($directory, true);
    }
    
    /**
     * Get an array of all files in a directory.
     *
     * @param  string|null  $directory
     * @param  bool  $recursive
     * @return array
     */
    public function files($directory = null, $recursive = false)
    {
        $contents = $this->driver->listContents($directory, $recursive);

        return $this->filterContentsByType($contents, 'file');
    }
    
    /**
     * Pass dynamic methods call onto Flysystem.
     *
     * @param  string  $method
     * @param  array  $parameters
     * @return mixed
     *
     * @throws \BadMethodCallException
     */
    public function __call($method, array $parameters)
    {
        return call_user_func_array([$this->driver, $method], $parameters);
    }

通过代码注释知道,Storage::disk('s3')->exists($parameters)实际上最后通过调用S3 SDK的(new S3Client())->doesObjectExist($parameters)检查S3上有没有该文件,Storage::disk('s3')->allFiles($parameters)也是同理,通过调用(new League\Flysystem\AwsS3v3\AwsS3Adapter(new S3Client(), $config))->listContents()来list contents of a dir.

根据上文的解释,那Storage::disk('s3')->readStream($path)可以调用不?
可以的。实际上,\Illuminate\Filesystem\FilesystemAdapter使用了PHP的重载(Laravel学习笔记之PHP重载(overloading)),通过__call($method, $parameters)魔术方法调用$driver里的$method,而这个$driver实际上就是(new \League\Flysystem\Filesystem),该Filesystem Abstract Layer中有readStream方法,可以调用。

Laravelgu官网中介绍通过Storage::extend($driver, Closure $callback)来自定义driver,这里我们知道实际上调用的是(new \Illuminate\Filesystem\FilesystemManager($parameters))->extend($driver, Closure $callback),上文中提到该对象的resolve($name)代码时会先检查自定义驱动有没有,有的话调用自定义驱动,再看下resolve()代码:

    /**
     * Resolve the given disk.
     *
     * @param  string  $name
     * @return \Illuminate\Contracts\Filesystem\Filesystem
     *
     * @throws \InvalidArgumentException
     */
    protected function resolve($name)
    {
        $config = $this->getConfig($name);

        // 检查自动以驱动是否存在,存在的话,调用callCustomCreator来解析出$driver
        if (isset($this->customCreators[$config['driver']])) {
            return $this->callCustomCreator($config);
        }

        $driverMethod = 'create'.ucfirst($config['driver']).'Driver';

        if (method_exists($this, $driverMethod)) {
            return $this->{$driverMethod}($config);
        } else {
            throw new InvalidArgumentException("Driver [{$config['driver']}] is not supported.");
        }
    }    
   
   /**
     * Call a custom driver creator.
     *
     * @param  array  $config
     * @return \Illuminate\Contracts\Filesystem\Filesystem
     */
    protected function callCustomCreator(array $config)
    {
        $driver = $this->customCreators[$config['driver']]($this->app, $config);

        if ($driver instanceof FilesystemInterface) {
            return $this->adapt($driver);
        }

        return $driver;
    }     

extend()方法就是把自定义的驱动注册进$customCreators里:

    /**
     * Register a custom driver creator Closure.
     *
     * @param  string    $driver
     * @param  \Closure  $callback
     * @return $this
     */
    public function extend($driver, Closure $callback)
    {
        $this->customCreators[$driver] = $callback;

        return $this;
    }

总结:上篇主要讲述了Laravel Filesystem Bridge,该Bridge只是把League/Flysystem这个package简单做了桥接和封装,便于在Laravel中使用。明天再写下篇,主要学习下League/Flysystem这个package的源码,League/Flysystem作为一个Filesystem Abstractor Layer,利用了Adapter Pattern来封装各个filesystem的SDK,如AWS S3 SDK或Dropbox SDK。到时见。

欢迎关注Laravel-China

RightCapital招聘Laravel DevOps


lx1036
3.1k 声望923 粉丝

为五斗米折腰