How to import CSV data into WordPress Custom Fields
Tags: wordpress, opensource, plugin, acf
Published: 2023-04-16 11:05:49

As a developer in a communications house, I tend to use WordPress a lot because it is a well-known and client-friendly platform.
One of the requests I receive most often from clients is **to be able to use spreadsheets to enter data into the site**.

Usually, this data has to be inserted within custom fields that allow us to create beautiful custom UI experiences without sacrificing the ease of use of WordPress. 

There are already many solutions on the market for importing CSV files within custom fields, but they are all **expensive** and **not open-source**.

For this reason, I created a **[simple plugin](https://github.com/matteodf/csv-to-acf)** that allows you to create new custom fields from the file's columns and then create pages (or other custom post types) by filling the fields with the contents of those columns.

It should be noted that **the plugin is still in the alpha stage** so it may contain bugs and glitches. Prior knowledge of custom fields, custom post types, and similar concepts is preferable to understand how it works. 

**Any pull requests or issues in the repo are appreciated.**

---

## Prerequisites

The plugin works in a simple way. All data parsing and cleaning is done client-side, while database entry is done server-side. 

For the plugin to work, it is **necessary to have the Advanced Custom Fields plugin installed**. Also, the only supported custom field types are _text_, _image_, _oEmbed_, and _link_.

There are also some prerequisites regarding **file formatting** for the plugin to work correctly. Specifically:

- **The first row must contain the field names** or at least an indication of the contents of each column. It must not include the data to be imported in any case.
- **Every other row must contain the data** to be imported. In particular, **each row must correspond to the contents of a page**.
- **The first column must contain the title of the page**.
- **The image fields must contain the URL of the image**. The URL must point directly to the image file, not the page where the image is displayed (Google Drive preview, Dropbox, and the like).

_N.B: When the image link does not provide the image extension, the plugin sets it to .jpg by default._

---

## How does it work

1. The loaded CSV file is parsed into a JSON structure. The latter is previewed to verify that the data has been correctly loaded.
2. Then, you can select **which content type to import the data into** (pages, posts, or any custom post type) and enter the **name of the custom fields group** to be inserted into the database.
3. A page is then shown where you can specify the **label**, **name**, and **type for each field detected in the file**. **An autocomplete button is also available** to fill all fields with the contents of the first line of the CSV file. The label for each field coincides with the contents of the relevant column, the name is created in snake case, and the type is automatically set as text.
4. Once the fields have been created and entered into the database, you can **decide whether to automatically publish the pages or save them as a draft**.
5. You can start the import of the pages

---

## The code

The plugin is based on [Devin Vinson's boilerplate](https://github.com/DevinVinson/WordPress-Plugin-Boilerplate) and has a very simple structure. 

The main files are contained in the admin folder. In particular, all client-side functions are contained in the `admin/js/csv-to-acf-admin.js` file and are sufficiently self-explanatory.

On the server side, two APIs take care of custom field creation and data import.

### create-field-group.php

This API deals with the insertion of new custom fields into the database. 

First, a check is made on the installation of Advanced Custom Fields and the presence of all the necessary parameters

```php
// Check if ACF is installed
if (!class_exists("ACF")) {
    print_r("ACF is not installed");
    return;
};

// Check if all required parameters are present
if (empty($_POST['title']) || empty($_POST['fields']) || empty($_POST['location']) || empty($_POST['headers'])) {
    print_r("Missing parameters");
    return;
};
```
Next, the custom field group is saved to the database using the `acf_import_field_group($field_group)` function (contained in the `includes/acf-field-group-functions.php` file in the ACF plugin folder).

The function takes in an array related to the custom fields group, built following the [ACF documentation itself](https://www.advancedcustomfields.com/resources/register-fields-via-php/).

```php
// Get parameters
$group_title = $_POST['title'];
$fields = $_POST['fields'];
$location = $_POST['location'];
$headers = $_POST['headers'];

// Create field group
$group = array(
    'key' => 'group_' . md5($group_title),
    'title' => $group_title,
    'fields' => $fields,
    'location' => array(
        array(
            array(
                'param' => 'post_type',
                'operator' => '==',
                'value' => $location,
            ),
        ),
    ),
);
$field_group = acf_import_field_group($group);
```
Finally, the keys of the JSON object are modified with the new names related to the custom fields to facilitate subsequent import operations, and the result is returned to the caller as a string

```php
// Create array with old and new field names, to be used by JS to update the JSON object with the new field names
$names = [];

foreach ($fields as $key => $field) {
    $temp = [];
    $temp['new_name'] = $field['name'];
    $temp['old_name'] = $headers[$key];
    array_push($names, $temp);
}

if ($field_group) {
    echo json_encode($names);
} else {
    print_r("Error creating field group");
}
```

### import-data.php

As for the data import, after the usual initial checks, we proceed to decode the JSON string sent from the `csv-to-acf-admin.js` file

```php
// Get parameters
$data = $_POST['data'];
$publish = $_POST['publish'];
$post_type = $_POST['post_type'];

// Decode JSON string
$tmp = substr($data, 1, -1);
$tmp = preg_split('/(?<=\})(.*?)(?=\{)/', $tmp);
foreach ($tmp as $key => $value) {
    $value = html_entity_decode(stripslashes($value));
    $tmp[$key] = json_decode($value, true);
}
```

Next, for each line, we proceed with the creation of a new page, using the [`wp_insert_post()`](https://developer.wordpress.org/reference/functions/wp_insert_post/) function.

```php
// Create posts
foreach ($tmp as $key => $row) {

    // Skip empty rows
    if ($row[array_key_first($row)] == '') {
        continue;
    }

    // Create post
    $post = array(
        'post_title' => $row[array_key_first($row)],
        'post_content' => '',
        'post_status' => $publish == "publish" ? 'publish' : 'draft',
        'post_type' => $post_type,
    );
    $post_id = wp_insert_post($post);


```

Finally, the custom fields are filled in. In particular, if the field in question is a **link**, an array is created for the [`update_field()`](https://www.advancedcustomfields.com/resources/update_field/) function to succeed. 

_Currently, the target attribute and link title are fixed values, but in a future release they may be editable_
```php

    // Add ACF fields
    if ($post_id) {
        foreach ($row as $field => $value) {

            // Skip empty fields
            if ($value == '') {
                continue;
            } elseif (acf_get_field($field)['type'] == 'link') {
                // Create link
                $site = array(
                    'title' => 'Link',
                    'url' => $value,
                    'target' => '',
                );
                update_field($field, $site, $post_id);
```

If, on the other hand, the field in question is for an **image**, the file is **first downloaded and uploaded to the WordPress media library**. Only then its ID is used to update the contents of the field.

```php

            } elseif (acf_get_field($field)['type'] == 'image') {

                // Sets upload directory and sets file type
                $uploaddir = wp_upload_dir();
                $uploadurl = $uploaddir['url'] . '/' . basename($value);
                $uploadfile = $uploaddir['path'] . '/' . basename($value);
                $wp_filetype = wp_check_filetype(basename(basename($value)), null);
                $filetype = $wp_filetype['type'];

                // Add .jpg extension if no extension is present
                if (pathinfo(basename($value), PATHINFO_EXTENSION) == "") {
                    $uploadfile .= ".jpg";
                    $uploadurl .= ".jpg";
                    $filetype = 'image/jpeg';
                };

                // Download image
                $image = file_get_contents($value);
                file_put_contents($uploadfile, $image);

                // Add image to media library
                $attachment = array(
                    'post_mime_type' => $filetype,
                    'post_title' => basename($value),
                    'post_content' => '',
                    'post_status' => 'inherit',
                    'guid' => $uploadurl,
                );
                $attach_id = wp_insert_attachment($attachment, $uploadfile);

                // Add image to ACF field
                if ($attach_id > 0 && !is_wp_error($attach_id)) {
                    update_field($field, $attach_id, $post_id);
                }
```

Finally, if the field is a **text** or an **oEmbed**, the field is updated by directly entering the desired value. 

```php

            } else {
                // Add other fields
                update_field($field, $value, $post_id);
            }
        }
    }
}
```
---

## Links

You can find the repo on GitHub: if you liked my work, give me a star or feel free to contribute with a PR or issue!

{% github matteodf/csv-to-acf %}