Uploading images in text editor for Easy Admin 3

Uploading images in text editor for Easy Admin 3

If you are using Symfony framework and have just discovered Easy Admin, you might have been blown away by how easy it is to set up an admin interface. Through a simple CLI it's easy to generate CRUD controllers for your entities, then it's off to add links for these controllers on the dashboard controller. Today we're not going to dive deeper into how to set up Easy Admin, but we are going to take a look at one of the fields that we find: the Text Editor Field, and how we can insert images to it.

The built in text editor for Easy Admin 3 is Trix. Check it out at Trix:s showcase webpageor its GitHub page. With this simple editor it will style (add markup) to the text that we add in the way we want. It's not as fully fledged as CKEditor but it has the most common editing tools that we want in a text editor. It even has a feature for uploading image files that are dragged onto it.

Today we are going to accomplish this:
easyadmin01.gif 2.11 MB

There's two things we need to address to be able to upload an image: sending and receiving! While Trix is built to be able to send images to a remote destination this is not built into the package that is delivered with Easy Admin. And on the receiving end there is nothing but what we add to our Symfony application. We are first going to set up a controller to receive files and store them.

Set up a receiving controller
We are going to build a Controller that will receive a POST request containing a file. In response we are going to send a response indicating whether the post was successful or not. We are also going to include the path to the file destination in the response.

We are as always extending our controller from AbstractController and naming it FileUploadController. We will place it on an admin route to secure it for only authenticated users (remember to update your security.yml file for that). The controller will have one method: addFile(Request $request). Here's the whole code for this controller:

namespace App\Controller\Admin;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class FileUploadController extends \Symfony\Bundle\FrameworkBundle\Controller\AbstractController
{
/**
* @Route("/admin/file-upload", name="admin_upload", methods={"post", "put"})
*/
public function addFile(Request $request) {

// We are getting whatever is stored as 'file', might be nothing!
$uploadedFile = $request->files->get('file');
$destination = $this->getParameter('kernel.project_dir').'/public/uploads';

// If there was not any file, just return an error response.
if (!$uploadedFile) {
    return new JsonResponse(array(
        'status' => 'Error',
        'message' => 'No image to upload'),
        Response::HTTP_BAD_REQUEST);
    }

    // If we get this far, we can go ahead and store the file
    $originalFilename = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME);
    $newFilename = $originalFilename.'-'.uniqid().'.'.$uploadedFile->guessExtension();
    $uploadedFile->move($destination, $newFilename);
    
    // Ready a response to indicate that all went well, and where to find the uploaded image.
    $response = new Response();
    $response->setContent(json_encode([
        'filepath' => '/uploads/'.$newFilename,
    ]));
    
    $response->headers->set('Content-Type', 'application/json');

    return $response;
    }
}

We are doing setting up some rules for how to upload files, but they aren't very stringent. First, the uploader must have been accepted to access routes under "/admin/". Second, the uploaded file must be located under a key named 'file' in the Request object. Third, the response from this route will contain information of where the uploaded file can be found. These are all information that will be useful as we move over to code our sending side of things.

Trix Editor does contain features for uploading file, which is the reason that it will fire off an event if an image is dropped in the text area. This event is called "trix-attachment-add". The GitHub page for Trixlinks to an example for a script on how to upload images asynchronously. We are going to tweak the script a bit to use fetch instead of XHR, which means that it's mostly the uploadFile function that is different from the example. We are also going to set the HOST to match the route we set up in FileUploadController. The script is stored in a file named uploader.js. Here's our JavaScript code for the upload functionality:

(function() {
    console.log("uploader.js is loaded");
    var HOST = "https://localhost:8000/admin/file-upload"

    addEventListener("trix-attachment-add", function(event) {
        console.log('Event registered.');
        if (event.attachment.file) {
            console.log('Event contains file.');
            uploadFileAttachment(event.attachment)
        }
    })

    function uploadFileAttachment(attachment) {
        uploadFile(attachment.file, setProgress, setAttributes)

        function setProgress(progress) {
            attachment.setUploadProgress(progress)
        }

        function setAttributes(attributes) {
            attachment.setAttributes(attributes)
        }
    }

    function uploadFile(file, progressCallback, successCallback) {
        var key = createStorageKey(file);
        var formData = createFormData(key, file);
        console.log(formData, key, file);

        fetch(HOST, {
            method: 'POST',
            credentials: 'same-origin',
            body: formData
        })
            .then(response => response.json())
            .then(json => {
                console.log('Success: ', json);
                var attributes = {
                    url: 'https://localhost:8000' + json.filepath,
                    href: 'https://localhost:8000' + json.filepath + "?content-disposition=attachment"
                }
                successCallback(attributes)
            })
            .catch(error => {
                console.error('Error: ', error);
            });
    }

    function createStorageKey(file) {
        var date = new Date()
        var day = date.toISOString().slice(0,10)
        var name = date.getTime() + "-" + file.name
        return [ "tmp", day, name ].join("/")
    }

    function createFormData(key, file) {
        var data = new FormData()
        data.append("key", key)
        data.append("Content-Type", file.type)
        data.append("file", file)
        return data
    }
})();

We now have a JS-file containing the logic to send file attachments that we place in our text area. Our next task is to attach the JS-file to Easy Admin. When doing operation on entities in Easy Admin 3 we have created a CRUD controller for it. We are going to modify such a controller. Let's assume that we have a BlogPostCrudController which extends an AbstractCrudController from EasyAdminBundle. This will lend us a method called configureAssets. We are going to use this to insert our JS-file:

public function configureAssets(Assets $assets): Assets
{
    return $assets->addJsFile('assets/js/uploader.js');
}

We have now completed the functionality for uploading an image. We have a JS-file that will send an image, and once we get all clear, update the image with correct path information. We also have a controller which will handle the logic of storing and responding.

Let's address some caveats! This code example assumes that we don't care about storing any relations between a blog post and the images it contains. This could make it a bit harder to track when images should be removed from storage. Another caveat is that we don't restrict the type of file being uploaded nor the size of it.

I hope you found this tutorial helpful. You are welcome to email or tweet at me if you did. See you around!