How to connect elFinder with Amazon S3 for Yii2

If you are planning to create a blog for your website, you will surely end up requiring an image uploader to spice up your blog posts. On this article I am going to explain how I solved that challenge by using SimpleMDE Markdown editor and elFinder.

When you create a website, one of the most important things that you usually need to add is blogging. Since our site is a Yii2 application, we first searched for a good blog module that could suit our needs, but we failed to find the right one. None of the available modules matched our exact needs, so we decided to create our own custom blog module.

The Blog Post Editor

Being nerds, we wanted to use Markdown instead of regular HTML. We decided to use SimpleMDE Markdown Editor, as it included preview capabilities. The first thing we did was to include it as a bower asset requirement to our composer.json file (we assume that you have already installed the composer asset plugin as it's required for any Yii2 application):

Adding SimpleMDE Markdown Editor Source

composer require bower-asset/simplemde-markdown-editor

At the time of this blog post entry, the version registered on your composer.json file would be "bower-asset/simplemde-markdown-editor": "^1.10". Once, we had the assets registered, we will create a widget for its use on our view. At 2amigOS!, we tend to create reusable components. It allows us to ease the task of our developers if they need the same functionality again on other current or future projects. This time the new component was a widget called, SimpleMDEWidget. Here is the source code for both SimpleMDEAsset and SimpleMDEWidget (of course, we will write an open source version of our blog, so it will be included there, too):

SimpleMDEAsset

namespace backend\widgets; // update your namespace!

use yii\web\AssetBundle;

class SimpleMDEAsset extends AssetBundle
{
    public $sourcePath = '@bower/simplemde-markdown-editor/dist';
    public $js = [
        'simplemde.min.js'
    ];
    public $css = [
        'simplemde.min.css'
    ];
		public $depends = [
		    'yii\web\JqueryAsset'
		];
}

SimpleMDEWidget

namespace backend\widgets;

use yii\helpers\Html;
use yii\helpers\Json;
use yii\web\JsExpression;
use yii\widgets\InputWidget;


class SimpleMDEWidget extends InputWidget
{
    public $htmlOptions = [];

    public function run()
    {
        if ($this->hasModel()) {
            echo Html::activeTextarea($this->model, $this->attribute, $this->htmlOptions);
        } else {
            $this->htmlOptions['id'] = $this->getId();
            echo Html::textarea($this->name, $this->value, $this->htmlOptions);
        }
        $this->registerScripts();
    }

    public function registerScripts()
    {
        $view = $this->getView();
        SimpleMDEAsset::register($view);
        $this->options['element'] = new JsExpression("jQuery('#{$this->options['id']}')[0]");
        $options = Json::encode($this->options);
        $js = "jQuery('#{$this->options['id']}').data('simpleMDE', new SimpleMDE($options));";
        $view->registerJs($js);
    }
}

Now that we have the widget ready, it is time to add it to our view. This is how we did it:

<?= $form->field($model, 'text')->widget( SimpleMDEWidget::className(), [ ]) ?>

The widget handles the registration of all required assets and the initialization scripts. This is how actually looks:

SimpleMDEWidget

ElFinder FileBrowser

We have just solved the challenge on how to write the contents for our blog posts using markdown. After that we wanted to be able to add images to our content, and we also wanted to be able to upload them to our Amazon S3 instance with ease. So there was no need to first upload images and browse for them from our UI.

We were about to write our own extension again as there were not really any clean solutions out there, but that would end up slowing down the process of publishing articles and getting our blog live. So, we decided to use ElFinder as MihailDev did create what appears to be a good Yii2 extension. The only problem? All the instructions were in Russia! :)

So, after I pulled my hair out and nearly became bald trying to understand what it meant (my Russian sucks), I finally got it working the way I wanted:

  • ∗ I wanted to use FlySystem, as that component was already in use within our internal backend admin panel.
  • ∗ It had be opened in a modal window, preferably the one from Bootstrap (I dislike the window.open feature).

In order to do that we had to add the following libraries to our composer.json file (I placed them in multiple lines for the sake of readability):

composer require league/flysystem 
composer require league/flysystem-aws-s3-v3 
composer require creocoder/yii2-flysystem 
composer require mihaildev/yii2-elfinder 
composer require mihaildev/yii2-elfinder-flysystem

Configuring FlySystem To Work With Amazon S3

Creocoder did some good work writing those FlySystem adapters for Yii2 , but we wanted to connect to our Amazon S3 and its AwsS3FileSystem. This required a minor tweak: region was required, not optional, and also it was using a deprecated method factory to create an S3Client. We couldn't wait for the developer to review and accept a PR so we built our own version:

AwsS3FileSystem

namespace common\components; // modify NS to match yours!

use Aws\S3\S3Client;
use creocoder\flysystem\Filesystem;
use League\Flysystem\AwsS3v3\AwsS3Adapter;
use yii\base\InvalidConfigException;


class AwsS3Filesystem extends Filesystem
{
    /**
     * @var string
     */
    public $key;
    /**
     * @var string
     */
    public $secret;
    /**
     * @var string
     */
    public $region;
    /**
     * @var string
     */
    public $bucket;
    /**
     * @var string|null
     */
    public $prefix;
    /**
     * @var string
     */
    public $version = "latest";
    /**
     * @var array
     */
    public $options = [];

    /**
     * @inheritdoc
     */
    public function init()
    {
        if ($this->key === null) {
            throw new InvalidConfigException('The "key" property must be set.');
        }

        if ($this->secret === null) {
            throw new InvalidConfigException('The "secret" property must be set.');
        }

        if ($this->bucket === null) {
            throw new InvalidConfigException('The "bucket" property must be set.');
        }

        if ($this->region === null) {
            throw new InvalidConfigException('The "region" property must be set.');
        }

        parent::init();
    }

    /**
     * @return AwsS3Adapter
     */
    protected function prepareAdapter()
    {
        $config = [
            'credentials' => ['key' => $this->key, 'secret' => $this->secret],
            'region' => $this->region,
            'version' => $this->version
        ];

        $client = new S3Client($config);

        return new AwsS3Adapter(
            $client,
            $this->bucket,
            $this->prefix
        );
    }
}

With the correct version of AwsS3FileSystem we were ready to configure the component:

'components' => [
   // ... 
    'fs' => [
        'class' => 'common\components\AwsS3Filesystem', // We placed here!
        'key' => '{YOURAMAZONKEY}',
        'secret' => '{YOURAMAZONSECRET}',
        'bucket' => '{YOURBUCKET}',
        'region' => 'us-east-1',
        'options' => [
            'version' => '2006-03-01', // important
        ]
    ],
		// ...
],

Configuring ElFinder Yii2 Extension

Having the FlySystem component set, now we can configure MihailDev the extension:

return [
    // ....
		'controllerMap' => [
        'elfinder' => [
            'class' => 'mihaildev\elfinder\PathController',
            'access' => ['@'],
            'disabledCommands' => ['netmount'],
            'root' => [
                'class' => 'mihaildev\elfinder\flysystem\Volume',
                // This is actually very important as will be prepending 
								// your files URL!
                'url' => 'https://s3.amazonaws.com/2amigos/', 
								// Check the previous section!
                'component' => 'fs'
            ],
        ]
    ],
		// ...
];

Displaying ElFinder In A Modal

Last, but not least, we wanted to display ElFinder in a modal. Having built our backend on top of the Bootstrap CSS Framework, we did the following:

Create A Custom Button on SimpleMDE

First of all, we had to add a custom button to the SimpleMDE markdown editor. That wasn't hard to do as SimpleMDE Markdown Editor allows you to do it by adding a custom configuration button to its toolbar section. This is how we did it:

<?= $form->field($model, 'text')->widget(
    SimpleMDEWidget::className(),
    [
        'options' => [
            'toolbar' => [
                'bold', 'italic', 'strikethrough', '|',
                'heading-1', 'heading-2', 'heading-3', '|',
                'code','quote','unordered-list', 'ordered-list', '|',
                'clean-block', 'link',
                [
                    'name' => 'test',
                    'className' => "fa fa-picture-o", // Look for a suitable icon
                    'action' => new JsExpression(
                        'function(editor){
                            var $modal = $("#s3Dialog"); // The modal dialog id
                            if(!$modal.data("editor")) {
                                $modal.data("editor", editor);
                            }
                            $modal.modal("show");
                        }'
                    )
                ],
                '|',
                'table', 'horizontal-rule', '|',
                'preview', 'side-by-side', 'fullscreen', '|',
                'guide',
            ]
        ]
    ]
) ?>

As you can see, the script adds a reference to the SimpleMDE Markdown Editor instance to its data attributes. The reason is to allow ElFinder::callbackFunction() to actually access the instance with ease as we are going to use it to write the image markdown syntax to the editor.

Render ElFinder Widget

Finally, we had to add the bootstrap modal syntax to the view rendering the SimpleMDEWidget. The modal would contain an ElFinder widget with a custom callbackFunction() that would receive the selected file object, render the image tag in Markdown syntax and close the modal:

<div id="s3Dialog" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="s3Dialog"
     aria-hidden="true" style="display: none;">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
                <h4 class="modal-title" id="myModalLabel">S3 Image File Browser</h4>
            </div>
            <div class="modal-body">
                <p>
                    <?= ElFinder::widget(
                        [
                            'controller' => 'elfinder',
                            'filter' => 'image',
                            'callbackFunction' => new JsExpression(
                                'function(file, id){
                                    var editor = $("#s3Dialog").data("editor");
                                    if(editor) {
                                       var cm = editor.codemirror;
                                       var output = "![](" + file.url + ")";
                                       cm.replaceSelection(output);
                                    }
                                    $("#s3Dialog").modal("hide");
                                }'
                            ),
                            'containerOptions' => [
                                'style' => 'height:375px'
                            ],
                            'frameOptions' => [
                                'style' => 'height:100%;width:100%;border:0'
                            ]
                        ]
                    );
                    ?>
                </p>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-primary btn-lg" data-dismiss="modal">Close</button>
            </div>
        </div><!-- /.modal-content -->
    </div><!-- /.modal-dialog -->
</div><!-- /.modal -->

And that's it! It took a bit more than expected, right? But there you go, a beautiful file browser connected to your Amazon S3 bucket that allows you to not only insert them to the contents of your SimpleMDE Markdown Editor but also manage your bucket without leaving the form of your blog post.

Posted by Antonio Ramirez

Antonio Ramirez is the co-founder and co-innovator of 2amigOS! Consulting Group LLC. Open Source believer and technology enthusiast.