Send, validate, and store Base64 files with Laravel
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.
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.
driesvints, karimalik, ol-serdiuk liked this article
Other articles you might like
Laravel Custom Query Builders Over Scopes
Hello 👋 Alright, let's talk about Query Scopes. They're awesome, they make queries much easier to r...
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....
🍣 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...
The Laravel portal for problem solving, knowledge sharing and community building.
The community