Laravel5.2之Filesystem源码解析(下)
2019独角兽企业重金招聘Python工程师标准>>>
说明:本文主要学习下\League\Flysystem这个Filesystem Abstract Layer,学习下这个package的设计思想和编码技巧,把自己的一点点研究心得分享出来,希望对别人有帮助。实际上,这个Filesystem Abstract Layer也不是很复杂,总的来说有几个关键概念:
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的设计巧妙的地方,当然,这种模式生活中随处可见,不复杂,有点类似于机器人行业的模块化组装一样。
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
。很简单的概念。File First:这个概念简单,意思就是相对于Directory是二等公民,File是一等公民。在创建一个file时,如
2016-09-09/daily/file.txt
时,如果没有2016-09-09/daily这个directory时,会自动递归创建。指定一个文件时,需要给出相对路径,如2016-09-09/daily/file.txt,但不是file.txt,这个指定无意义。Cache:文件缓存还提高性能,但只缓存文件的meta-data,不缓存文件的内容,Cache模块作为一个独立的模块利用Decorator Pattern,把一个CacheInterface和AdapterInterface装入进CacheAdapterInterface中,所以也可以拆解不使用该模块。Decorator Pattern也是Laravel中实现Middleware的一个重要技术手段,以后应该还会聊到这个技术。
Plugin:\League\Flysystem还提供了Plugin供自定义该package中没有的feature,\League\Flysystem\Filesystem中有一个addPlugin($plugin)方法供向\League\Flysystem\Filesystem装入plugin,当然,\League\Flysystem中也已经提供了七八个plugin供开箱即用。Plugin的设计个人感觉既合理也美妙,可以实现需要的feature,并很简单就能装入,值得学习下。
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源码解析(下)相关推荐
- spring 源码深度解析_spring源码解析之SpringIOC源码解析(下)
前言:本篇文章接SpringIOC源码解析(上),上一篇文章介绍了使用XML的方式启动Spring,介绍了refresh 方法中的一些方法基本作用,但是并没有展开具体分析.今天就和大家一起撸一下ref ...
- Laravel 学习笔记之 Query Builder 源码解析(下)
说明:本文主要学习下Query Builder编译Fluent Api为SQL的细节和执行SQL的过程.实际上,上一篇聊到了\Illuminate\Database\Query\Builder这个非常 ...
- Laravel 学习笔记5.3之 Query Builder 源码解析(下)
2019独角兽企业重金招聘Python工程师标准>>> 说明:本文主要学习下Query Builder编译Fluent Api为SQL的细节和执行SQL的过程.实际上,上一篇聊到了\ ...
- Android之EventBus框架源码解析下(源码解析)
转载请标明出处:[顾林海的博客] 个人开发的微信小程序,目前功能是书籍推荐,后续会完善一些新功能,希望大家多多支持! 前言 EventBus是典型的发布订阅模式,多个订阅者可以订阅某个事件,发布者通过 ...
- prometheus变量_TiKV 源码解析系列文章(四)Prometheus(下)
本文为 TiKV 源码解析系列的第四篇,接上篇继续为大家介绍 rust-prometheus.上篇主要介绍了基础知识以及最基本的几个指标的内部工作机制,本篇会进一步介绍更多高级功能的实现原理. 与上篇 ...
- Retrofit2源码解析——网络调用流程(下)
Retrofit2源码解析系列 Retrofit2源码解析(一) Retrofit2源码解析--网络调用流程(上) 本文基于Retrofit2的2.4.0版本 implementation 'com. ...
- erlang下lists模块sort(排序)方法源码解析(二)
上接erlang下lists模块sort(排序)方法源码解析(一),到目前为止,list列表已经被分割成N个列表,而且每个列表的元素是有序的(从大到小) 下面我们重点来看看mergel和rmergel ...
- 面试官系统精讲Java源码及大厂真题 - 31 AbstractQueuedSynchronizer 源码解析(下)
31 AbstractQueuedSynchronizer 源码解析(下) 低头要有勇气,抬头要有底气. 引导语 AQS 的内容太多,所以我们分成了两个章节,没有看过 AQS 上半章节的同学可以回首看 ...
- 关于 Android 中 TabLayout 下划线适配文字长度解析(附清晰详细的源码解析)
温故而知新 坚持原创 请多多支持 一.问题背景 假期在做项目的时候,当时遇到了一个需求就是需要使用 TabLayout + ViewPager 来实现一个上部导航栏的动态效果,并且希望下划线的长度等于 ...
最新文章
- nginx如何解决超长请求串
- 为什么很多SpringBoot开发者放弃了Tomcat,选择了Undertow?
- Javascript元编程创建DOM节点
- 解决activiti中由模板转换的流程图连线名称缺失问题
- 特斯拉是l3还是l2_比特斯拉还“高一级”,长安的“L3级自动驾驶”到底什么来头?...
- Linux监控命令之 top
- 【Swift】iOS UICollectionView 计算 Cell 大小的陷阱
- 被知乎反杀,是一种什么体验?
- bzoj 相似回文串 3350 3103 弦图染色+manacher
- 小程序 做二维码带logo绘制二维码
- 用ruby编写标准计算器_WatirMaker再次用Ruby编写
- Mac配置环境变量的位置
- 计算机内存的存储单位换算,电脑内存换算(电脑内存单位及换算)
- flink的window,时间语义,watermark,状态编程,容错机制checkpoint
- DEDE源码分析与学习---index.php文件解读。
- flea-db使用之基于对象池的FleaJPAQuery
- 营业执照注册号是不是统一社会信用代码?
- android错误之解析包时出现问题(一)
- 苹果6s速度有多快?
- 洛谷 P4883 mzf的考验 解题报告