Send, validate, and store Base64 files with Laravel
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.
Converting the image to Base64
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 Base64 Guru website (you can, of course, use any other), and save the output for later use.
Here is an example of what that might look like:
Postman example
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:
Initial setup
Ok, we know what to expect. Let's create a controller and a route to handle the request.
php artisan make:controller Base64Controller
// api.php
use App\Http\Controllers\Base64Controller;
Route::post('base64', [Base64Controller::class, 'index']);
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.
Helper functions
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 Stack Overflow answer.
// 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 Base64Controller.php
head out to Gist.
Conclusion
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 newsletter for future updates.
Originally published at geoligard.com.
Subscribe to my newsletter
Read articles from Goran Popović directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Goran Popović
Goran Popović
Software developer, enjoys working with Laravel, blogging at https://geoligard.com.