From the About section on GitHub of league/flysystem package:
- Flysystem is a file storage library for PHP. It provides one interface to interact with many types of filesystems.
Thanks to this package, Laravel supports working with several filesystems, for example, local disk, FTP, SFTP and Amazon S3. We just have to change some configurations in config/filesystems.php
file. We can even create our own filesystem driver with minimal effort.
Without further ado, let's figure how this package is used in Laravel.
Code Dive
In this blogpost, I am going to take a different approach of code-dive as compared to previous blogposts. I will start from the service provider of the filesystem in Laravel to see what happens when framework is booted.
laravel/framework
repository has separate directories for each major module. We can easily find Filesystem module under vendor/laravel/framework/src/Illuminate/Filesystem
Let's take a look at FilesystemServiceProvider
class.
<?php
namespace Illuminate\Filesystem;
use Illuminate\Support\ServiceProvider;
class FilesystemServiceProvider extends ServiceProvider
{
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->registerNativeFilesystem();
$this->registerFlysystem();
}
/**
* Register the native filesystem implementation.
*
* @return void
*/
protected function registerNativeFilesystem()
{
$this->app->singleton('files', function () {
return new Filesystem;
});
}
/**
* Register the driver based filesystem.
*
* @return void
*/
protected function registerFlysystem()
{
$this->registerManager();
$this->app->singleton('filesystem.disk', function ($app) {
return $app['filesystem']->disk($this->getDefaultDriver());
});
$this->app->singleton('filesystem.cloud', function ($app) {
return $app['filesystem']->disk($this->getCloudDriver());
});
}
/**
* Register the filesystem manager.
*
* @return void
*/
protected function registerManager()
{
$this->app->singleton('filesystem', function ($app) {
return new FilesystemManager($app);
});
}
/**
* Get the default file driver.
*
* @return string
*/
protected function getDefaultDriver()
{
return $this->app['config']['filesystems.default'];
}
/**
* Get the default cloud based file driver.
*
* @return string
*/
protected function getCloudDriver()
{
return $this->app['config']['filesystems.cloud'];
}
}
Laravel first registers Native Filesystem and then explicitly register Flysystem. We are interested in flysystem so we will dig into that.
- Native Filesystem uses native PHP file methods under-the-hood and are available via
File
facade, e.g.File::put()
Inside registerManager()
method, we can see that a singleton is being bind to the container for filesystem
string and it returns an instance of FilesystemManager
class when resolved. After this, it's also binding the default (local) filesystem driver to the container.
The FilesystemManager
FilesystemManager
class has methods that deal with instantiation of disk/driver class. Below I have only given the declarations of related methods. We'll look into their implementations later. We can see classes ( adapters ) from league/flysystem
package here.
<?php
namespace Illuminate\Filesystem;
use League\Flysystem\Adapter\Ftp as FtpAdapter;
use League\Flysystem\Adapter\Local as LocalAdapter;
use League\Flysystem\AdapterInterface;
use League\Flysystem\AwsS3v3\AwsS3Adapter as S3Adapter;
use League\Flysystem\Cached\CachedAdapter;
use League\Flysystem\Cached\Storage\Memory as MemoryStore;
use League\Flysystem\Filesystem as Flysystem;
use League\Flysystem\FilesystemInterface;
use League\Flysystem\Sftp\SftpAdapter;
/**
* @mixin \Illuminate\Contracts\Filesystem\Filesystem
*/
class FilesystemManager implements FactoryContract
{
/**
* Get a filesystem instance.
*
* @param string|null $name
* @return \Illuminate\Contracts\Filesystem\Filesystem
*/
public function disk($name = null);
/**
* Attempt to get the disk from the local cache.
*
* @param string $name
* @return \Illuminate\Contracts\Filesystem\Filesystem
*/
protected function get($name);
/**
* Resolve the given disk.
*
* @param string $name
* @return \Illuminate\Contracts\Filesystem\Filesystem
*
* @throws \InvalidArgumentException
*/
protected function resolve($name);
/**
* Create an instance of the local driver.
*
* @param array $config
* @return \Illuminate\Contracts\Filesystem\Filesystem
*/
public function createLocalDriver(array $config);
/**
* Adapt the filesystem implementation.
*
* @param \League\Flysystem\FilesystemInterface $filesystem
* @return \Illuminate\Contracts\Filesystem\Filesystem
*/
protected function adapt(FilesystemInterface $filesystem);
}
But, how it all works?
Laravel makes it extremely simple to interact with filesystem. In below snippet, we are creating a file named example.txt
with Contents
string and putting it on local
disk.
use Illuminate\Support\Facades\Storage;
Storage::disk('local')->put('example.txt', 'Contents');
Let's try to understand the above code one part at a time.
Storage Facade
- I am assuming reader knows how Laravel Facade pattern works as that is out of scope of this blogpost.
<?php
namespace Illuminate\Support\Facades;
use Illuminate\Filesystem\Filesystem;
class Storage extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'filesystem';
}
}
We can see inside getFacadeAccessor()
it returns filesystem
string. Which is exactly the same as we saw earlier inside FilesystemServiceProvider
inside registerManager()
method.
- We can safely conclude that, by using
Storage
facade, we are basically interacting withFilesystemManager
class.
Storage::disk('local')
disk()
method on FilesystemManager
class receives the disk name, which in this case is local ( it is also default ) and try to resolve an instance of that disk based Filesystem
class.
/**
* Get a filesystem instance.
*
* @param string|null $name
* @return \Illuminate\Contracts\Filesystem\Filesystem
*/
public function disk($name = null)
{
$name = $name ?: $this->getDefaultDriver();
return $this->disks[$name] = $this->get($name);
}
/**
* Attempt to get the disk from the local cache.
*
* @param string $name
* @return \Illuminate\Contracts\Filesystem\Filesystem
*/
protected function get($name)
{
return $this->disks[$name] ?? $this->resolve($name);
}
/**
* Resolve the given disk.
*
* @param string $name
* @return \Illuminate\Contracts\Filesystem\Filesystem
*
* @throws \InvalidArgumentException
*/
protected function resolve($name)
{
$config = $this->getConfig($name);
if (empty($config['driver'])) {
throw new InvalidArgumentException(
"Disk [{$name}] does not have a configured driver."
);
}
$name = $config['driver'];
$driverMethod = 'create' . ucfirst($name) . 'Driver';
if (!method_exists($this, $driverMethod)) {
throw new InvalidArgumentException(
"Driver [{$name}] is not supported."
);
}
return $this->{$driverMethod}($config);
}
If we look at resolve()
method, we can see that its loading the config
for the given disk and creating a method name dynamically.
$driverMethod = 'create' . ucfirst($name) . 'Driver';
In our case, it will result in createLocalDriver()
. We can find this method inside FilesystemManager
class along with others like createS3Driver()
, createSftpDriver()
.
use League\Flysystem\Filesystem as Flysystem;
use League\Flysystem\Adapter\Local as LocalAdapter;
/**
* Create an instance of the local driver.
*
* @param array $config
* @return \Illuminate\Contracts\Filesystem\Filesystem
*/
public function createLocalDriver(array $config)
{
$permissions = $config['permissions'] ?? [];
$links = ($config['links'] ?? null) === 'skip'
? LocalAdapter::SKIP_LINKS
: LocalAdapter::DISALLOW_LINKS;
return $this->adapt($this->createFlysystem(new LocalAdapter(
$config['root'],
$config['lock'] ?? LOCK_EX,
$links,
$permissions
), $config));
}
/**
* Create a Flysystem instance with the given adapter.
*
* @param \League\Flysystem\AdapterInterface $adapter
* @param array $config
* @return \League\Flysystem\FilesystemInterface
*/
protected function createFlysystem(AdapterInterface $adapter, array $config)
{
$config = Arr::only($config, ['visibility', 'disable_asserts', 'url']);
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);
}
Inside createLocalDriver()
method it is creating a new instance of Local
adapter from flysystem package and then pass that adapter to base class Filesystem
(aliased as Flysystem
) of flysystem package.
In the end, Laravel wraps the base class instance in its own implementation inside adapt()
method. FilesystemAdapter
class can be found in the same directory as FilesystemManager
class.
Storage::disk('local')→put()
By the end of disk()
method execution, we have FilesystemAdapter
instance. So we can safely assume that put()
method must be inside FilesystemAdapter
class.
<?php
namespace Illuminate\Filesystem;
class FilesystemAdapter
{
/**
* Write the contents of a file.
*
* @param string $path
* @param string|resource $contents
* @param mixed $options
* @return bool
*/
public function put($path, $contents, $options = [])
{
$options = is_string($options)
? ['visibility' => $options]
: (array) $options;
if ($contents instanceof File ||
$contents instanceof UploadedFile) {
return $this->putFile($path, $contents, $options);
}
if ($contents instanceof StreamInterface) {
return $this->driver->putStream(
$path, $contents->detach(), $options
);
}
return is_resource($contents)
? $this->driver->putStream($path, $contents, $options)
: $this->driver->put($path, $contents, $options);
}
}
Other commonly used methods like Storage::get()
, Storage::delete()
, Storage::download()
can also be found in FilesystemAdapter()
class.
Link to source code of the class.
This is how Laravel leverages flysystem
package to provide fluent and easy-to-use Filesystem API.
Interesting facts about Flysystem in Laravel context
league/flysystem
package was first added to Laravel in v5.0- Taylor Otwell made the commit "Working on filesystem" on 24 Aug, 2014
- Initial version of this packaged used in Laravel was
v0.5
. It was upgraded tov1.0
in Dec, 2014. - Initially, Rackspace was also included along with AWS S3 as cloud filesystems.
league/flysystem
package has major changes inv2
. Laravel v9 is expected to containv2
of this package.- Pull request by Dries Vints already merged into
master
branch, which upgrades this package tov2
in Laravelv9
.
I hope you enjoyed this post. Next, we will see how Laravel uses monolog/monolog
package. You can follow me on Twitter or join my newsletter for more content.