Cleaning Up Form Input with Transpose

This post is adapted from my book + video course, "Refactoring to Collections". Learn more about it here.

Dealing with arrays in form submissions is a pain in the ass.

Imagine you need to build a page that allows users to add multiple contacts at once. If a contact has a name, email, and occupation, ideally the incoming request would look something like this:

[
    'contacts' => [
        [
            'name' => 'Jane',
            'occupation' => 'Doctor',
            'email' => 'jane@example.com',
        ],
        [
            'name' => 'Bob',
            'occupation' => 'Plumber',
            'email' => 'bob@example.com',
        ],
        [
            'name' => 'Mary',
            'occupation' => 'Dentist',
            'email' => 'mary@example.com',
        ],
    ],
];

The problem is that crafting a form that actually submits this format is surprisingly complicated.

If you haven't had to do this before, you might think you can get away with something like this, using just a pinch of JavaScript to duplicate the form fields while keeping all of the field names the same:

<form method="POST" action="/contacts">
    <div>
        <label>
            Name
            <input name="contacts[][names]">
        </label>
        <label>
            Email
            <input name="contacts[][emails]">
        </label>
        <label>
            Occupation
            <input name="contacts[][occupations]">
        </label>
    </div>

    <!-- Adds another set of form fields using JavaScript -->
    <button type="button">Add another contact</button>

    <button type="submit">Save contacts</button>
</form>

...but this gives you a request that looks like this:

[
    'contacts' => [
        [ 'name' => 'Jane' ],
        [ 'occupation' => 'Doctor' ],
        [ 'email' => 'jane@example.com' ],
        [ 'name' => 'Bob' ],
        [ 'occupation' => 'Plumber' ],
        [ 'email' => 'bob@example.com' ],
        [ 'name' => 'Mary' ],
        [ 'occupation' => 'Dentist' ],
        [ 'email' => 'mary@example.com' ],
    ],
];

To get the form to submit in the correct format, you need to give each set of fields an explicit index:

<form method="POST" action="/contacts">
    <div>
        <label>
            Name
            <input name="contacts[0][names]">
        </label>
        <label>
            Email
            <input name="contacts[0][emails]">
        </label>
        <label>
            Occupation
            <input name="contacts[0][occupations]">
        </label>
    </div>

    <!-- Adds another set of form fields using JavaScript -->
    <button type="button">Add another contact</button>

    <button type="submit">Save contacts</button>
</form>

...which means that when you add another set of fields, you need to change the name of every input, incrementing the index by one.

Doesn't seem too unreasonable at first, just count the sets of fields and add one for the new set right?

Wrong! What if a user removes a set of fields? Or two sets of fields? Now there might only be 3 sets remaining but the last set still has an index of 4, so just counting the fields is going to result in a collision.

So what can you do? Well, you have a few options:

  1. Parse out the index from the last set of fields and add one to that number whenever you add new fields.
  2. Keep track of the index as state in your JavaScript.
  3. Throw away all of the indexes and recalculate them every time you add or remove a set of fields.

All of a sudden this seems like a lot more work on the front-end than you signed up for! But there's one other option:

Submit the data in a different format and deal with it on the server.

As long as we aren't nesting past the empty square brackets, PHP is happy to let us leave out the index. So what you'll commonly see people do in this situation (and what you may have done yourself) is name the form fields like this:

<form method="POST" action="/contacts">
    <div>
        <label>
            Name
            <input name="names[]">
        </label>
        <label>
            Email
            <input name="emails[]">
        </label>
        <label>
            Occupation
            <input name="occupations[]">
        </label>
    </div>

    <!-- Adds another set of form fields using JavaScript -->
    <button type="button">Add another contact</button>

    <button type="submit">Save contacts</button>
</form>

The benefit of course is that now we don't have to keep track of the index. We can reuse the same markup for every set of fields, never worrying about the total number of fields in the form, or what happens when a set of fields is removed. Excellent!

The disadvantage is that now our incoming request looks like this:

[
    'names' => [
        'Jane',
        'Bob',
        'Mary',
    ],
    'emails' => [
        'jane@example.com',
        'bob@example.com',
        'mary@example.com',
    ],
    'occupations' => [
        'Doctor',
        'Plumber',
        'Dentist',
    ],
];

Ruh-roh!

Quick and Dirty

We need to get these contacts out of the request and into our system. Say we want our controller action to take this general form:

public function store()
{
    $contacts = /* Build the contacts using the request data */;

    Auth::user()->contacts()->saveMany($contacts);

    return redirect()->home();
}

How can we translate our request data into actual Contact objects? An imperative solution might look something like this:

public function store(Request $request)
{
    $contacts = [];

    $names = $request->get('names');
    $emails = $request->get('emails');
    $occupations = $request->get('occupations');

    foreach ($names as $i => $name) {
        $contacts[] = new Contact([
            'name' => $name,
            'email' => $emails[$i],
            'occupation' => $occupations[$i],
        ]);
    }

    Auth::user()->contacts()->saveMany($contacts);

    return redirect()->home();
}

First, we grab the names, emails, and occupations from the request. Then we arbitrarily iterate over one of them (the names in this case), pull out the other fields we need by matching up the index, and create our Contact objects.

There's certainly nothing wrong with this approach, I mean, it works, right? But something about it just feels dirty and inelegant, and I think we can do better.

Let's do it with collections :)

Identifying a Need

First things first, let's get our request data into a collection.

public function store(Request $request)
{
    $requestData = collect($request->only('names', 'emails', 'occupations'));

    // ...
}

This pulls names, emails, and occupations out into a new collection, which is about the best starting point we're going to get from that form submission.

Next, we need to somehow get our Contact objects out of this collection.

public function store(Request $request)
{
    $requestData = collect($request->only('names', 'emails', 'occupations'));

    $contacts = $requestData->/* ??? */;

    // ...
}

Typically when we have a collection of data and we need to transform each element into something new, we use map.

But in order to map our contact data into Contact objects, we need each element in our collection to contain the name, email, and occupation for a single contact. Right now, the first element in our array is all of the names, the second element is all emails, and the last element is all occupations.

So before we can use map, we need some mystery function to get our data into the right structure.

public function store(Request $request)
{
    $requestData = collect($request->only('names', 'emails', 'occupations'));

    $contacts = $requestData->/*

        Mystery operation!

    */->map(function ($contactData) {
        return new Contact([
            'name' => $contactData['name'],
            'email' => $contactData['email'],
            'occupation' => $contactData['occupation'],
        ]);
    });

    // ...
}

Introducing Transpose

Transpose is an often overlooked list operation that I first noticed in Ruby.

The goal of transpose is to rotate a multidimensional array, turning the rows into columns and the columns into rows.

Say we had this array:

$before = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
];

If we transpose that array, [1, 2, 3] becomes the first column rather than the first row, [4, 5, 6] becomes the second column, and [7, 8, 9] becomes the last column.

$after = [
    [1, 4, 7],
    [2, 5, 8],
    [3, 6, 9],
];

Let's look at our incoming request again:

[
    'names' => [
        'Jane',
        'Bob',
        'Mary',
    ],
    'emails' => [
        'jane@example.com',
        'bob@example.com',
        'mary@example.com',
    ],
    'occupations' => [
        'Doctor',
        'Plumber',
        'Dentist',
    ],
];

If we get rid of the keys, we're left with a multidimensional array that looks like this:

[
    ['Jane', 'Bob', 'Mary'],
    ['jane@example.com', 'bob@example.com', 'mary@example.com'],
    ['Doctor', 'Plumber', 'Dentist'],
];

I wonder what happens if we transpose that array?

[
    ['Jane', 'jane@example.com', 'Doctor'],
    ['Bob', 'bob@example.com', 'Plumber'],
    ['Mary', 'mary@example.com', 'Dentist'],
];

Whoa! This looks pretty close to the structure we wanted in first place, albeit without the keys. We can work with this!

Implementing Transpose

Laravel's Collection class doesn't implement transpose out of the box, but since collections are macroable, we can add it at runtime.

Here's what a basic implementation looks like:

Collection::macro('transpose', function () {
    $items = array_map(function (...$items) {
        return $items;
    }, ...$this->values());

    return new static($items);
});

I keep all of my collection extensions in a service provider like this:

class CollectionExtensions extends ServiceProvider
{
    public function boot()
    {
        Collection::macro('transpose', function () {
            $items = array_map(function (...$items) {
                return $items;
            }, ...$this->values());

            return new static($items);
        });

        // ...and any other lovely macros you'd like to add.
    }

    public function register()
    {
        // ...
    }
}

Transpose in Practice

Now that we've found our mystery function, we can finish off our controller action:

public function store(Request $request)
{
    $requestData = collect($request->only('names', 'emails', 'occupations'));

    $contacts = $requestData->transpose()->map(function ($contactData) {
        return new Contact([
            'name' => $contactData[0],
            'email' => $contactData[1],
            'occupation' => $contactData[2],
        ]);
    });

    Auth::user()->contacts()->saveMany($contacts);

    return redirect()->home();
}

We can even collapse this down further, turning just about the whole action into a single chain:

public function store(Request $request)
{
    collect($request->only([
        'names',
        'emails',
        'occupations'
    ]))->transpose()->map(function ($contactData) {
        return new Contact([
            'name' => $contactData[0],
            'email' => $contactData[1],
            'occupation' => $contactData[2],
        ]);
    })->each(function ($contact) {
        Auth::user()->contacts()->save($contact);
    });

    return redirect()->home();
}

Even though this style of programming might seem foreign at first, I think this is an improvement over our original solution.

Instead of being deep in the details worrying about looping over a data set and matching up keys between different arrays, we're operating on the entire data set at once, using a more declarative style at a higher level of abstraction.

Learning More

If you enjoyed this post, you might be interested in the new book + video course I recently released.

"Refactoring to Collections" is an in-depth guide to understanding and implementing functional programming ideas in PHP.

It covers fundamental topics like imperative vs. declarative programming, understanding higher order functions, and includes 4 hours of screencasts going over refactorings like the one in this post.

Check out the website to learn more, or subscribe below for a free chapter sample.

Refactoring-to-collections

Enjoyed the post?

I recently released a full book and video course covering other refactorings you can do with collections.

If you'd like to check out a chapter sample, subscribe below and I'll email it to you!

Powered by ConvertKit