Support the ongoing development of Laravel.io →

Send, validate, and store Base64 files with Laravel

12 Feb, 2024 6 min read

Photo by Getty Images on Unsplash

Laravel makes it easy to send and upload files, at least when it comes to handling binary files. You would typically send binary files using an HTML form with a multipart/form-data encoding type. Inside your controller, you can simply get the file like this: $request->file('image') and save it on the disk. The file you receive returns an instance of the Illuminate\Http\UploadedFile class, which provides you with a lot of handy methods to manipulate the received file further. Pretty easy and straightforward, right?

But what if you want to send and receive files with JSON requests? Things get a bit tricky there since JSON requests can't transport binary data. You would have to convert your file to a Base64 string and then process it within your application to extract the necessary data. Let's see how we can do that.

<h2 class="subheading" id="covert-base64">Converting the image to Base64</h2>

First, let's see how we can convert an image of our choosing to a Base64 string. This can be of course done programmatically, but for simplicity, we are just going to upload a simple PNG image on a <a title="Convert image" href="https://base64.guru/converter/encode/image" target="_blank" rel="noopener noreferrer nofollow">Base64 Guru website</a> (you can, of course, use any other), and save the output for later use. 

Here is an example of what that might look like:

<a title="Converting an image" href="https://geoligard.com/storage/posts/January2024/covert-file.png" target="_blank"><img title="Converting an image" src="https://geoligard.com/storage/posts/January2024/covert-file.png" alt="Converting an image" width="100%" height="auto"></a>

<h2 class="subheading" id="postman-example">Postman example</h2>

Now, before we proceed, let's see a complete example of that kind of request in Postman. So, in short, we will be sending a raw JSON POST request with a single image param, and in return, we will just get the URL of the newly stored file.

That might look something like this:

<a title="Postman example" href="https://geoligard.com/storage/posts/January2024/base64-postman-example_1.png" target="_blank"><img title="Postman example" src="https://geoligard.com/storage/posts/January2024/base64-postman-example_1.png" alt="Postman example" width="100%" height="auto"></a>

<h2 class="subheading" id="initial-setup">Initial setup</h2>

Ok, we know what to expect. Let's create a controller and a route to handle the request.

<pre><code class="language-bash">php artisan make:controller Base64Controller</code></pre> <pre><code class="language-php">// api.php use App\Http\Controllers\Base64Controller; Route::post('base64', [Base64Controller::class, 'index']);</code></pre>

So far, so good. Our base64 route will target the index method inside of the Base64Controller. Next, we will add some basic code to handle the JSON request in the index method.

// Base64Controller.php

namespace App\Http\Controllers;

use Illuminate\Http\File;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\Validator;

class Base64Controller extends Controller
{
    public function index()
    {
        if (request()->isJson() && !empty(request()->input('image'))) {
            $base64Image = request()->input('image');

            if (!$tmpFileObject = $this->validateBase64($base64Image, ['png', 'jpg', 'jpeg', 'gif'])) {
                return response()->json([
                    'error' => 'Invalid image format.'
                ], 415);
            }

            $storedFilePath = $this->storeFile($tmpFileObject);

            if(!$storedFilePath) {
                return response()->json([
                    'error' => 'Something went wrong, the file was not stored.'
                ], 500);
            }

            return response()->json([
                'image_url' => url(Storage::url($storedFilePath)),
            ]);
        }

        return response()->json([
            'error' => 'Invalid request.'
        ], 400);
    }

We are first checking if we are dealing with a JSON request and if the image parameter is not empty. We then validate and upload the image. In the end, we return the URL of the newly uploaded file. Simple enough, right? You may have noticed that we have defined two helper functions, validateBase64 and storeFile that are validating and storing the file for us. Time to define them.

<h2 class="subheading" id="helper-functions">Helper functions</h2>

The first function (validateBase64) will just check if the image param is a Base64 representation of an image. It will accept a Base64 string and an array of allowed mime types. If the validation doesn't fail, a temporary file object will be returned. The function is mostly based on this <a title="Validate base64" href="https://stackoverflow.com/questions/51419310/validating-base64-image-laravel/52914093#52914093" target="_blank" rel="noopener noreferrer nofollow">Stack Overflow answer</a>.

// Base64Controller.php

/**
 * Validate base64 content.
 *
 * @see https://stackoverflow.com/a/52914093
 */
private function validateBase64(string $base64data, array $allowedMimeTypes)
{
    // strip out data URI scheme information (see RFC 2397)
    if (str_contains($base64data, ';base64')) {
        list(, $base64data) = explode(';', $base64data);
        list(, $base64data) = explode(',', $base64data);
    }

    // strict mode filters for non-base64 alphabet characters
    if (base64_decode($base64data, true) === false) {
        return false;
    }

    // decoding and then re-encoding should not change the data
    if (base64_encode(base64_decode($base64data)) !== $base64data) {
        return false;
    }

    $fileBinaryData = base64_decode($base64data);

    // temporarily store the decoded data on the filesystem to be able to use it later on
    $tmpFileName = tempnam(sys_get_temp_dir(), 'medialibrary');
    file_put_contents($tmpFileName, $fileBinaryData);

    $tmpFileObject = new File($tmpFileName);

    // guard against invalid mime types
    $allowedMimeTypes = Arr::flatten($allowedMimeTypes);

    // if there are no allowed mime types, then any type should be ok
    if (empty($allowedMimeTypes)) {
        return $tmpFileObject;
    }

    // Check the mime types
    $validation = Validator::make(
        ['file' => $tmpFileObject],
        ['file' => 'mimes:' . implode(',', $allowedMimeTypes)]
    );

    if($validation->fails()) {
        return false;
    }

    return $tmpFileObject;
}

Great, now let's define the second helper function that will process the temporary file object that was returned from the previous function and upload the file.

// Base64Controller.php

/**
 * Store the temporary file object
 */
private function storeFile(File $tmpFileObject)
{
    $tmpFileObjectPathName = $tmpFileObject->getPathname();

    $file = new UploadedFile(
        $tmpFileObjectPathName,
        $tmpFileObject->getFilename(),
        $tmpFileObject->getMimeType(),
        0,
        true
    );

    $storedFile = $file->store('images/base64', ['disk' => 'public']);

    unlink($tmpFileObjectPathName); // delete temp file

    return $storedFile;
}

And just like that, we are done. If you would like to copy the whole code from <code>Base64Controller.php</code> head out to <a title="Gist example" href="https://gist.github.com/goran-popovic/d5af7366af2a7f6f9b04ebc4a48a4e7e" target="_blank" rel="noopener noreferrer nofollow">Gist</a>.

<h2 class="subheading" id="conclusion">Conclusion</h2>

Today we made a few great functions that would allow us to accept, validate, and store Base64 encoded files. In our example, we were validating images, but you can use these functions for other types of files as well, like .txt, .pdf, and so on. Also, for simplicity, all of the code was added to the same controller, but you could also move these functions to your custom helper function file, or create a trait, and then easily reuse them whenever you need to process a Base64 file.

Last updated 3 weeks ago.

driesvints, karimalik, ol-serdiuk liked this article

3
Like this article? Let the author know and give them a clap!
geoligard (Goran Popović) Software developer, enjoys working with Laravel, blogging at geoligard.com.

Other articles you might like

November 18th 2024

Laravel Custom Query Builders Over Scopes

Hello 👋 Alright, let's talk about Query Scopes. They're awesome, they make queries much easier to r...

Read article
November 19th 2024

Access Laravel before and after running Pest tests

How to access the Laravel ecosystem by simulating the beforeAll and afterAll methods in a Pest test....

Read article
November 11th 2024

🍣 Sushi — Your Eloquent model driver for other data sources

In Laravel projects, we usually store data in databases, create tables, and run migrations. But not...

Read article

We'd like to thank these amazing companies for supporting us

Your logo here?

Laravel.io

The Laravel portal for problem solving, knowledge sharing and community building.

© 2024 Laravel.io - All rights reserved.