资讯专栏INFORMATION COLUMN

Laravel学习笔记之Filesystem源码解析(下)

Luosunce / 389人阅读

摘要:源码解析这个类的源码主要就是文件的操作和文件属性的操作,而具体的操作是通过每一个实现的,看其构造函数看以上代码知道对于操作,实际上是通过的实例来实现的。可以看下的使用上文已经说了,使得对各种的操作变得更方便了,不管是还是得。

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

Adapter:定义了一个AdapterInterface并注入到LeagueFlysystemFilesystem,利用Adapter Pattern来桥接不同的filesystem。如AWS S3的filesystem SDK,只要该SDK的S3 Adapter实现了AdapterInterface,就可以作为LeagueFlysystemFilesystem文件系统驱动之一。再比如,假设阿里云的一个filesystem SDK名叫AliyunFilesystem SDK,想要把该SDK装入进LeagueFlysystemFilesystem作为驱动之一,那只要再做一个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:LeagueFlysystem还提供了Plugin供自定义该package中没有的feature,LeagueFlysystemFilesystem中有一个addPlugin($plugin)方法供向LeagueFlysystemFilesystem装入plugin,当然,LeagueFlysystem中也已经提供了七八个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. LeagueFlysystemFilesystem源码解析

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实现LeagueFlysytemAdapterInterface;3. 在Laravel中AppServiceProvider中使用Storage::extend($name, Closure $callback)注册一个自定义的filesystem。

LeagueFlysystem已经提供了几个adapter,如Local、Ftp等等,并且抽象了一个abstract class AbstractAdapter供继承,所以AliyunAdapter只需要extends 这个AbstractAdapter就行了:

LeagueFlysystemFilesystem又是implements了FilesystemInterface,所以觉得这个Filesystem不太好可以自己写个替换掉它,只要实现这个FilesystemInterface就行。

2. PluggableTrait源码解析

OK, 现在需要做一个Plugin,实现对一个文件的内容进行sha1加密,看如下代码:

    // AbstractPlugin这个抽象类league/flysystem已经提供
    use LeagueFlysystemFilesystemInterface;
    use LeagueFlysystemPluginInterface;

    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 LeagueFlysystemFilesystem;
use LeagueFlysystemAdapter;
use LeagueFlysystemPlugin;

$filesystem = new Filesystem(new AdapterLocal(__DIR__."/path/to/file.txt"));
$filesystem->addPlugin(new PluginSha1File);
$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 LeagueFlysystemFilesystem($ftpAdapter);
$s3 = new LeagueFlysystemFilesystem($s3Adapter);
$local = new LeagueFlysystemFilesystem($localAdapter);

// Add them in the constructor
$manager = new LeagueFlysystemMountManager([
    "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 LeagueFlysystemFilesystem;
use LeagueFlysystemAdapterLocal as LocalAdapter;
use LeagueFlysystemCachedCachedAdapter;
use LeagueFlysystemCachedStoragePredis;

// 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模块学习了LeagueFlysystem的源码,并聊了该package的设计架构和设计技术,以后在使用中就能够知道它的内部流程,不至于黑箱使用。下次遇到好的技术再聊吧。

欢迎关注Laravel-China。

RightCapital招聘Laravel DevOps

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/30428.html

相关文章

  • Laravel学习笔记Filesystem源码解析(上)

    摘要:说明本文主要学习的模块的源码逻辑,把自己的一点点研究心得分享出来,希望对别人有所帮助。实际上,使用了的重载学习笔记之重载,通过魔术方法调用里的,而这个实际上就是,该中有方法,可以调用。 说明:本文主要学习Laravel的Filesystem模块的源码逻辑,把自己的一点点研究心得分享出来,希望对别人有所帮助。总的来说,Filesystem模块的源码也比较简单,Laravel的Illumi...

    AlphaGooo 评论0 收藏0
  • Laravel学习笔记IoC Container实例化源码解析

    摘要:说明本文主要学习容器的实例化过程,主要包括等四个过程。看下的源码如果是数组,抽取别名并且注册到中,上文已经讨论实际上就是的。 说明:本文主要学习Laravel容器的实例化过程,主要包括Register Base Bindings, Register Base Service Providers , Register Core Container Aliases and Set the ...

    ningwang 评论0 收藏0
  • Laravel学习笔记Filesystem-从Dropbox中载文件到AWS S3

    摘要:说明本文主要讲述了的文件系统的小,逻辑不复杂,主要就是把上的一个文件下载到本地,和下载到中。写驱动由于没有驱动,需要自定义下在中写上名为的驱动同时在注册下该就行。执行命令后,显示上文件从上下载到上的文件该逻辑简单,但很好玩。 说明:本文主要讲述了Laravel的文件系统Filesystem的小Demo,逻辑不复杂,主要就是把Dropbox上的一个文件下载到本地local,和下载到AWS...

    tylin 评论0 收藏0
  • Laravel学习笔记bootstrap源码解析

    摘要:总结本文主要学习了启动时做的七步准备工作环境检测配置加载日志配置异常处理注册注册启动。 说明:Laravel在把Request通过管道Pipeline送入中间件Middleware和路由Router之前,还做了程序的启动Bootstrap工作,本文主要学习相关源码,看看Laravel启动程序做了哪些具体工作,并将个人的研究心得分享出来,希望对别人有所帮助。Laravel在入口index...

    xiaoxiaozi 评论0 收藏0
  • Laravel核心——Ioc服务容器源码解析(服务器解析

    摘要:而函数作用是加载延迟服务,与容器解析关系不大,我们放在以后再说。在构造之前,服务容器会先把放入中,继而再去解析。利用服务容器解析依赖的参数。 make解析 首先欢迎关注我的博客: www.leoyang90.cn 服务容器对对象的自动解析是服务容器的核心功能,make 函数、build 函数是实例化对象重要的核心,先大致看一下代码: public function make($abst...

    hearaway 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<