说明:本文主要学习Laravel的Filesystem模块的源码逻辑,把自己的一点点研究心得分享出来,希望对别人有所帮助。总的来说,Filesystem模块的源码也比较简单,Laravel的Illuminate\Filesystem模块主要依赖于League\Flysystem这个Filesystem Abstractor Layer,类似于是League\Flysystem的Laravel Bridge。而不同的Filesystem SDK有着各自的具体增删改查逻辑,如AWS S3 SDK,Dropbox SDK,这些SDK都是通过Adapter Pattern装载入这个Filesystem Abstractor Layer。Filesystem模块的整体架构如下两张图:
开发环境:Laravel5.2+MAMP+PHP7+MySQL5.6
1. Illuminate\Filesystem\FilesystemServiceProvider
Laravel中每一个Service模块都有对应的ServiceProvider,主要帮助把该Service注册到Container中,方便在应用程序中利用Facade调用该Service。同样,Filesystem Service有对应的FilesystemServiceProvider,帮助注册files
和filesystem
等Service:
// Illuminate\Filesystem
$this->app->singleton('files', function () {
return new Filesystem;
});
$this->app->singleton('filesystem', function () {
return new FilesystemManager($this->app);
});
使用Container的singleton单例注册,同时还注册了filesystem.disk
(config/filesystems.php的default配置选项)和filesystem.cloud
(config/filesystems.php的cloud配置选项)。其中,files
的Facade为IlluminateSupportFacadesFile
,filesystem
的Facade为IlluminateSupportFacadesFilesystem
。
2. Illuminate\Filesystem\FilesystemManager
Laravel官网上有类似这样代码:
// Recursively List下AWS S3上路径为dir/to的所有文件,迭代所有的文件和文件夹下的文件
$s3AllFiles = Storage::disk('s3')->allFiles('dir/to');
// Check S3 上dir/to/filesystem.png该文件是否存在
$s3AllFiles = Storage::disk('s3')->exists('dir/to/filesystem.png');
那这样的代码内部实现逻辑是怎样的呢?
翻一下Illuminate\Filesystem\FilesystemManager代码就很容易知道了。首先Storage::disk()是利用了Facade模式,Storage是名为filesystem
的Facade,而filesystem
从上文知道实际是FilesystemManager的对象,所以可以看做(new FilesystemManager)->disk(),看disk()方法源码:
// Illuminate\Filesystem\FilesystemManager
/**
* Get a filesystem instance.
*
* @param string $name
* @return \Illuminate\Contracts\Filesystem\Filesystem
*/
public function disk($name = null)
{
// 如果不传参,就默认filesystems.default的配置
$name = $name ?: $this->getDefaultDriver();
// 这里传s3,$this->get('s3')取S3 driver
return $this->disks[$name] = $this->get($name);
}
/**
* Get the default driver name.
*
* @return string
*/
public function getDefaultDriver()
{
return $this->app['config']['filesystems.default'];
}
/**
* Attempt to get the disk from the local cache.
*
* @param string $name
* @return \Illuminate\Contracts\Filesystem\Filesystem
*/
protected function get($name)
{
// PHP7里可以这样简洁的写 $this->disks[$name] ?? $this->resolve($name);
return isset($this->disks[$name]) ? $this->disks[$name] : $this->resolve($name);
}
/**
* Resolve the given disk.
*
* @param string $name
* @return \Illuminate\Contracts\Filesystem\Filesystem
*
* @throws \InvalidArgumentException
*/
protected function resolve($name)
{
// 取出S3的配置
$config = $this->getConfig($name);
// 检查自定义驱动中是否已经提前定义了,自定义是通过extend($driver, Closure $callback)定制化driver,
// 如果已经定义则取出定制化driver,下文介绍
if (isset($this->customCreators[$config['driver']])) {
return $this->callCustomCreator($config);
}
// 这里有个巧妙的技巧,检查Illuminate\Filesystem\FilesystemManager中是否有createS3Driver这个方法,
// 有的话代入$config参数执行该方法,看createS3Driver()方法
$driverMethod = 'create'.ucfirst($config['driver']).'Driver';
if (method_exists($this, $driverMethod)) {
return $this->{$driverMethod}($config);
} else {
throw new InvalidArgumentException("Driver [{$config['driver']}] is not supported.");
}
}
/**
* Get the filesystem connection configuration.
*
* @param string $name
* @return array
*/
protected function getConfig($name)
{
return $this->app['config']["filesystems.disks.{$name}"];
}
/**
* Create an instance of the Amazon S3 driver.
*
* @param array $config
* @return \Illuminate\Contracts\Filesystem\Cloud
*/
public function createS3Driver(array $config)
{
$s3Config = $this->formatS3Config($config);
$root = isset($s3Config['root']) ? $s3Config['root'] : null;
$options = isset($config['options']) ? $config['options'] : [];
// use League\Flysystem\AwsS3v3\AwsS3Adapter as S3Adapter,这里用了League\Flysystem\Filesystem,
// 上文说过Laravel的Filesystem只是个Filesystem Bridge,实际上用的是League\Flysystem这个依赖。
// League\Flysystem源码解析会在下篇中讲述,
// 主要使用了Adapter Pattern把各个Filesystem SDK 整合到一个\League\Flysystem\FilesystemInterface实例中,
// 有几个核心概念:Adapters, Relative Path, Files First, Plugin, MountManager(File Shortcut), Cache。
// 下面代码类似于
// (new \Illuminate\Filesystem\FilesystemAdapter(
// new \League\Flysystem\Filesystem(
// new S3Adapter(new S3Client(), $options), $config)
// )
// ))
return $this->adapt($this->createFlysystem(
new S3Adapter(new S3Client($s3Config), $s3Config['bucket'], $root, $options), $config
));
}
/**
* Create a Flysystem instance with the given adapter.
*
* @param \League\Flysystem\AdapterInterface $adapter
* @param array $config
* @return \League\Flysystem\FlysystemInterface
*/
protected function createFlysystem(AdapterInterface $adapter, array $config)
{
$config = Arr::only($config, ['visibility', 'disable_asserts']);
// use League\Flysystem\Filesystem as Flysystem
return new Flysystem($adapter, count($config) > 0 ? $config : null);
}
/**
* Adapt the filesystem implementation.
*
* @param \League\Flysystem\FilesystemInterface $filesystem
* @return \Illuminate\Contracts\Filesystem\Filesystem
*/
protected function adapt(FilesystemInterface $filesystem)
{
return new FilesystemAdapter($filesystem);
}
通过代码里注释,可以看出Storage::disk('s3')实际上返回的是这样一段类似代码:
(new \Illuminate\Filesystem\FilesystemAdapter(new \League\Flysystem\Filesystem(new S3Adapter(new S3Client(), $options), $config))))
所以,Storage::disk('s3')->allFiles($parameters)或者Storage::disk('s3')->exists($parameters),实际上调用的是IlluminateFilesystemFilesystemAdapter这个对象的allFiles($parameters)和exists($parameters)方法。
3. Illuminate\Filesystem\FilesystemAdapter
查看FilesystemAdapter的源码,提供了关于filesystem的增删改查的一系列方法:
/**
* Determine if a file exists.
*
* @param string $path
* @return bool
*/
public function exists($path)
{
// 实际上又是调用的driver的has()方法,$driver又是\League\Flysystem\Filesystem对象,
// 查看\League\Flysystem\Filesystem对象的has()方法,
// 实际上是通过League\Flysystem\AwsS3v3\AwsS3Adapter的has()方法,
// 当然最后调用的是AWS S3 SDK包的(new S3Client())->doesObjectExist($parameters)检查S3上该文件是否存在
return $this->driver->has($path);
}
/**
* Get all of the files from the given directory (recursive).
*
* @param string|null $directory
* @return array
*/
public function allFiles($directory = null)
{
return $this->files($directory, true);
}
/**
* Get an array of all files in a directory.
*
* @param string|null $directory
* @param bool $recursive
* @return array
*/
public function files($directory = null, $recursive = false)
{
$contents = $this->driver->listContents($directory, $recursive);
return $this->filterContentsByType($contents, 'file');
}
/**
* Pass dynamic methods call onto Flysystem.
*
* @param string $method
* @param array $parameters
* @return mixed
*
* @throws \BadMethodCallException
*/
public function __call($method, array $parameters)
{
return call_user_func_array([$this->driver, $method], $parameters);
}
通过代码注释知道,Storage::disk('s3')->exists($parameters)实际上最后通过调用S3 SDK的(new S3Client())->doesObjectExist($parameters)检查S3上有没有该文件,Storage::disk('s3')->allFiles($parameters)也是同理,通过调用(new League\Flysystem\AwsS3v3\AwsS3Adapter(new S3Client(), $config))->listContents()来list contents of a dir.
根据上文的解释,那Storage::disk('s3')->readStream($path)
可以调用不?
可以的。实际上,\Illuminate\Filesystem\FilesystemAdapter使用了PHP的重载(Laravel学习笔记之PHP重载(overloading)),通过__call($method, $parameters)魔术方法调用$driver里的$method,而这个$driver实际上就是(new \League\Flysystem\Filesystem),该Filesystem Abstract Layer中有readStream方法,可以调用。
Laravelgu官网中介绍通过Storage::extend($driver, Closure $callback)来自定义driver,这里我们知道实际上调用的是(new \Illuminate\Filesystem\FilesystemManager($parameters))->extend($driver, Closure $callback),上文中提到该对象的resolve($name)代码时会先检查自定义驱动有没有,有的话调用自定义驱动,再看下resolve()代码:
/**
* Resolve the given disk.
*
* @param string $name
* @return \Illuminate\Contracts\Filesystem\Filesystem
*
* @throws \InvalidArgumentException
*/
protected function resolve($name)
{
$config = $this->getConfig($name);
// 检查自动以驱动是否存在,存在的话,调用callCustomCreator来解析出$driver
if (isset($this->customCreators[$config['driver']])) {
return $this->callCustomCreator($config);
}
$driverMethod = 'create'.ucfirst($config['driver']).'Driver';
if (method_exists($this, $driverMethod)) {
return $this->{$driverMethod}($config);
} else {
throw new InvalidArgumentException("Driver [{$config['driver']}] is not supported.");
}
}
/**
* Call a custom driver creator.
*
* @param array $config
* @return \Illuminate\Contracts\Filesystem\Filesystem
*/
protected function callCustomCreator(array $config)
{
$driver = $this->customCreators[$config['driver']]($this->app, $config);
if ($driver instanceof FilesystemInterface) {
return $this->adapt($driver);
}
return $driver;
}
extend()方法就是把自定义的驱动注册进$customCreators里:
/**
* Register a custom driver creator Closure.
*
* @param string $driver
* @param \Closure $callback
* @return $this
*/
public function extend($driver, Closure $callback)
{
$this->customCreators[$driver] = $callback;
return $this;
}
总结:上篇主要讲述了Laravel Filesystem Bridge,该Bridge只是把League/Flysystem这个package简单做了桥接和封装,便于在Laravel中使用。明天再写下篇,主要学习下League/Flysystem这个package的源码,League/Flysystem作为一个Filesystem Abstractor Layer,利用了Adapter Pattern来封装各个filesystem的SDK,如AWS S3 SDK或Dropbox SDK。到时见。
欢迎关注Laravel-China。
RightCapital招聘Laravel DevOps