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

說明:本文主要學習下\League\Flysystem這個Filesystem Abstract Layer,學習下這個package的設計思想和編碼技巧,把本身的一點點研究心得分享出來,但願對別人有幫助。實際上,這個Filesystem Abstract Layer也不是很複雜,總的來講有幾個關鍵概念:laravel

  1. Adapter:定義了一個AdapterInterface並注入到\League\Flysystem\Filesystem,利用Adapter Pattern來橋接不一樣的filesystem。如AWS S3的filesystem SDK,只要該SDK的S3 Adapter實現了AdapterInterface,就能夠做爲\League\Flysystem\Filesystem文件系統驅動之一。再好比,假設阿里雲的一個filesystem SDK名叫AliyunFilesystem SDK,想要把該SDK裝入進\League\Flysystem\Filesystem做爲驅動之一,那隻要再作一個AliyunAdapter實現AdapterInterface就行。這也是Adapter Pattern的設計巧妙的地方,固然,這種模式生活中隨處可見,不復雜,有點相似於機器人行業的模塊化組裝同樣。redis

  2. Relative Path:這個相對路徑概念就比較簡單了,就是每個文件的路徑是相對路徑,如AWS S3中若是指向一個名叫file.txt的文件路徑,能夠這麼定義Storage::disk('s3')->get('2016-09-09/daily/file.txt')就能夠了,這裏2016-09-09/daily/file.txt是相對於存儲bucket的相對路徑(bucket在AWS S3中稱爲的意思,就是能夠定義多個bucket,不一樣的bucket存各自的文件,互不干擾,在Laravel配置S3時得指定是哪一個bucket,這裏假設file.txt存儲在laravel bucket中),儘管其實際路徑爲相似這樣的:https://s3.amazonaws.com/laravel/2016-09-09/daily/file.txt。很簡單的概念。緩存

  3. File First:這個概念簡單,意思就是相對於Directory是二等公民,File是一等公民。在建立一個file時,如2016-09-09/daily/file.txt時,若是沒有2016-09-09/daily這個directory時,會自動遞歸建立。指定一個文件時,須要給出相對路徑,如2016-09-09/daily/file.txt,但不是file.txt,這個指定無心義。架構

  4. Cache:文件緩存還提升性能,但只緩存文件的meta-data,不緩存文件的內容,Cache模塊做爲一個獨立的模塊利用Decorator Pattern,把一個CacheInterface和AdapterInterface裝入進CacheAdapterInterface中,因此也能夠拆解不使用該模塊。Decorator Pattern也是Laravel中實現Middleware的一個重要技術手段,之後應該還會聊到這個技術。composer

  5. Plugin:\League\Flysystem還提供了Plugin供自定義該package中沒有的feature,\League\Flysystem\Filesystem中有一個addPlugin($plugin)方法供向\League\Flysystem\Filesystem裝入plugin,固然,\League\Flysystem中也已經提供了七八個plugin供開箱即用。Plugin的設計我的感受既合理也美妙,能夠實現須要的feature,並很簡單就能裝入,值得學習下。ide

  6. Mount Manager:Mount Manager是一個封裝類,簡化對多種filesystem的CRUD操做,無論該filesystem是remote仍是local。這個概念有點相似於這樣的東西:MAC中裝有iCloud Drive這個雲盤,把local的一個文件file.txt中複製到iCloud Drive中感受和複製到本地盤是沒有什麼區別,那用代碼來表示能夠在複製操做時給文件路徑加個"協議標識",如$mountManager->copy('local://2016-09-09/daily/file.txt', 'icloud://2016-09-09/daily/filenew.txt'),這樣就把本地磁盤的file.txt複製到icloud中,而且文件名稱指定爲2016-09-09/daily/filenew.txt。這個概念也很好理解。模塊化

1. \League\Flysystem\Filesystem源碼解析

Filesystem這個類的源碼主要就是文件的CRUD操做和文件屬性的setter/getter操做,而具體的操做是經過每個Adapter實現的,看其構造函數:函數

/**
     * Constructor.
     *
     * @param AdapterInterface $adapter
     * @param Config|array     $config
     */
    public function __construct(AdapterInterface $adapter, $config = null)
    {
        $this->adapter = $adapter;
        $this->setConfig($config);
    }
    
    /**
     * Get the Adapter.
     *
     * @return AdapterInterface adapter
     */
    public function getAdapter()
    {
        return $this->adapter;
    }

    /**
     * @inheritdoc
     */
    public function write($path, $contents, array $config = [])
    {
        $path = Util::normalizePath($path);
        $this->assertAbsent($path);
        $config = $this->prepareConfig($config);

        return (bool) $this->getAdapter()->write($path, $contents, $config);
    }

看以上代碼知道對於write($parameters)操做,其實是經過AdapterInterface的實例來實現的。因此,假設對於S3的write操做,看AwsS3Adapter的write($parameters)源碼就行,具體代碼可看這個依賴:性能

composer require league/flysystem-aws-s3-v3

因此,若是假設要在Laravel程序中使用Aliyun的filesystem,只須要幹三件事情:1. 拿到Aliyun的filesystem的PHP SDK;2. 寫一個AliyunAdapter實現\League\Flysytem\AdapterInterface;3. 在Laravel中AppServiceProvider中使用Storage::extend($name, Closure $callback)註冊一個自定義的filesystem。學習

\League\Flysystem已經提供了幾個adapter,如Local、Ftp等等,而且抽象了一個abstract class AbstractAdapter供繼承,因此AliyunAdapter只須要extends 這個AbstractAdapter就好了:

\League\Flysystem\Filesystem又是implements了FilesystemInterface,因此以爲這個Filesystem不太好能夠本身寫個替換掉它,只要實現這個FilesystemInterface就行。

2. PluggableTrait源碼解析

OK, 如今須要作一個Plugin,實現對一個文件的內容進行sha1加密,看以下代碼:

// AbstractPlugin這個抽象類league/flysystem已經提供
    use League\Flysystem\FilesystemInterface;
    use League\Flysystem\PluginInterface;

    abstract class AbstractPlugin implements PluginInterface
    {
        /**
         * @var FilesystemInterface
         */
        protected $filesystem;
    
        /**
         * Set the Filesystem object.
         *
         * @param FilesystemInterface $filesystem
         */
        public function setFilesystem(FilesystemInterface $filesystem)
        {
            $this->filesystem = $filesystem;
        }
    }
    
    // 只需繼承AbstractPlugin抽象類就行
    class Sha1File extends AbstractPlugin 
    {
        public function getMethod ()
        {
            return 'sha1File';
        }
        
        public function handle($path = null)
        {
            $contents = $this->filesystem->read($path);
            
            return sha1($contents);
        }
    }

這樣一個Plugin就已經造好了,如何使用:

use League\Flysystem\Filesystem;
use League\Flysystem\Adapter;
use League\Flysystem\Plugin;

$filesystem = new Filesystem(new Adapter\Local(__DIR__.'/path/to/file.txt'));
$filesystem->addPlugin(new Plugin\Sha1File);
$sha1 = $filesystem->sha1File('path/to/file.txt');

Plugin就是這樣製造並使用的,內部調用邏輯是怎樣的呢?實際上,Filesystem中use PluggableTrait,這個trait提供了addPlugin($parameters)方法。但$filesystem是沒有sah1File($parameters)方法的,這是怎麼工做的呢?看PluggableTrait的__call():

/**
     * Plugins pass-through.
     *
     * @param string $method
     * @param array  $arguments
     *
     * @throws BadMethodCallException
     *
     * @return mixed
     */
    public function __call($method, array $arguments)
    {
        try {
            return $this->invokePlugin($method, $arguments, $this);
        } catch (PluginNotFoundException $e) {
            throw new BadMethodCallException(
                'Call to undefined method '
                . get_class($this)
                . '::' . $method
            );
        }
    }
    /**
     * Invoke a plugin by method name.
     *
     * @param string $method
     * @param array  $arguments
     *
     * @return mixed
     */
    protected function invokePlugin($method, array $arguments, FilesystemInterface $filesystem)
    {
        $plugin = $this->findPlugin($method);
        $plugin->setFilesystem($filesystem);
        $callback = [$plugin, 'handle'];

        return call_user_func_array($callback, $arguments);
    }
    /**
     * Find a specific plugin.
     *
     * @param string $method
     *
     * @throws LogicException
     *
     * @return PluginInterface $plugin
     */
    protected function findPlugin($method)
    {
        if ( ! isset($this->plugins[$method])) {
            throw new PluginNotFoundException('Plugin not found for method: ' . $method);
        }

        if ( ! method_exists($this->plugins[$method], 'handle')) {
            throw new LogicException(get_class($this->plugins[$method]) . ' does not have a handle method.');
        }

        return $this->plugins[$method];
    }

看上面源碼發現,$sha1 = $filesystem->sha1File('path/to/file.txt')會調用invokePlugin($parameters),而後從$plugins[$method]中找有沒有名爲'sha1File'的Plugin,看addPlugin()源碼:

/**
     * Register a plugin.
     *
     * @param PluginInterface $plugin
     *
     * @return $this
     */
    public function addPlugin(PluginInterface $plugin)
    {
        $this->plugins[$plugin->getMethod()] = $plugin;

        return $this;
    }

addPlugin($parameters)就是向$plugins[$name]中註冊Plugin,這裏$filesystem->addPlugin(new PluginSha1File)就是向$plugins[$name]註冊名爲'sha1File' = (new PluginSha1File))->getMethod()的Plugin,而後return call_user_func_array([new PluginSha1File, 'handle'], $arguments),等同於調用(new PluginSha1File)->handle($arguments),因此$sha1 = $filesystem->sha1File('path/to/file.txt')就是執行(new PluginSha1File)->handle('path/to/file.txt')這段代碼。

3. MountManager源碼解析

上文已經學習了主要的幾個技術:Filesystem、Adapter和Plugin,也包括學習了它們的設計和使用,這裏看下MountManager的使用。MountManager中也use PluggableTrait並定義了__call()方法,因此在MountManager中使用Plugin和Filesystem中同樣。能夠看下MountManager的使用:

$ftp = new League\Flysystem\Filesystem($ftpAdapter);
$s3 = new League\Flysystem\Filesystem($s3Adapter);
$local = new League\Flysystem\Filesystem($localAdapter);

// Add them in the constructor
$manager = new League\Flysystem\MountManager([
    'ftp' => $ftp,
    's3' => $s3,
]);
// Or mount them later
$manager->mountFilesystem('local', $local);
// Read from FTP
$contents = $manager->read('ftp://some/file.txt');
// And write to local
$manager->write('local://put/it/here.txt', $contents);
$mountManager->copy('local://some/file.ext', 'backup://storage/location.ext');
$mountManager->move('local://some/upload.jpeg', 'cdn://users/1/profile-picture.jpeg');

上文已經說了,MountManager使得對各類filesystem的CRUD操做變得更方便了,不論是remote仍是local得。MountManager還提供了copy和move操做,只須要加上prefix,就知道被操做文件是屬於哪個filesystem。而且MountManager提供了copy和move操做,看上面代碼就像是在本地進行copy和move操做似的,毫無違和感。那read和write操做MountManager是沒有定義的,如何理解?很好理解,看__call()魔術方法:

/**
     * Call forwarder.
     *
     * @param string $method
     * @param array  $arguments
     *
     * @return mixed
     */
    public function __call($method, $arguments)
    {
        list($prefix, $arguments) = $this->filterPrefix($arguments);

        return $this->invokePluginOnFilesystem($method, $arguments, $prefix);
    }
    /**
     * Retrieve the prefix from an arguments array.
     *
     * @param array $arguments
     *
     * @return array [:prefix, :arguments]
     */
    public function filterPrefix(array $arguments)
    {
        if (empty($arguments)) {
            throw new LogicException('At least one argument needed');
        }

        $path = array_shift($arguments);

        if ( ! is_string($path)) {
            throw new InvalidArgumentException('First argument should be a string');
        }

        if ( ! preg_match('#^.+\:\/\/.*#', $path)) {
            throw new InvalidArgumentException('No prefix detected in path: ' . $path);
        }

        list($prefix, $path) = explode('://', $path, 2);
        array_unshift($arguments, $path);

        return [$prefix, $arguments];
    }
    /**
     * Invoke a plugin on a filesystem mounted on a given prefix.
     *
     * @param $method
     * @param $arguments
     * @param $prefix
     *
     * @return mixed
     */
    public function invokePluginOnFilesystem($method, $arguments, $prefix)
    {
        $filesystem = $this->getFilesystem($prefix);

        try {
            return $this->invokePlugin($method, $arguments, $filesystem);
        } catch (PluginNotFoundException $e) {
            // Let it pass, it's ok, don't panic.
        }

        $callback = [$filesystem, $method];

        return call_user_func_array($callback, $arguments);
    }
    /**
     * Get the filesystem with the corresponding prefix.
     *
     * @param string $prefix
     *
     * @throws LogicException
     *
     * @return FilesystemInterface
     */
    public function getFilesystem($prefix)
    {
        if ( ! isset($this->filesystems[$prefix])) {
            throw new LogicException('No filesystem mounted with prefix ' . $prefix);
        }

        return $this->filesystems[$prefix];
    }

仔細研究__call()魔術方法就知道,$manager->read('ftp://some/file.txt')會把$path切割成'ftp'和'some/file.txt',而後根據'ftp'找到對應的$ftp = new LeagueFlysystemFilesystem($ftpAdapter),而後先從Plugin中去invokePlugin,若是找不到Plugin就觸發PluginNotFoundException並被捕捉,說明read()方法不是Plugin中的,那就調用call_user_func_array([$filesystem, $method], $arguments),等同於調用$ftp->write('some/file.txt')。MountManager設計的也很巧妙。

4. Cache源碼解析

最後一個好的技術就是Cache模塊的設計,使用了Decorator Pattern,設計的比較巧妙,這樣只有在須要這個decorator的時候再裝載就行,就如同Laravel中的Middleware同樣。使用Cache模塊須要先裝下league/flysystem-cached-adapter這個dependency:

composer require league/flysystem-cached-adapter

看下CachedAdapter這個類的構造函數:

class CachedAdapter implements AdapterInterface
{
    /**
     * @var AdapterInterface
     */
    private $adapter;

    /**
     * @var CacheInterface
     */
    private $cache;

    /**
     * Constructor.
     *
     * @param AdapterInterface $adapter
     * @param CacheInterface   $cache
     */
    public function __construct(AdapterInterface $adapter, CacheInterface $cache)
    {
        $this->adapter = $adapter;
        $this->cache = $cache;
        $this->cache->load();
    }
}

發現它和FilesystemAdapter實現同一個AdapterInterface接口,而且在構造函數中又須要注入AdapterInterface實例和CacheInterface實例,也就是說Decorator Pattern(裝飾者模式)是這樣實現的:對於一個local filesystem的LocalAdapter(起初是沒有Cache功能的),須要給它裝扮一個Cache模塊,那須要一個裝載類CachedAdapter,該CachedAdapter類得和LocalAdapter實現共同的接口以保證裝載後仍是原來的物種(經過實現同一接口),而後把LocalAdapter裝載進去同時還得把須要裝載的裝飾器(這裏是一個Cache)同時裝載進去。這樣看來,Decorator Pattern也是一個很巧妙的設計技術,並且也不復雜。看下如何把Cache這個decorator裝載進去CachedAdapter,並最終裝入Filesystem的:

use League\Flysystem\Filesystem;
use League\Flysystem\Adapter\Local as LocalAdapter;
use League\Flysystem\Cached\CachedAdapter;
use League\Flysystem\Cached\Storage\Predis;

// Create the adapter
$localAdapter = new LocalAdapter('/path/to/root');
// And use that to create the file system without cache
$filesystemWithoutCache = new Filesystem($localAdapter);


// Decorate the adapter
$cachedAdapter = new CachedAdapter($localAdapter, new Predis);
// And use that to create the file system with cache
$filesystemWithCache = new Filesystem($cachedAdapter);

Cache模塊也一樣提供了文件的CRUD操做和文件的meta-data的setter/getter操做,但不緩存文件的內容。Cache設計的最巧妙之處仍是利用了Decorator Pattern裝載入Filesystem中使用。學會了這一點,對理解Middleware也有好處,之後再聊Middleware的設計思想。

總結:本文主要經過Laravel的Filesystem模塊學習了\League\Flysystem的源碼,並聊了該package的設計架構和設計技術,之後在使用中就可以知道它的內部流程,不至於黑箱使用。下次遇到好的技術再聊吧。

相關文章
相關標籤/搜索