2019独角兽企业重金招聘Python工程师标准>>>

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

转载于:https://my.oschina.net/botkenni/blog/775317

Laravel5.2之Filesystem源码解析(下)相关推荐

  1. spring 源码深度解析_spring源码解析之SpringIOC源码解析(下)

    前言:本篇文章接SpringIOC源码解析(上),上一篇文章介绍了使用XML的方式启动Spring,介绍了refresh 方法中的一些方法基本作用,但是并没有展开具体分析.今天就和大家一起撸一下ref ...

  2. Laravel 学习笔记之 Query Builder 源码解析(下)

    说明:本文主要学习下Query Builder编译Fluent Api为SQL的细节和执行SQL的过程.实际上,上一篇聊到了\Illuminate\Database\Query\Builder这个非常 ...

  3. Laravel 学习笔记5.3之 Query Builder 源码解析(下)

    2019独角兽企业重金招聘Python工程师标准>>> 说明:本文主要学习下Query Builder编译Fluent Api为SQL的细节和执行SQL的过程.实际上,上一篇聊到了\ ...

  4. Android之EventBus框架源码解析下(源码解析)

    转载请标明出处:[顾林海的博客] 个人开发的微信小程序,目前功能是书籍推荐,后续会完善一些新功能,希望大家多多支持! 前言 EventBus是典型的发布订阅模式,多个订阅者可以订阅某个事件,发布者通过 ...

  5. prometheus变量_TiKV 源码解析系列文章(四)Prometheus(下)

    本文为 TiKV 源码解析系列的第四篇,接上篇继续为大家介绍 rust-prometheus.上篇主要介绍了基础知识以及最基本的几个指标的内部工作机制,本篇会进一步介绍更多高级功能的实现原理. 与上篇 ...

  6. Retrofit2源码解析——网络调用流程(下)

    Retrofit2源码解析系列 Retrofit2源码解析(一) Retrofit2源码解析--网络调用流程(上) 本文基于Retrofit2的2.4.0版本 implementation 'com. ...

  7. erlang下lists模块sort(排序)方法源码解析(二)

    上接erlang下lists模块sort(排序)方法源码解析(一),到目前为止,list列表已经被分割成N个列表,而且每个列表的元素是有序的(从大到小) 下面我们重点来看看mergel和rmergel ...

  8. 面试官系统精讲Java源码及大厂真题 - 31 AbstractQueuedSynchronizer 源码解析(下)

    31 AbstractQueuedSynchronizer 源码解析(下) 低头要有勇气,抬头要有底气. 引导语 AQS 的内容太多,所以我们分成了两个章节,没有看过 AQS 上半章节的同学可以回首看 ...

  9. 关于 Android 中 TabLayout 下划线适配文字长度解析(附清晰详细的源码解析)

    温故而知新 坚持原创 请多多支持 一.问题背景 假期在做项目的时候,当时遇到了一个需求就是需要使用 TabLayout + ViewPager 来实现一个上部导航栏的动态效果,并且希望下划线的长度等于 ...

最新文章

  1. nginx如何解决超长请求串
  2. 为什么很多SpringBoot开发者放弃了Tomcat,选择了Undertow?
  3. Javascript元编程创建DOM节点
  4. 解决activiti中由模板转换的流程图连线名称缺失问题
  5. 特斯拉是l3还是l2_比特斯拉还“高一级”,长安的“L3级自动驾驶”到底什么来头?...
  6. Linux监控命令之 top
  7. 【Swift】iOS UICollectionView 计算 Cell 大小的陷阱
  8. 被知乎反杀,是一种什么体验?
  9. bzoj 相似回文串 3350 3103 弦图染色+manacher
  10. 小程序 做二维码带logo绘制二维码
  11. 用ruby编写标准计算器_WatirMaker再次用Ruby编写
  12. Mac配置环境变量的位置
  13. 计算机内存的存储单位换算,电脑内存换算(电脑内存单位及换算)
  14. flink的window,时间语义,watermark,状态编程,容错机制checkpoint
  15. DEDE源码分析与学习---index.php文件解读。
  16. flea-db使用之基于对象池的FleaJPAQuery
  17. 营业执照注册号是不是统一社会信用代码?
  18. android错误之解析包时出现问题(一)
  19. 苹果6s速度有多快?
  20. 洛谷 P4883 mzf的考验 解题报告

热门文章

  1. 如何备份思科、锐捷、Juniper的配置文件
  2. SQL高级查询(层次化查询,递归)
  3. 用jenkins搭建android自动打包环境
  4. Spark LogisticRegression 逻辑回归之建模
  5. VMware中安装CentOS7网络配置静态IP地址,常用配置和工具安装
  6. 并行口设计数字键盘实验
  7. BIEE物理业务层编辑之后发布路径
  8. 关于node.js的进程管理
  9. C#提取TXT文档指定内容
  10. java常用类型转换