Uploading image file through API using Symfony2 & FOSRESTBundle

MoonwalkerHN picture MoonwalkerHN · Jun 16, 2013 · Viewed 28.7k times · Source

I have been coding an API for a photo sharing app like Instagram using Symfony2, FOSRESTBundle, and Vichuploader for file uploads.

I'm able to work around GET and POST requests, but I can't find an example of how to attach to a POST request, the actual image so that in my case, Vichuploader can grab it and help me out with the file upload.

By the way, I can upload a file without issue using the stack mentioned through the use of a normal form.

Answer

AlixB picture AlixB · May 23, 2014

I have been looking for a solution about the exact same problem. Here is what I did.

First let me explain my constraints. I wanted my API to be full JSON and to take power of the HTTP protocol (headers, methods, etc.). I chose to use:

  • Symfony2 for the "everything is almost done for you, just code your business logic".
  • Doctrine2 because by default with Symfony2 and provide a way to integrate with most popular DBMS by changing one line.
  • FOSRestBundle to handle the REST part of my API (do the maximum with annotations, body listener, auto format for the response with JMSSerializer support, etc.).
  • KnpGaufretteBundle because I wanted to be allowed to change the way I store blob file quickly.

First solution envisaged: base64

My first thought, because I was thinking JSON everytime, was to encode all the incoming images in base64, then decode them inside my API and store them.

The advantage with this solution is that you can pass images along with other data. For instance upload a whole user's profile in one API call. But I read that encoding images in base64 make them grow by 33% of their initial size. I did not wanted my users to be out of mobile data after sending 3 images.

Second solution envisaged: form

Then I thought using forms as described above. But I did not know how my clients could send both JSON data (for instance {"last_name":"Litz"}) and image data (for instance image/png one). I know that you can deal with an Content-Type: multipart/form-data but nothing more.

Plus I was not using forms in my API since the beginning and I wanted it to be uniform in all my code. [mini-edit: hoho, something I just discovered, here]

Third and last solution: use HTTP..

Then, one night, the revelation. I'm using Content-Type: application/json for send JSON data. Why not use image/* to send images? So easy that I searched for days before coming with this idea. This is how I did it (simplified code). Let suppose that the user is calling PUT /api/me/image with a Content-Type: image/*

  1. UserController::getUserImageAction(Request $request) - Catching the Request

        // get the service to handle the image
        $service = $this->get('service.user_image');
        $content = $request->getContent();
        $userImage = $service->updateUserImage($user, $content);
    
        // get the response from FOSRestBundle::View
        $response = $this->view()->getResponse();
        $response->setContent($content);
        $response->headers->set('Content-Type', $userImage->getMimeType());
    
        return $response;
    
  2. UserImageService::updateUserImage($user, $content) - Business Logic (I put everything here to be simplier to read)

        // Create a temporary file on the disk
        // the temp file will be delete at the end of the script
        // see http://www.php.net/manual/en/function.tmpfile.php
        $file = tmpfile();
        if ($file === false) 
            throw new \Exception('File can not be opened.');
    
        // Put content in this file
        $path = stream_get_meta_data($file)['uri'];
        file_put_contents($path, $content);
    
        // the UploadedFile of the user image
        // referencing the temp file (used for validation only)
        $uploadedFile = new UploadedFile($path, $path, null, null, null, true);
    
        // the UserImage to return
        $userImage = $user->getUserImage();
        if (is_null($userImage))
        {
            $userImage = new UserImage();
            $userImage->setOwner($user); 
            // auto persist with my configuration
            // plus generation of a unique ID that allows
            // me to retrieve the image at anytime
            $userImage->setKey(/*random string*/);
        }
    
        // fill the UserImage properties
        $userImage->setImage($uploadedFile);
        $userImage->setMimeType($uploadedFile->getMimeType());
    
        /** @var ConstraintViolationInterface $validationError */
        if (count($this->getValidator()->validate($userImage)) > 0)
            throw new \Exception('Validation');
    
        // if no error we can write the file definitively
        // [KnpGaufretteBundle code to store on disk]
        // [use the UserImage::key to store]
        $this->getEntityManager()->flush();
    
        return $userImage;