說明:本文主要學習下\League\Flysystem這個Filesystem Abstract Layer,學習下這個package的設計思想和編碼技巧,把本身的一點點研究心得分享出來,但願對別人有幫助。實際上,這個Filesystem Abstract Layer也不是很複雜,總的來講有幾個關鍵概念:laravel
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
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的一個重要技術手段,之後應該還會聊到這個技術。composer
Plugin:\League\Flysystem還提供了Plugin供自定義該package中沒有的feature,\League\Flysystem\Filesystem中有一個addPlugin($plugin)方法供向\League\Flysystem\Filesystem裝入plugin,固然,\League\Flysystem中也已經提供了七八個plugin供開箱即用。Plugin的設計我的感受既合理也美妙,能夠實現須要的feature,並很簡單就能裝入,值得學習下。ide
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。這個概念也很好理解。模塊化
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就行。
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')這段代碼。
上文已經學習了主要的幾個技術: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設計的也很巧妙。
最後一個好的技術就是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的設計架構和設計技術,之後在使用中就可以知道它的內部流程,不至於黑箱使用。下次遇到好的技術再聊吧。