PHP: create CSV file
Glossary overview

PHP: create CSV file

Functions: fputcsv, fputs, fopen, fclose

Result:

Плагин для выкачки в CSV формате всех CPT Experience со всеми acf полями.

Code:

<?php
/*
Plugin Name: Experience CSV Export/Import
Description: Allows admin to export and import 'experience' CPT with ACF fields via CSV.
Version: 1.0
Author: **
*/

add_action('admin_menu', function () {
    add_submenu_page(
        'tools.php',
        'Export/Import Experiences',
        'Experiences CSV',
        'manage_options',
        'experience-csv',
        'render_experience_csv_page'
    );
});

add_action('admin_post_export_experiences_csv', function () {
    if (!current_user_can('manage_options') || !check_admin_referer('export_experiences_csv')) {
        wp_die('Access denied or invalid nonce.');
    }

    $acf_fields = get_acf_field_keys_for_experience();
    export_experiences_csv($acf_fields);
});

function render_experience_csv_page()
{
    if (!current_user_can('manage_options')) {
        wp_die('Access denied');
    }

    $acf_fields = get_acf_field_keys_for_experience();

    echo '<div class="wrap"><h1>Export/Import Experiences</h1>';

    // Export
    echo '<h2>Export CSV</h2>';
    echo '<form method="post" action="' . admin_url('admin-post.php') . '">';
    echo '<input type="hidden" name="action" value="export_experiences_csv">';
    echo wp_nonce_field('export_experiences_csv', '_wpnonce', true, false);
    submit_button('Download CSV');
    echo '</form>';

    // Import
    echo '<h2>Import CSV</h2>';
    echo '<form method="post" enctype="multipart/form-data">';
    echo '<input type="file" name="csv_file" accept=".csv" required>'; 
    submit_button('Upload CSV');
    echo '</form>';

    echo '</div>';

    // Handle Import
    if (!empty($_FILES['csv_file']['tmp_name'])) {
        import_experiences_csv($_FILES['csv_file']['tmp_name'], $acf_fields);
    }
}

function export_experiences_csv($acf_fields)
{
    header('Content-Type: text/csv; charset=UTF-8');
    header('Content-Disposition: attachment; filename="experiences.csv"');

    $output = fopen('php://output', 'w');

    // Добавим BOM для Excel
    fputs($output, chr(239) . chr(187) . chr(191));

    fputcsv($output, array_merge(['ID', 'Title'], $acf_fields));

    $experiences = get_posts([
        'post_type' => 'experience',
        'posts_per_page' => -1,
        'post_status' => 'publish'
    ]);

    foreach ($experiences as $post) {
        $row = [$post->ID, $post->post_title];
        foreach ($acf_fields as $field) {
            $value = get_field($field, $post->ID);

            // Если массив (например, галерея, repeater) — сериализуем в JSON
            if (is_array($value)) {
                $value = json_encode($value, JSON_UNESCAPED_UNICODE);
            }

            // Добавим в строку
            $row[] = $value;
        }
        fputcsv($output, $row);
    }

    fclose($output);
    exit;
}


function import_experiences_csv($file, $acf_fields)
{
    $handle = fopen($file, 'r');
    $header = fgetcsv($handle);

    while ($row = fgetcsv($handle)) {
        $data = array_combine($header, $row);
        $post_id = (int) $data['ID'];

        if (get_post_type($post_id) === 'experience') {
            wp_update_post([
                'ID' => $post_id,
                'post_title' => sanitize_text_field($data['Title'])
            ]);

            foreach ($acf_fields as $field) {
                if (isset($data[$field])) {
                    update_field($field, $data[$field], $post_id);
                }
            }
        }
    }

    fclose($handle);
    echo '<div class="notice notice-success"><p>Import completed successfully.</p></div>';
}

function get_acf_field_keys_for_experience() {
    $acf_fields = [];
    $groups = acf_get_field_groups(['post_type' => 'experience']);

    foreach ($groups as $group) {
        $fields = acf_get_fields($group['key']);
        if (!$fields) continue;

        foreach ($fields as $field) {
            if (empty($field['name']) || $field['type'] === 'message') {
                continue;
            }

            if (in_array($field['type'], ['group', 'repeater'])) {
                if (!empty($field['sub_fields'])) {
                    foreach ($field['sub_fields'] as $sub_field) {
                        if (!empty($sub_field['name'])) {
                            $acf_fields[] = $field['name'] . '_' . $sub_field['name'];
                        }
                    }
                }
            } elseif ($field['type'] === 'clone') {
                foreach ($field['clone'] as $cloned_group_key) {
                    $cloned_fields = acf_get_fields($cloned_group_key);
                    if (!$cloned_fields) continue;

                    foreach ($cloned_fields as $cloned_field) {
                        if (!empty($cloned_field['name'])) {
                            $acf_fields[] = $cloned_field['name'];
                        }
                    }
                }
            } else {
                $acf_fields[] = $field['name'];
            }
        }
    }

    return array_unique($acf_fields);
}

Общая цель

Создать CSV-файл с данными из WordPress и отдать его браузеру для скачивания.

1. 📥 Получение запроса от пользователя

Когда админ нажимает кнопку “Download CSV”, в WordPress отправляется POST-запрос на:

/wp-admin/admin-post.php?action=export_experiences_csv

WordPress вызывает соответствующий обработчик:

add_action('admin_post_export_experiences_csv', function () {
    export_experiences_csv($acf_fields);
});

Дальше запускается функция export_experiences_csv(), которая и создаёт файл.

Вот наш action указан:

admin-post.php — это встроенный обработчик WordPress, который позволяет выполнять кастомные действия (например, экспорт, импорт, генерацию файла) через HTTP-запрос.

Как это работает.

Ты создаёшь форму:

<form method="post" action="<?php echo admin_url('admin-post.php'); ?>">
  <input type="hidden" name="action" value="export_experiences_csv">

🔹 Когда пользователь нажимает кнопку — отправляется POST-запрос на URL:

/wp-admin/admin-post.php?action=export_experiences_csv

WordPress ловит параметр action

  • WordPress по умолчанию ищет $_POST['action'] или $_GET['action']
  • И вызывает специальный хук:
do_action("admin_post_{$action}");

В нашем случае:

do_action("admin_post_export_experiences_csv");

📤 Установка заголовков (header)

header('Content-Type: text/csv; charset=UTF-8');
header('Content-Disposition: attachment; filename="experiences.csv"');

Что делают эти заголовки?

  • Content-Type: text/csv; charset=UTF-8
    🔹 Сообщает браузеру, что это CSV-файл в кодировке UTF-8.
  • Content-Disposition: attachment; filename="..."
    🔹 Указывает браузеру не отображать файл в окне, а запустить загрузку, предлагая сохранить файл под указанным именем (experiences.csv)

📄 Открытие потока в браузер (fopen)

$output = fopen('php://output', 'w');

Что это значит?

  • 'php://output' — специальный поток в PHP, который выводит данные напрямую в браузер.
  • 'w' — режим “запись”.

👉 С этого момента всё, что мы запишем в $output, будет отправлено клиенту.

✅ BOM для Excel (fputs)

Что это делает?

  • Записывает BOM (Byte Order Mark) — специальные байты EF BB BF в начале файла.
  • Это помогает Excel правильно распознать кодировку UTF-8, особенно для кириллицы и юникода.

5. 🧱 Запись данных (fputcsv)

Сначала — заголовки колонок:

fputcsv($output, ['ID', 'Title', 'Field1', 'Field2']);
  • fputcsv автоматически:
    • Экранирует значения.
    • Разделяет их запятыми.
    • Добавляет \n в конце.

Потом — строки данных:

foreach ($posts as $post) {
    $row = [$post->ID, $post->post_title, ...];
    fputcsv($output, $row);
}

Это превращает массив PHP в строку CSV, отправляемую в браузер.

🔚 Закрытие потока и завершение (fclose, exit)

fclose($output);
exit;
  • fclose($output) — закрывает поток php://output, хотя это не обязательно, но хорошая практика.
  • exit — завершает выполнение скрипта, чтобы WordPress не добавил лишний HTML после CSV.

✅ Результат

📎 Пользователь получает чистый CSV-файл, который:

  • скачивается автоматически,
  • открывается в Excel без ошибок кодировки,
  • содержит правильно структурированные данные.

Основные функции и их роль:

ФункцияНазначение
header()Устанавливает заголовки HTTP, чтобы браузер знал, как обрабатывать файл
fopen()Открывает поток вывода в браузер
fputs()Записывает BOM (спецсимволы для UTF-8)
fputcsv()Преобразует массив в строку CSV и пишет в поток
fclose()Закрывает поток вывода
exitЗавершает скрипт, чтобы браузер не получил лишнего контента