Posted in Laravel 21 Oct 2018 @ 02:45 PM~5 min read

Dealing with FormData

FormData can get messy. Really messy. Because, apparently, null === null”. With a combination of a frontend helper and a backend middleware, all problems may be solved in a pretty intuitive way. Note: This is designed for the Laravel/​Vue combination – feel free to amend to your requirements; the concept is pretty damn simple.

So let’s quickly talk about FormData. The Mozilla developer docs inherently state that the spec for this in-built class requires that data added to an instance of the class must be a sub-type of USVString or Blob (including subclasses, such as a File). The USVString part is what makes this a challenge.

In Vue, or any other frontend-framework alike, we sometimes need to perform an XHR request that contains both normal data (such as a string, number, boolean) as well as a file the user has uploaded. The problem comes in when you append the data iteratively from your source object – primitives are read as strings, and there’s no way to determine their types out of the box.

In a few projects, this has become really cumbersome, and a workaround’ became needed. Rather than re-writing the spec (haha), it’s often best to find a method that can be used to treat the issue, but in a clean and simple way. The method below solves the problem – it may not be bullet-proof, but it’s something, and works in the general range of scenarios.

Source Object

So we have a source object, stored in state. Let’s call this user. A user has several fields, such as name, email and profile_picture.

// Pre-populated for brevity of flow.
export default class User {
  constructor() {
    this.name = 'Mike Rockett'
    this.email = 'mike@test.com'
    this.profile_picture = <File>
  }
}

Here, <File> is just pseudo-code for an actual file, prepared by a file-reader, or grabbed from event.target.files[0] on a file input element.

Next, here’s a state reference:

export let state = {
  user: new User
}

Parsing the data

In a normal situation, we’d want to iterate through the source object and append each key/​value pair to an instance of FormData. We could accomplish this with the following (in a file called form-data.js, for example):

export default object => Object.keys(object).reduce((formData, key) => {
  formData.append(key, object[key])
  return formData
}, new FormData())

We’d then import the function and parse the source object in an action:

import makeFormData from './form-data'

export let actions = {
  doSomething: context => new Promise((resolve, reject) => {
    api.doSomething(makeFormData(context.state.user), resolve, reject)
  })
}

The problem

In this case, name and email become strings – they have no content type, and so Laravel will see null as 'null', which can be problematic when it comes to validation. A nullable check on a form field will fail because, well, null isn’t 'null'.

The Solution

The solution here is convoluted, but needed because of spec-limitations. In a nutshell, FormData was likely not designed for this use-case – it was really designed for browser-controlled <form> requests. With the advent of data-driven frameworks like Vue (along with it’s state manager, Vuex), requests became XHR requests by default, and so this caveat’ came into the picture.

We start with a modified helper to parse our source object, and turn it into something that we can reverse-engineer’ at the server-side. Again, this is convoluted, but it does the job pretty well.

The basic concept is to iterate the source object and make a differentiation between values that are instances of File or Blob (I think they go hand in hand, but it doesn’t really matter), and have anything that isn’t move into a key called json, which is a stringified version of the object, built up by the helper.

With that said, here’s our new helper:

export default object => {
  let data = new FormData(), json = {}

  Object.keys(object).reduce((object, key) => {
    if (object[key] instanceof File) {
      data.append(key, object[key])
    } else {
      json[key] = object[key]
    }
    return object
  }, object)

  data.append('json', JSON.stringify(json));

  return data
}

Instead of simply iterating through the source object and appending it to an instance of FormData (created by the reducer’s initiator), we iterate and do an instance-type-check before deciding what to do next. If the value of the iterated key is a file (or blob, inherently), we simply append it to the instance. Otherwise, we append to an object with the aim of passing this object to the instance in stringified form.

This is really the key here. By stringifying the object, we keep our types as we want them. But, this means we need to do something about the backend – we don’t want to deal with a json key in our request, but rather work with the input as we normally would.

The Middleware

Given that we’re working in the context of a Laravel-Vue combination, a middleware seems most apt for this kind of situation. Such a middleware would need to detect a) if the incoming request is a multi-part form-data request and b) if the request content contains a key named json. If the conditions are right, we can iterate through the json object after decoding it, and move each key/​value pair into the request itself. Finally, once the data has been transferred, we can purge the json object, and let the framework continue as normal.

Here’s the middleware:

<?php
namespace App\Http\Middleware;

use Closure;

class ParseFormData
{
  /**
   * @param  \Illuminate\Http\Request  $request
   * @param  \Closure  $next
   * @return mixed
   */
  public function handle($request, Closure $next)
  {
    $isFormData = str_contains(
      $request->headers->get('content-type'),
      'multipart/form-data'
    );

    if ($isFormData && $request->has('json')) {
      $request->merge((array) json_decode($request->json));
      $request->request->remove('json');
    }

    return $next($request);
  }
}

This middleware needs to be registered, of course. Given the nature of the middleware (it manipulates a request), we need to make sure it runs before the TrimStrings middleware, provided by the framework itself:

// app/Http/Kernel.php

protected $middleware = [
  …
  \App\Http\Middleware\ParseFormData::class,
  \App\Http\Middleware\TrimStrings::class,
  …
];

Our request is now clean, and we can interact with it as we normally would. Files are files, and type-safe parameters are type-safe parameters, as we’d want them to be.

If you have any suggestions on how this could be improved, or if you have another idea in mind, feel free let me know in the comments. Also, if you’d like to share your framework-specific implementation, post it in the comments to share with others.

Loading Comments...