Laravel5.2之Filesystem源碼解析(上)

說明:本文主要學習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模塊的總體架構以下兩張圖:php

開發環境:Laravel5.2+MAMP+PHP7+MySQL5.6架構

1. Illuminate\Filesystem\FilesystemServiceProvider

Laravel中每個Service模塊都有對應的ServiceProvider,主要幫助把該Service註冊到Container中,方便在應用程序中利用Facade調用該Service。一樣,Filesystem Service有對應的FilesystemServiceProvider,幫助註冊filesfilesystem等Service:app

// 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爲IlluminateSupportFacadesFilesystemide

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');

那這樣的代碼內部實現邏輯是怎樣的呢?this

翻一下Illuminate\Filesystem\FilesystemManager代碼就很容易知道了。首先Storage::disk()是利用了Facade模式,Storage是名爲filesystem的Facade,而filesystem從上文知道實際是FilesystemManager的對象,因此能夠看作(new FilesystemManager)->disk(),看disk()方法源碼:spa

// 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')實際上返回的是這樣一段相似代碼:code

(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)方法。orm

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的重載(Laravel5.2之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。到時見。

相關文章
相關標籤/搜索