2

说明:本文主要学习下\League\Flysystem这个Filesystem Abstract Layer,学习下这个package的设计思想和编码技巧,把自己的一点点研究心得分享出来,希望对别人有帮助。实际上,这个Filesystem Abstract Layer也不是很复杂,总的来说有几个关键概念:

  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的设计巧妙的地方,当然,这种模式生活中随处可见,不复杂,有点类似于机器人行业的模块化组装一样。

  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的一个重要技术手段,以后应该还会聊到这个技术。

  5. Plugin:\League\Flysystem还提供了Plugin供自定义该package中没有的feature,\League\Flysystem\Filesystem中有一个addPlugin($plugin)方法供向\League\Flysystem\Filesystem装入plugin,当然,\League\Flysystem中也已经提供了七八个plugin供开箱即用。Plugin的设计个人感觉既合理也美妙,可以实现需要的feature,并很简单就能装入,值得学习下。

  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的设计架构和设计技术,以后在使用中就能够知道它的内部流程,不至于黑箱使用。下次遇到好的技术再聊吧。

欢迎关注Laravel-China

RightCapital招聘Laravel DevOps


lx1036
3.1k 声望923 粉丝

为五斗米折腰