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