2
\$\begingroup\$

I wrote this code about one year ago. I'm looking to refactor it to be solid SOLID and secure, as this is being generated off a custom endpoint. The script is to read parameters from a query and generate custom CSV exports based off categories, tags, and order completion date. These reports can be weekly, monthly, yearly, or for all time. An email or comma separated list of emails can be passed as recipients.

The original flow I had was like this:

  • Enable errors
  • Check if correct query string set
  • If not, exit, else continue
  • Load Wordpress/Woocommerce
  • Sanitize GET variables and set interpolate things like date ranges from string values.
  • Create a collection array and loop through all orders, excluding ones that do not meet search params.
  • Sort by category
  • If no orders, exit
  • Else, write to CSV, email the CSV, and unlink files

A few questions - Should I convert this to OOP? - What are some design patterns I can use to make the logic more intuitive?

P.S. This is the cron url

(wget -O- "https://site.com/wp-content/themes/theme/scripts/auto-export.php?export=GET&filter_units=all&period=month&filter_categories=product&filter_skus=all&email_list=dan@email.com" --no-check-certificate >> log.txt)

// Set INI values for testing
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

// Exit if export type not set (correctly)
if ( !isset($_GET[ 'export' ]) || $_GET['export'] !== 'GET')
 exit;

// load Wordpress
define('WP_USE_THEMES', false);
require($_SERVER['DOCUMENT_ROOT'] . "/wp-load.php");

// Sanititze GET data
$search_data = sanitize_get();

// Get orders sorted
$orders = get_product_purchases( $search_data );
$orders = sort_orders_by( 'category', 'DESC', $orders );


// Exit if no orders
if (count($orders) == 0 ) {
    echo "There were no orders given the parameters";
    exit;
}

write_to_csv_get([
    'search_data' => $search_data,
    'orders'      => $orders,
]);
echo "Export Successful";
exit;


/**
 * get_product_purchases
 * Return array of orders
 */
function get_product_purchases( $search_data ) {
    // Destructure search data
    $cat_data   = $search_data['categories'];
    $sku_data   = $search_data['skus'];
    $period     = $search_data['period'];
    $unit_data  = $search_data['units'];

    // See set_order_args() doc-block for more details
    $args      = set_order_args( $search_data );
    $WC_Orders = wc_get_orders( $args );
    $results   = [];

    foreach ( $WC_Orders as $WC_Order ) {
        if ( empty($WC_Order)) continue;

        $order_data = $WC_Order->get_data();

        $my_order[ 'order_id' ]          = $WC_Order->get_id();
        $my_order['order_date']          = $order_data['date_created']->date('Y-m-d');
        $my_order['shipping_first_name'] = $order_data['shipping']['first_name'];
        $my_order['shipping_last_name']  = $order_data['shipping']['last_name'];
        $my_order['shipping_address_1']  = $order_data['shipping']['address_1'];
        $my_order['shipping_address_2']  = $order_data['shipping']['address_2'];
        $my_order['shipping_city']       = $order_data['shipping']['city'];
        $my_order['shipping_state']      = $order_data['shipping']['state'];
        $my_order['shipping_postcode']   = $order_data['shipping']['postcode'];

        foreach ( $WC_Order->get_items() as $item_key => $item_values ) {
            $my_order_copy = $my_order;

            ## Using WC_Order_Item methods ##
            $my_item['id'] = $item_values->get_id();           // Item ID is directly accessible from the $item_key in the foreach loop or

            ## Access Order Items data properties (in an array of values) ##
            $item_data = $item_values->get_data();

            $product_id                   = $item_data['product_id'];
            $product['product_name']      = $item_data['name'];
            $product['quantity']          = $item_data['quantity'];

            // Get data from The WC_product object using methods (examples)
            $WC_Product = $item_values->get_product(); // the WC_Product object
            if ( empty($WC_Product)) {
                continue;
            };

            $item['item_name'] = preg_replace( '/[\x00-\x1F\x7F-\xFF]/', '', $item_values->get_name() ) ; // Name of the product
            $product['sku']    = $WC_Product->get_sku();
            $product['price']  = ( $WC_Product->get_price() )
                ? sprintf( '%.2f', $WC_Product->get_price() )
                : '';

            // Tags = business units
            $product['tag']     = strtolower( get_tag_name( $product_id ) );

            // Term names are categories
            $terms   = get_the_terms( $product_id, 'product_cat' );
            $cats    = [];

            foreach( $terms as $term ) {
                array_push( $cats, strtolower( $term->name ));
            }

            $product['category']    = $cats[0];

            /*
                 Perform condition check on whether or not to push to export

                If a filter list is set, check the product property against
                the filter list and see if it matches.  If it does not,
                exclude it from the CSV
             */
            $push = check_against_list( $cat_data, $product, 'category' );
            if ( $push == 0 ) continue;

            $push = check_against_list( $unit_data, $product, 'tag' );
            if ( $push == 0 ) continue;

            $push = check_against_list( $sku_data, $product, 'sku' );
            if ( $push == 0 ) continue;

            if ( $push == 1 ) {
                $row = array_merge( $my_order_copy, $product );
                array_push( $results, $row );
            }
        }
    }

    return $results;
}

/**
 * @param  array $params [search_data & orders]
 * @return [void]
 */
function write_to_csv_get( $params ) {
    // Destructure params
    $period  = $params['search_data']['period'];
    $orders  = $params['orders'];

    $date_summary = ( $period['all'] == 0 )
        ? "from {$period['range']['start_date']} to {$period['range']['end_date']}"
        : "Since Beginning";

    $breakdown_filename = "company-{$period['type']}-order-details.csv";
    $path     = "/tmp";
    $fp       = fopen("$path/$breakdown_filename", 'w');

    if ( fopen( "$path/$breakdown_filename", "w+" ) ) {
        $totals        = [];  // Sum of quantities
        $placed_header = false;

        foreach ( $orders as $index => $row) {
            $totals = sum( $row, $totals );

            // Old code for writing all details of orders
            if (! $placed_header)
            {
                fputcsv( $fp, array_keys( $row ) );
                $placed_header = true;
            }

            // If row set, write it to CSV
            if ( count( $row ) != 0 )
                fputcsv( $fp, $row );
        }
    } else {
        fclose( $fp );
        unlink("$path/$breakdown_filename");
        error_log( "Auto-report: CSV Breakdown - failed to open CSV" );
    }

    notify( $breakdown_filename, $path, 'Download Order Details Export', 'detailed', $params );

    fclose($fp);
    unlink("$path/$filename");

    $totals_filename = "company-{$period['type']}-orders-totals.csv";
    $fp       = fopen("$path/$totals_filename", 'w');

    if ( fopen( "$path/$totals_filename", "w+" ) ) {
        // Send totals email
        fputcsv( $fp, [ "Orders - {$date_summary}"] );
        fputcsv( $fp, [ 'SKU', 'Name', 'Totals' ] );

        // Output two column totals ( name | total )
        foreach( $totals as $name => $details ) {
            $num = $details[0];
            $sku = $details[1];
            /*
                Excel doesn't like some of the ASCII characters, use 7 bit ASCII
                https://stackoverflow.com/questions/1176904/php-how-to-remove-all-non-printable-characters-in-a-string
             */
            fputcsv( $fp, [ $sku, $name, $num ]);
        }

    } else {
        fclose( $fp );
        unlink("$path/$totals_filename");
        error_log( "Auto-report: Totals CSV - Failed to open CSV" );
    }

    // Send break down CSV
    notify( $totals_filename, $path, 'Download Totals Export', 'totals for', $params );

    fclose($fp);
    unlink("$path/$totals_filename");

    return;
}

function sort_orders_by_date( $results, $order = 'DESC' ) {
    foreach ($results as $key => $part) {
         $sort[$key] = strtotime($part['order_date']);
    }

    ( $order == 'DESC' )
        ? array_multisort( $sort, SORT_DESC, $results )
        : array_multisort( $sort, SORT_ASC, $results );

    return $results;
}

function sort_orders_by( $field, $order = 'DESC', $results ) {
    if ( count( $results ) > 0 )
    {
        foreach ($results as $key => $part) {
             $sort[$key] = $part[ $field ];
        }

        ( $order == 'DESC' )
            ? array_multisort( $sort, SORT_DESC, $results )
            : array_multisort( $sort, SORT_ASC, $results );
    }

    return $results;
}

function sanitize_get() {
     /*---------------------------------------------
             Filter / Sanitize Dates
     --------------------------------------------- */
     if ( isset( $_GET['period']))
     {
         $period['all'] = 0;

         switch( $_GET['period'])
         {
             case( 'week' ):
                 $period['type'] = 'weekly';
                 $previous_week = strtotime("-1 week +1 day");

                 $start_week    = strtotime( "last sunday", $previous_week );
                 $end_week      = strtotime( "next saturday", $start_week );

                 $period['range']['start_date'] = filter_var( date( "Y-m-d", $start_week ), FILTER_SANITIZE_STRING );
                 $period['range']['end_date']   = filter_var( date( "Y-m-d", $end_week), FILTER_SANITIZE_STRING );
             break;
             case( 'month' ):
                 $period['type'] = 'monthly';

                 $period['range']['start_date'] = date('Y-m-d',strtotime('first day of last month'));
                 $period['range']['end_date']   = date('Y-m-d',strtotime('last day of last month'));
             break;
             case( 'year'):
                 $year = date( 'Y' ) - 1;

                 $period['type'] = 'yearly';
                 $period['range']['start_date'] = "{$year}-01-01";
                 $period['range']['end_date']   = "{$year}-12-31";
             break;
             case( 'forever' ):
                 $period['type'] = 'full';
                 $period[ 'all' ] = 1;
             break;
         }
     }
     else
     {
         error_log( 'Autoexport: no period given to auto export');
         exit;
     }

     /*---------------------------------------------
             Filter / Sanitize Categories
      ---------------------------------------------*/
     if ( isset( $_GET['filter_categories']))
     {
        $categories = [];
        $categories['all'] = 0;

         if ( $_GET['filter_categories'] == 'all' )
         {
             $categories['all'] = 1;
         }
         else
         {
             $categories['list'] = explode( ',', $_GET['filter_categories']);

             foreach( $categories['list'] as $i => $cat )
             {
                 $categories['list'][$i] = str_replace( '-', ' ', strtolower( filter_var( $cat, FILTER_SANITIZE_STRING ) ) );
             }


             if ( $categories['all'] === 0 && count( $categories['list'] ) < 1 )
                 error_log( 'Empty value for categories was passed' );
         }
     }
     else
     {
         error_log( 'Autoexport: Empty value for categories was passed' );
         exit;
     }

     /*---------------------------------------------
             Filter / Sanitize Business Units
      ---------------------------------------------*/
     if ( isset( $_GET['filter_units']) )
     {
         if ( $_GET['filter_units'] == 'all' )
         {
             $units['all'] = 1;
         }
         else
         {
             $units = [];
             $units['list'] = explode( ',', $_GET['filter_units']);

             foreach( $units['list'] as $i => $unit )
             {
                 $units['list'][$i] = str_replace( '-', ' ', strtolower( filter_var( $unit, FILTER_SANITIZE_STRING ) ) );
             }

             if ( $units['all'] == 0 && count( $units['list'] ) < 1 )
                 error_log( 'Empty value for business units was passed' );
         }
     }
     else
     {
         error_log( 'Autoexport: Empty value for business units was passed' );
         exit;
     }

     /*---------------------------------------------
             Filter / Sanitize Skus
      ---------------------------------------------*/
     if ( isset( $_GET['filter_skus']) )
     {
         $skus = [];

         if ( $_GET['filter_skus'] == 'all' )
         {
             $skus['all'] = 1;
         }
         else
         {
             $skus['list'] = explode( ',', $_GET['filter_skus'] );

             // Filter unit string and push it to the search list
             foreach( $skus['list'] as $i => $sku )
             {
                 $skus['list'][$i] = str_replace( '-', ' ', filter_var( $sku, FILTER_SANITIZE_STRING ) );
             }

             if ( $skus['all'] === 0 && count( $skus['list'] ) < 1 )
                 error_log( 'Autoexport: Empty value for skus was passed' );
         }
     }
     else
     {
         error_log( 'Autoexport: Empty value for skus was passed' );
         exit;
     }

      /*---------------------------------------------
              Filter / Sanitize Emails
      --------------------------------------------- */
      // Optionally pass in string of double pipe separated emails to send to
      if ( isset( $_GET['email_list']))
      {
          $email_list = explode( ',', $_GET['email_list'] );

          foreach ( $email_list as $i => $email )
          {
              $email_list[ $i ] = filter_var( $email, FILTER_SANITIZE_EMAIL );
          }

          $email_list = implode( ',', $email_list );
      }
      else
      {
          error_log( 'Autoexport: email given to auto export');
          exit;
      }

     $search_data = [
         'validated'  => 1,
         'period'     => $period,
         'categories' => $categories,
         'units'      => $units,
         'skus'       => $skus,
         'email_list' => $email_list,
     ];

     if ( $search_data['validated'] != 1 )
         die( 'Sorry, the data you entered is invalid' );

     return $search_data;
}

/*
 More info on wc_order arguments
 https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query
*/
function set_order_args( $search_data ) {
    $period = $search_data['period'];

    $args = [
     // 'status' => [ 'completed' ],
     'status' => 'completed',
     'order_by' => 'date',
     'order' => 'DESC',
    ];

    if ( isset( $period['range'] ) ) {
        if ( isset( $period['range']['start_date']) && isset( $period['range']['end_date'])) {
            $args['date_before'] = $period['range']['end_date'];
            $args['date_after']  = $period['range']['start_date'];
            // Old way WC worked
            // WC date range format (2018-08-01...2018-08-31)
            // $range = implode( '...', $period['range'] );
            // $args[ 'date_created'] = $range;
        }
    }

    return $args;
}

function get_term_names() {
    $tags      = get_terms( array('product_tag'), "hide_empty=1" );
    $tag_list = [];

    if ( ! empty( $tags ) && ! is_wp_error( $tags ) ){
        foreach ( $tags as $tag ) {
            $tag_list[] = $tag->name;
        }
    }

    return $tag_list;
}

/*
 Read more here:
 https://stackoverflow.com/questions/21858431/woocommerce-get-a-list-for-all-sku-product
*/
function get_sku_list() {
    $skus = [];
    $args = array( 'post_type' => 'product', 'posts_per_page' => -1 );

    query_posts( $args );

    if( have_posts() ):
        while ( have_posts() ) : the_post();
            global $post;
            $product = wc_get_product($post->ID);
            $sku = $product->get_sku();

            if ( $sku != '')
            {
                array_push( $skus, $sku);
            }
        endwhile;
    endif;

    return $skus;
}

function get_tag_name( $product_id ) {
    $tags = wp_get_post_terms( $product_id, 'product_tag' );

    if ( count($tags) > 0 ) {
        foreach ( $tags as $tag ) {
            return $tag->name;
        }
    } else {
        return 'blank';
    }
}

function check_against_list( $data, $product, $property ) {
    $push = (int) true;

    if ( $data['all'] == 0 ) {
        if ( is_array( $data['list'])) {
            if ( ! in_array( htmlspecialchars_decode( $product['tag']), $data['list'] ) ) {
                $push = 0;
            }
        } else {
            $push = 0;
        }
    }

    return $push;
}

function sum($row, $totals) {
    (empty( $totals[$row['product_name' ]] ))
        ? $totals[$row['product_name' ]] = [(int) 1, $row['sku']]
        : $totals[$row['product_name' ]][0]++;

    return $totals;
}

function check_in_range( $start_date, $end_date, $order_date ) {
    // Convert to timestamp
    $start_ts = strtotime( $start_date );
    $end_ts   = strtotime( $end_date );
    $user_ts  = strtotime( $order_date );

    // Check that user date is between start & end
    return (( $user_ts >= $start_ts) && ( $user_ts <= $end_ts ));
}

// EMAIL
function mail_attachment($filename, $path, $mailto,
                        $from_mail, $from_name, $replyto,
                        $subject, $message)
{
    $file = $path."/".$filename;
    $file_size = filesize($file);
    $handle = fopen($file, "r");
    $content = fread($handle, $file_size);
    fclose($handle);
    $content = chunk_split(base64_encode($content));
    $uid = md5(uniqid(time()));
    $header = "From: ".$from_name." <".$from_mail.">\r\n";
    $header .= "Reply-To: ".$replyto."\r\n";
    $header .= "MIME-Version: 1.0\r\n";
    $header .= "Content-Type: multipart/mixed; boundary=\"".$uid."\"\r\n";

    $my_message = $message;

    $message  = "";
    $message .= "This is a multi-part message in MIME format.\r\n";
    $message .= "--".$uid."\r\n";
    $message .= "Content-type:text/plain; charset=iso-8859-1\r\n";
    $message .= "Content-Transfer-Encoding: 7bit\r\n\r\n";
    $message .= $my_message."\r\n\r\n";
    $message .= "--".$uid."\r\n";
    $message .= "Content-Type: application/octet-stream; name=\"".$filename."\"\r\n"; // use different content types here
    $message .= "Content-Transfer-Encoding: base64\r\n";
    $message .= "Content-Disposition: attachment; filename=\"".$filename."\"\r\n\r\n";
    $message .= $content."\r\n\r\n";
    $message .= "--".$uid."--";

    mail($mailto, $subject, $message, $header);
}

function notify( $filename, $path,
                $subject = 'Export',
                $report_type, $params)
{
    $subject = $params['period']['type'] . ' Export';
    $recipients = $params['search_data']['email_list'];

    $sender_address = "web@thecompanydomain.com";
    $sender_name = "Company Web site";
    $reply_to = "web@thecompanydomain.com";
    $subject = "Brand $subject";
    $message = "Attached is the list of $report_type orders.";


    mail_attachment( $filename, $path, $recipients,
                    $sender_address, $sender_name,
                    $reply_to, $subject, $message);

    return 0;
}

if( !function_exists('pre')){
    function pre($var = '') {
        echo "<pre>";
        print_r( $var );
        echo "</pre><br>\n";
    }
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.