Export class for your custom WordPress plugin or project

I have been developing custom WordPress plugins for clients for many years. Even though there are so many great plugins in existence, they cannot all do exactly what your client needs and a custom plugin is required. Often several custom post types are created and, of course, metadata is saved for all of them. I will often use existing plugins like Advanced Custom Fields and Gravity Forms to help speed up the development. Then, there is the need to export all this data because the organization wants to run reports on everything in their favorite tools they already know and love.

I know WP All Import exists. It does a lot and looks like it works really well but I have never used it. I just happen to be of the mindset that the fewer plugins the better since it reduces overhead and increases exact control over what is done for the client. I want the client to have a one-click experience and WP All Import doesn’t provide that. It is a tool built to handle any situation and I just want to have a tool that handles our situation. The fewer things a client has to do or click in the admin the easier it is, the more likely they will use it, and most importantly less likely they will need help with it. I don’t like handling really basic support questions so my goal is always to make things as easy for the client as possible from the start.

So, I created a base class for building a CSV export utility that can easily be included in any custom WordPress plugin. Include the base class, add a custom export class for your needs, and off we go with a great export utility for the project that now takes just minutes to set up. Unlimited different exports can be set up each with the ability to have different data columns, file names, iterations, etc.

When CSV files are created, they are put into a custom folder which you can define within “wp-content/uploads”.

I inline CSS and JS into this single PHP class to reduce extra file dependencies to make this as easy for you as the developer as it will be for your client to use.

Base Class

<?php
// Define the maximum number of files that gets stored in the history before being deleted
define( 'WPS_EXPORT_MAX_FILES', 20 );
// Define the folder name that files will be saved to within wp-content/uploads
define( 'WPS_EXPORT_FOLDER','custom-exports' );

abstract class WPSunshine_Export {

    protected $name; // human readable name of the export
    protected $description; // Descirption to help admin know what this export does
    protected $global = true; // Show on main Exports page, otherwise a link must be included elsewhere
    protected $id;
    protected $filename; // Filename to be downloaded
    protected $iteration = 100; // How many data points get processed each iteration
    protected $headers = array(); // CSV headers
    protected $total = 0;
    protected $args = array();

    public function __construct() {

        if ( !$this->name ) { // At minimum need a name
            return;
        }

        $this->args = array_map( 'sanitize_text_field', $_GET );

        add_filter( 'wps_exports', array( $this, 'register_export' ) );

    }

    public function register_export( $exports ) {
        $this->id = $this->get_id();
        $exports[ $this->id ] = $this;
        return $exports;
    }

    public function get_id() {
        if ( !$this->id ) {
            $this->id = sanitize_title( $this->name );
        }
        return $this->id;
    }

    public function get_name() {
        return $this->name;
    }

    public function get_description() {
        return $this->description;
    }

    public function get_filename() {
        if ( !$this->filename ) {
            // Default file name is the name with the date
            $this->filename .= sanitize_title( $this->get_name() . ' ' . date( 'Y-m-d h:i:s' ) ) . '.csv';
        }
        return $this->filename;
    }

    public function get_iteration() {
        return $this->iteration;
    }

    public function get_headers() {
        return $this->headers;
    }

    public function is_global() {
        return $this->global;
    }

    public function export() { }

    public function get_total() {
        return $this->total;
    }

    public function process_rows( $fh, $args ) { }

}

// Add admin menu page to tools.php
add_action( 'admin_menu', 'wps_export_submenu_page' );
function wps_export_submenu_page() {
    add_submenu_page(
        'tools.php',
        __( 'Custom Exports', 'wps-export' ),
        __( 'Custom Exports', 'wps-export' ),
        'manage_options',
        'wps-export',
        'wps_export'
    );
}

add_action( 'admin_init', 'wps_export_folder' );
function wps_export_folder() {
	$directory = wps_export_directory();
	if ( !file_exists( $directory ) ) {
	    mkdir( $directory );
	}
}

function wps_export_directory( $type = '' ) {
	$upload_dir = wp_upload_dir();
	if ( $type == 'url' ) {
		$directory = $upload_dir['baseurl'] . '/' . WPS_EXPORT_FOLDER;
	} else {
		$directory = $upload_dir['basedir'] . '/' . WPS_EXPORT_FOLDER;
	}
	return $directory;
}

function wps_get_exports() {
    $exports = apply_filters( 'wps_exports', array() );
    return $exports;
}

function wps_export() {
	global $wpdb;

    $tab = ( !empty( $_GET['tab'] ) ) ? sanitize_key( $_GET['tab'] ) : 'exports';
?>
    <style>
        #wps-exports { border: 1px solid #c3c4c7; background: #FFF; margin: 20px 0 0 0; width: 100%; border-spacing: 0; }
        #wps-exports tr:nth-child( even ) td { background: #fcfcfc; }
        #wps-exports td { padding: 20px 30px; }
        #wps-exports td:nth-child( 2 ) { text-align: right; }
        #wps-exports h2 { margin: 0 0 5px 0; }
        #wps-exports p { color: #666; margin: 0; }
        #wps-export-files li { list-style: none; }
    </style>
    <div class="wrap">

        <nav class="nav-tab-wrapper">
            <a href="<?php echo admin_url( 'tools.php?page=wps-export' ); ?>" class="nav-tab <?php echo ( $tab == 'exports' ) ? 'nav-tab-active' : ''; ?>"><?php _e( 'Exports', 'wps-export' ); ?></a>
            <a href="<?php echo admin_url( 'tools.php?page=wps-export&tab=history' ); ?>" class="nav-tab <?php echo ( $tab == 'history' ) ? 'nav-tab-active' : ''; ?>"><?php _e( 'Exports', 'wps-export' ); ?></a>
        </nav>

        <?php
        if ( $tab == 'exports' ) {

            if ( empty( $_GET['export'] ) ) {

                echo '<table id="wps-exports">';
                $exports = wps_get_exports();
                foreach ( $exports as $id => $export ) {
                    if ( $export->is_global() ) {
                        echo '<tr>';
                        echo '<td>';
                        echo '<h2>' . $export->get_name() . '</h2>';
                        if ( $export->get_description() ) {
                            echo '<p>' . $export->get_description() . '</p>';
                        }
                        $url = admin_url( 'admin.php?page=wps-export&export=' . $export->get_id() );
                        $url = wp_nonce_url( $url, 'wps-export', 'wps-export-nonce' );
                        echo '</td><td>';
                        echo '<p><a href="' . $url . '" class="button button-primary primary">' . __( 'Start Export', 'wps-export' ) . '</a></p>';
                        echo '</td></tr>';
                    }
                }
                echo '</table>';

            } else {
                do_action( 'wps_export_process' );
            }

        } elseif ( $tab == 'history' ) {
            $directory = wps_export_directory();
            $files = glob( $directory . '/*.csv' );
            if ( count( $files ) > 0 ) {
                $final_files = array();
                foreach ( $files as $file ) {
                    $final_files[ filemtime( $file ) ] = $file;
                }
                ksort( $final_files );
                $final_files = array_reverse( $final_files );
                echo '<ul id="wps-export-files">';
                $base_url = wps_export_directory( 'url' );
                $count = 0;
                foreach ( $final_files as $file ) {
                    $count++;
                    if ( $count <= WPS_EXPORT_MAX_FILES ) {
                        echo '<li><a href="' . $base_url . '/' . basename( $file ) . '" target="_blank">' . basename( $file ) . '</a></li>';
                    } else {
                        unlink( $file ); // Delete all files more than the allowed max
                    }
                }
                echo '</ul>';
            }
        }
    ?>
    </div>
<?php
}

add_action( 'wps_export_process', 'wps_export_process' );
function wps_export_process() {
    global $wpdb;

	if ( empty( $_GET['export'] ) || empty( $_GET['wps-export-nonce'] ) || !wp_verify_nonce( $_GET['wps-export-nonce'], 'wps-export' ) ) {
        return;
    }

    $exports = wps_get_exports();
    foreach ( $exports as $id => $export ) {
        if ( $id == $_GET['export'] ) {
            break;
        }
    }

    if ( !$export ) {
        return;
    }

    $total = $export->get_total();
	$filename = $export->get_filename();
	$directory = wps_export_directory( 'url' );
    $iteration = $export->get_iteration();
?>

	<h2><?php echo sprintf( __( 'Building "%s" %d rows at a time...', 'wps-export' ), $export->get_name(), $iteration ); ?></h2>
    <?php if ( $total > 0 ) { ?>
    	<div id="progress-bar" style="background: #000; height: 30px; position: relative;">
    		<div id="percentage" style="height: 30px; background-color: green; width: 0%;"></div>
    		<div id="processed" style="position: absolute; top: 0; left: 0; width: 100%; color: #FFF; text-align: center; font-size: 18px; height: 30px; line-height: 30px;">
    			<span id="processed-count">0</span> of <span id="processed-total"><?php echo $total; ?></span>
    		</div>
    	</div>
    <?php } else { ?>
        <ol id="results" reversed style="display: none; max-height: 500px; overflow: scroll; background: #FFF; padding: 20px 40px; margin: 0;"></ol>
    <?php } ?>
	<p align="center" id="abort"><a href="users.php?page=wps-export"><?php _e( 'Abort Export', 'wps-export' ); ?></a></p>
	<p align="center" id="download" style="display: none;"><a href="<?php echo $directory . '/' . $filename; ?>" class="button button-primary primary"><?php _e( 'Download file', 'wps-export' ); ?></a></p>
	<script type="text/javascript">
	jQuery( document ).ready(function($) {
		var processed = 0;
		var total = <?php echo $total; ?>;
		var percent = 0;
		var filename = '<?php echo $filename; ?>';

        // Using promises to make ajax requests sequential
		function wps_build_export( starting_from ) {

            return function() {

                var defer = $.Deferred();

			    var data = {
    				'action': 'wps_export_rows',
                    'security': '<?php echo wp_create_nonce( 'wps_export_rows' ); ?>',
                    'export_id': '<?php echo $export->get_id(); ?>',
    				'starting_from': starting_from,
    				'filename': filename,
                    <?php
                        foreach ( $_GET as $key => $value ) {
                            echo "'" . $key . "': '" . $value . "',\r\n";
                        }
                    ?>
    			};
    			$.post( ajaxurl, data, function( response ) {
    				var result_data = JSON.parse( response );
    				processed += parseInt( result_data.processed );
    				if ( processed >= total || result_data.processed < <?php echo $iteration; ?> ) {
    					$( '#abort' ).hide();
    					$( '#download' ).show();
    				}
    				$( '#processed-count' ).html( processed );
    				percent = Math.round( ( processed / total ) * 100 );
    				$( '#percentage' ).css( 'width', percent + '%' );
    				if ( response ) {
    					$( '#results' ).show().prepend( '<li>' + result_data.processed + ' processed, ' + processed + ' total so far...</li>');
    				}
                    defer.resolve();
    			});

                return defer.promise();

            };

		}
        var base = $.when({});
		for ( i = 0; i < total; i += <?php echo $iteration; ?> ) {
            base = base.then( wps_build_export( i ) );
		}
	});
	</script>

<?php
}

add_action( 'wp_ajax_wps_export_rows', 'wps_export_rows' );
function wps_export_rows() {
	global $wpdb;

	extract( $_POST );

    if ( !wp_verify_nonce( $security, 'wps_export_rows' ) ) {
        return false;
    }

	// See if file exists. If not, make it.
	$directory = wps_export_directory();
	$file = $directory . '/' . $filename;
	if ( file_exists( $file ) ) {
	  $fh = fopen( $file, 'a' );
	} else {
	  $fh = fopen( $file, 'w' );
	}

	$processed = 0;

    $exports = apply_filters( 'wps_exports', array() );
    foreach ( $exports as $id => $export ) {
        if ( $id == $_POST['export_id'] ) {
            break;
        }
    }

	if ( $starting_from == 0 ) {
		$headers = $export->get_headers();
		fputcsv( $fh, $headers );
	}

    $args = array_merge( $_GET, $_POST );
    $processed = $export->process_rows( $fh, $args );

	fclose( $fh );

	// return/echo out how many were processed
	$result = array(
		'starting_from' => $starting_from,
		'processed' => $processed,
	);
	echo json_encode( $result );
	exit;

}

Example Extending Class

You can now create any number of extending classes for however number of unique exports you need. This is part of an example for a client of mine, Perfect Gift Club. On the site, customers are allowed to create Contacts (later changed to Recipients) as a list of people they want to be automatically reminded to get gifts for via email along with recommended gifts for that person based on age, gender, and additional information. The client wanted an export utility to get the information on all the Contacts for internal research purposes.

Be sure to customize this to your exact needs including your own class name. See below the code for what everything means and what can be customized.

<?php
class PGC_Export_Recipients extends WPSunshine_Export {

    protected $name = 'Recipients';
    protected $description = 'Export all recipients';
    protected $global = true;
    protected $iteration = 50;
    protected $headers = array(
        'First Name',
        'Last Name',
        'Birthdate',
        'Age',
        'Relationship',
        'Email',
    );

    public function process_rows( $fh, $args ) {

        $processed = 0;
        $args = array(
            'offset' => $args['starting_from'],
            'posts_per_page' => $this->iteration,
            'post_type' => 'contact',
        );
        $contacts = get_posts( $args );
        if ( !empty( $contacts ) ) {

            foreach ( $contacts as $c ) {

                $contact = new PGC_Contact( $c );
                $row = array();
                $row[] = $contact->get_first_name();
                $row[] = $contact->get_last_name();
                $row[] = $contact->get_birthdate( 'M d' );
                $row[] = $contact->get_age_name();
                $row[] = $contact->get_relationship_name();
                $row[] = $contact->get_email();

                fputcsv( $fh, $row );
                $processed++;

            }

        }

        return $processed;
    }

    public function get_total() {
        // Get all published posts for the "contact" custom post type
        $contact_counts = wp_count_posts( 'contact' );
        if ( $contact_counts ) {
            return $contact_counts->publish;
        }
        return 0;
    }

}
new PGC_Export_Recipients();

$name – Give your export a human-readable name to be viewed in the admin

$description – Optional description to help admin user understand what the export will do

$global – Should this appear automatically on the Tools > Custom Exports page. Setting this to false means you can include a link to this export from somewhere else in your plugin.

$iteration – How many rows of data will get processed with each request. If you are doing a lot of heavy lifting with each row, then you may need to use a low value for this to help reduce timeouts.

$headers – Array of data labels for the header row of the CSV file.

process_rows() – This function handles how each iteration of rows gets processed and how to build the row of data. Make sure your row data matches up with your headers!

get_total() – This is used to determine how many rows we need to work with to make the admin tool visually appealing for the admin user and see its progress.

You can even use your own get_filename() function in this class to create your own custom file name specific to this export instead of using the default.

What it looks like