phpoopdependency-injectionwrapperthephpleague

How To Wrap League Flysystem with Dependency Injection


The aim is to create a Reader class that is a wrapper on top of League Flysystem documentation

The Reader should provide convenient way of reading all files in a directory no matter the file physical form (local file, or a file in an archive)

Due to DI method a wrapper should not create instances of dependencies inside of it but rather take those dependencies as arguments into a constructor or other setter method.

Here is an example how to use League Flysystem on its own (without the mentioned wrapper) to read a regular file from a disk:

<?php
use League\Flysystem\Filesystem;
use League\Flysystem\Adapter\Local;

$adapter = new Local(__DIR__.'/path/to/root');
$filesystem = new Filesystem($adapter);
$content = $filesystem->read('path-to-file.txt');

As you can see firstly you create an adapter Local that requires path in its constructor then you create filesystem that requires instance of adapter in its constructor.

arguments for both: Filesystem and Local are not optional. They must be passed when creating objects from these classes. both classes also don't have any public setters for these arguments.

My question is how to write the Reader class that wraps Filesytem and Local by using Dependency Injection then?

I normally would do something similar to this:

<?php

use League\Flysystem\FilesystemInterface;
use League\Flysystem\AdapterInterface;

class Reader
{
    private $filesystem;
    private $adapter

    public function __construct(FilesystemInterface $filesystem, 
                                AdapterInterface $adapter)
    {
        $this->filesystem = $filesystem;
        $this->adapter = $adapter;
    }    

    public function readContents(string $pathToDirWithFiles)
    {
        /**
         * uses $this->filesystem and $this->adapter
         * 
         * finds all files in the dir tree
         * reads all files
         * and returns their content combined
         */
    }
}

// and class Reader usage
$reader = new Reader(new Filesytem, new Local);
$pathToDir = 'someDir/';
$contentsOfAllFiles = $reader->readContents($pathToDir);

//somwhere later in the code using the same reader object
$contentsOfAllFiles = $reader->readContents($differentPathToDir);

But this will not work because I need to pass a Local adapter to Filesystem constructor and in order to do that I need to pass to Local adapter path firstly which is completly against whole point of Reader's convinience of use that is just passing path to dir where all files are and the Reader does all what it needs to be done to provide content of these files with just a one method readContents().

So I'm stuck. Is it possible to acheive that Reader as a wrapper on the Filestem and its Local adapter?

I want to avoid tight coupling where I use keyword new and get dependecies' objects this way:

<?php
use League\Flysystem\Filesystem;
use League\Flysystem\Adapter\Local;

class Reader
{
    public function __construct()
    {
    }    

    public function readContents(string $pathToDirWithFiles)
    {

        $adapter = new Local($pathToDirWithFiles);
        $filesystem = new Filesystem($adapter);

        /**
         * do all dir listing..., content reading
         * and returning results.
         */
    }
}

Questions:

  1. Is there any way to write a wrapper that uses Filesystem and Local as dependencies in Dependency Injection fashion?

  2. Is there any other pattern than wrapper (adapter) that would help to build Reader class without tightly coupling to Filesystem and Local?

  3. Forgetting for a while about Reader class at all: If Filesystem requires Local instance in its constructor and Local requires string (path to dir) in its constructor, then is it possible to use these classes inside Dependency Injection Container (Symfony or Pimple) in reasonable way? DIC does not know what path arg pass to the Local adapter since the path will be evaluated somewhere later in the code.


Solution

  • You can use the Factory Pattern to generate a Filesystem on the fly, whenever your readContents method is called:

    <?php
    
    use League\Flysystem\FilesystemInterface;
    use League\Flysystem\AdapterInterface;
    
    class Reader
    {
        private $factory;
    
        public function __construct(LocalFilesystemFactory $factory)
        {
            $this->filesystem = $factory;
        }    
    
        public function readContents(string $pathToDirWithFiles)
        {
            $filesystem = $this->factory->createWithPath($pathToDirWithFiles);
    
            /**
             * uses local $filesystem
             * 
             * finds all files in the dir tree
             * reads all files
             * and returns their content combined
             */
        }
    }
    

    Your factory is then responsible for creating the properly configured filesystem object:

    <?php
    
    use League\Flysystem\Filesystem;
    use League\Flysystem\Adapter\Local as LocalAdapter;
    
    class LocalFilesystemFactory {
        public function createWithPath(string $path) : Filesystem
        {
            return new Filesystem(new LocalAdapter($path));
        }
    }
    

    Finally, when you construct your Reader, it would look like this:

    <?php
    
    $reader = new Reader(new LocalFilesystemFactory);
    $fooContents = $reader->readContents('/foo');
    $barContents = $reader->readContents('/bar');
    

    You delegate the work of creating the Filesystem to the factory, while still maintaining the goal of composition through dependency injection.