Send, validate, and store Base64 files with Laravel
Tags: laravel, php, base64, json
Published: 2024-03-05 00:25:35

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.

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

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>


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

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>


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

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

<br>
<pre><code class="language-bash">php artisan make:controller Base64Controller</code></pre>
<br>

<pre><code class="language-php">// api.php

use App\Http\Controllers\Base64Controller;

Route::post('base64', [Base64Controller::class, 'index']);</code></pre>
<br>

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.

<br>

```php
// 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);
    }
```

<br>

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.

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

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>.

<br>

```php
// 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;
}
```

<br>

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.

<br>

```php
// 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;
}
```

<br>

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>.

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

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.

---

_Enjoyed the article? Consider subscribing to my <a href="https://geoligard.com/newsletter" target="_blank">newsletter</a> for future updates._

---

_Originally published at <a href="https://geoligard.com/send-validate-and-store-base64-files-with-laravel" target="_blank">geoligard.com</a>._