How to migrate from Thinkific to WordPress. Part 2: Users import

How to migrate from Thinkific to WordPress. Part 2: Users import

4 months ago ·
· 6 min read

In the first part of this 5-article series, I discussed the project for which I performed the migration, how I planned the migration, and how I exported data from Thinkific.

Here, in the second part, I will cover the functionality of the WP-CLI command that I implemented to import users into WordPress. Below, I also provide you with the list of articles in the series:

Table of contents

As I mentioned, here begins the most interesting part of the entire process of migrating data from Thinkific to WordPress. I intend to create a plugin that encompasses everything needed for custom WP-CLI commands, specifically the PHP class I have implemented.

I will go through the key points of the class one by one and explain the approaches along the way.

Declaring custom WP-CLI commands

To begin with, I defined a method in which I added 3 custom WP-CLI commands that I implemented further:

// ...
protected function define_commands(): void {
    if ( class_exists( 'WP_CLI' ) ) {
        \WP_CLI::add_command( 'thinkific import users', array( $this, 'import_users' ) );
        \WP_CLI::add_command( 'thinkific import progress', array( $this, 'import_progress' ) );
        \WP_CLI::add_command( 'thinkific import reviews', array( $this, 'import_reviews' ) );
// ...

These 3 commands will be available by running them in a terminal shell using WP-CLI.

Reading from file

In all three callbacks derived from the method above, namely import_users, import_progress, and import_reviews, it was necessary to read from data files (2 CSV files and a JSON file). For this, I used the API from the WP_Filesystem class belonging to WordPress, which helped me keep the code quite clean and within standard limits.

public function import_users(): void {
    global $wp_filesystem;
    include_once ABSPATH . 'wp-admin/includes/file.php';
    $file_path    = plugin_dir_path( __FILE__ ) . 'data/users.csv';
    $file_content = $wp_filesystem->get_contents( $file_path );
    $csv_data     = $this->parse_csv_content( $file_content );

As can be seen, we have implemented another helper method to interpret the CSV content into an easily traversable vector format:

protected function parse_csv_content( $csv_content ): array {
    $lines = explode( "\n", $csv_content );
    $data  = array();
    foreach ( $lines as $line ) {
        $data[] = str_getcsv( $line );
    return $data;

I was then able to simply loop through the lines in the CSV file and proceed with the data processing:

foreach ( $csv_data as $line ) {
    if ( ! $i ) {
    list(, , , , , , $courses) = $line;
    $course_arr                = explode( ', ', $courses );
    if ( count( $course_arr ) && ! empty( $course_arr[0] ) ) {
        $user_id = $this->create_user( $line );
        $this->create_woo_order( $line, $user_id );

Obviously, I skipped the header row of the table (lines 2-5). Then, I processed only the users enrolled in at least one course (line 9). There were a few cases where users were merely registered on the platform but had not purchased any courses. Consequently, migrating this data was unnecessary as it was not relevant.

Subsequently, for the remaining users, I invoked the two methods: user creation and order creation in WooCommerce - as mentioned earlier, I needed to integrate with the normal course purchase process through Tutor LMS - WooCommerce.

Create user

In the user creation method, I didn't do anything special. I passed the information from the CSV line to the method, and if the user searched by email address did not already exist, I create them. In both cases, the user ID is returned, and only in the case of a user creation error, null is returned.

protected function create_user( $user_data ) {
    list($first_name, $last_name, , $created_at, $email) = $user_data;
    $user = get_user_by( 'email', $email );
    // User already exists, return current user ID.
    if ( $user ) {
        $user_id = $user->get( 'ID' );
        return $user->get( 'id' );
    $user_id = wp_insert_user(
            'user_login'      => $email,
            'user_email'      => $email,
            'first_name'      => $first_name,
            'last_name'       => $last_name,
            'use_ssl'         => true,
            'user_registered' => str_replace( ' UTC', '', $created_at ),
    if ( is_wp_error( $user_id ) ) {
        \WP_CLI::error( sprintf( 'Error creating user %1$s %2$s <%3$s>: %4$s', $first_name, $last_name, $email, $user_id->get_error_message() ) );
        return null;
    } else {
        // User created.
        return $user_id;

Of course, when creating the user, I used the data from the file and interpreted the registration date because the provided format is not the same as the one accepted by the structure in the WordPress user table - I removed the UTC part from the date.

Create WooCommerce order

I hope I've been clear when mentioning "creating a WooCommerce order". To elaborate, I'm referring to generating an order for products in an online store built with WooCommerce, which is intended for payment and so forth. Within the WooCommerce order creation method, I manually matched the IDs of the course products, as seen in lines 2-8.

IDs 1, 2, 3, and 4 are assigned to products in the database, linked to the respective courses in the Tutor LMS plugin. Of course, the actual IDs are different.

protected function create_woo_order( array $data, int $user_id ): void {
    $raw_course_list = array(
        // course_title => product_id.
        'Course title 1' => 1,
        'Course title 2' => 2,
        'Course title 3' => 3,
        'Course title 4' => 4,
    list($first_name, $last_name, , $created_at, $email, , $courses) = $data;
    $order = wc_create_order(
            'customer_id' => $user_id,
            'status'      => 'wc-completed',
    $course_list = explode( ', ', $courses );
    // Add products to order.
    foreach ( $course_list as $course_name ) {
        $course_id = $raw_course_list[ $course_name ];
        $order->add_product( wc_get_product( $course_id ), 1 ); // 1 is for quantity.
    $address = array(
        'first_name' => $first_name,
        'last_name'  => $last_name,
        'email'      => $email,
        'country'    => 'RO',
    $order->set_address( $address, 'billing' );
    $order->set_payment_method( 'cod' );
    $order->set_date_created( $created_at );
    $order->set_date_completed( $created_at );
    $order->set_date_paid( $created_at );

Instead of using the variable $raw_course_list, I could have performed a database search for courses, making the assignment of IDs dynamic. However, this would have introduced an additional database query and complicated things. If there were many more courses, I probably would have done it this way to save time.

Moving forward, I began preparing the "order", adding courses to the cart, calculating totals, and setting the customer's address, payment method, and date. Finally, I marked the "order" as paid. From that moment, the user becomes a learner on the new e-learning platform.

Closing thoughts

That's about it for the user import; now, all that's left is to run the command, and the magic happens. 🪄

$ wp thinkific import users

The next article will follow a similar pattern, but there are some interesting points to note there as well.

If you need such a migration, click below, and let's discuss.

Contact me

Share on: