Skip to content

Instantly share code, notes, and snippets.

@jmc734
Created August 12, 2012 01:19
Show Gist options
  • Save jmc734/3328652 to your computer and use it in GitHub Desktop.
Save jmc734/3328652 to your computer and use it in GitHub Desktop.
Shopify Batch Discount Automation (Create, Enable/Disable, Delete)
<?php
/**
* Discount
*
* Create, modify, and delete Shopify discounts
*
* PHP version 5
*
* @author Jacob McDonald <jmc734@gmail.com>
*/
class BatchDiscount {
private $base;
private $login;
private $password;
private $curl;
private $authToken;
/**
* Constructor
*
* initialize Shopify session
*
* @param string $login email used to login to Shopify
* @param string $password password used to login to Shopify
* @param string $base admin panel URL (e.g. https://test-store.myshopify.com/admin)
* @param string $cookies filepath to store cookies in
*
* @return boolean true if successfully logged in, else, exception thrown
* @access public
*/
public function __construct($login, $password, $base, $cookies) {
// Set properties
$this->login = $login;
$this->password = $password;
$this->base = $base;
$this->cookies = $cookies;
// Add login and password to post
$post = array(
'login' => $login,
'password' => $password
);
// Send login request to Shopify
$ch = $this->initCurl();
curl_setopt($ch, CURLOPT_URL, $base.'/auth/login');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
curl_exec($ch);
// Check if CURL returned an error
if(curl_errno($ch) !== 0){
$exception = new Exception('Login Request Failed: '.curl_error($ch).' ('.curl_errno($ch).')');
curl_close($ch);
throw $exception;
}
// Check if HTTP error returned
if(curl_getinfo($ch, CURLINFO_HTTP_CODE) !== 200){
curl_close($ch);
throw new Exception('HTTP Error: '.curl_getinfo($ch, CURLINFO_HTTP_CODE));
}
// Check if login was not successful
if(curl_getinfo($ch, CURLINFO_EFFECTIVE_URL) !== $base){
curl_close($ch);
throw new Exception('Login Failure: Wrong email or password');
}
curl_close($ch);
return true;
}
/**
* Create
*
* create new discounts
*
* @param array $codes codes for the new discounts, array of strings
* @param string $discountType type of discount (fixed_amount, percentage, or shipping)
* @param int,float $value value of discount ($ or %). only for fixed_amount or percentage
* @param string $start activation date (YYYY-MM-DD). leave blank to activate immediately
* @param string $end deactivation date (YYYY-MM-DD). leave blank to keep alive until manual deactivation
* @param int $usageLimit number of uses allowed. pass empty string for unlimited
* @param string $resourceType type of resource that the discount is applied to (minimum_order_amount, custom_collection, product, customer_group). leave blank to apply to all orders
* @param int,float $minOrderAmount minimum order value required to apply discount (only if $resourceType is minimum_order_amount)
* @param string $resourceId ID of the resource the discount applies to
*
* @return string ID of new discount if discount created successfully, else, exception thrown
* @access public
*/
public function create($codes, $discountType, $value, $start = '', $end = '', $usageLimit = '', $resourceType = '', $minOrderAmount = 0, $resourceId = '') {
$mh = curl_multi_init();
$handles = array();
for($i=0; $i<count($codes); $i++){
$code = $codes[$i];
// Check for valid Resource Type and corresponding argument (Minimum Order Amount, Resource ID)
switch($resourceType){
case(''): break;
case('minimum_order_amount'):
if($minOrderAmount <= 0){
throw new Exception('Argument Error: Must supply minimum order amount');
}
break;
case('custom_collection'):
case('product'):
case('customer_group'):
if($resourceId === ''){
throw new Exception('Argument Error: Must supply an Resource ID');
}
break;
default:
throw new Exception('Argument Error: Invalid Resource Type supplied');
}
// Build post string
$post = 'utf8=%E2%9C%93&authenticity_token='.$this->getAuthenticityToken().'&discount%5Bcode%5D='.$code.'&discount%5Bdiscount_type%5D='.$discountType.'&discount%5Bvalue%5D='.$value.'&discount%5Bapplies_to_resource%5D='.$resourceType.'&discount%5Bminimum_order_amount%5D='.$minOrderAmount.'&applies_to_product='.$resourceId.'&applies_to_collection='.$resourceId.'&applies_to_customer_group='.$resourceId.'&discount%5Bstarts_at%5D='.$start.'&discount%5Bends_at%5D='.$end.'&discount%5Busage_limit%5D='.$usageLimit.'&commit=Create%20discount&page=1';
// Send request to Shopify to create Discount
$ch = $this->initCurl();
curl_setopt($ch, CURLOPT_URL, $this->base.'/discounts');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
// Add cURL handle to curl_multi handle
curl_multi_add_handle($mh,$ch);
// Add reference to cURL handle to array with the associated code as the key, used to get the Shopify ID later
$handles[$code] = &$ch;
}
// Wait for all requests to finish
$running = null;
do {
curl_multi_exec($mh,$running);
} while($running > 0);
// Extract new Discount ID from returns
// TODO: Current method of extracting ID is terrible (unreliable). Multiple requests come back with the same ID.
// Somehow find the current code in the response then find and extract the associated ID
$return = array();
$match = array();
foreach($handles as $code=>$ch){
preg_match('@discount-([^"]*)"@', curl_multi_getcontent($ch), $match);
print_object(curl_multi_getcontent($ch).'<br/><br/><br/><br/>');
if(count($match) == 2){
$return[$code] = $match[1];
}
curl_multi_remove_handle($mh, $ch);
}
curl_multi_close($mh);
return $return;
}
/**
* Delete
*
* delete existing discounts
*
* @param array $ids IDs of the discounts to delete
*
* @return array IDs of discounts that could not be modified
* @access public
*/
public function delete($ids){
return $this->modify($ids, 'authenticity_token='.$this->getAuthenticityToken().'&_method=delete');
}
/**
* Disable
*
* disable existing discounts
*
* @param array $ids IDs of the discounts to disable
*
* @return array IDs of discounts that could not be modified
* @access public
*/
public function disable($ids){
return $this->modify($ids, 'authenticity_token='.$this->getAuthenticityToken(), '/disable');
}
/**
* Enable
*
* enable existing discounts
*
* @param array $ids IDs of the discounts to disable
*
* @return array IDs of discounts that could not be modified
* @access public
*/
public function enable($ids){
return $this->modify($ids, 'authenticity_token='.$this->getAuthenticityToken(), '/enable');
}
/**
* Destructor
*
* kill Shopify session
*
* @access public
*/
public function __destruct() {
$ch = $this->initCurl();
curl_setopt($ch, CURLOPT_URL, $this->base.'/auth/logout');
curl_exec($ch);
curl_close($ch);
}
/**
* Modify
*
* modify existing discounts
*
* @param string $uri string to append to URL
* @param string $post post string to send in request
*
* @return string response from Shopify if successful, else, exception thrown
* @access private
*/
private function modify($ids, $post, $append = ''){
$mh = curl_multi_init();
$handles = array();
foreach($ids as $id){
$ch = $this->initCurl();
curl_setopt($ch, CURLOPT_URL, $this->base.'/discounts/'.$id.$append);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
curl_multi_add_handle($mh,$ch);
$handles[$id] = $ch;
}
$running = null;
do {
curl_multi_exec($mh,$running);
} while($running > 0);
$failed = array();
foreach($handles as $id=>$ch){
if(!preg_match('@Messenger.notice\("([^"]*)"@', curl_multi_getcontent($ch))){
$failed[] = $id;
}
curl_multi_remove_handle($mh, $ch);
}
curl_multi_close($mh);
return $failed;
}
/**
* Initialize cURL
*
* create a new cURL handle with some presets
*
* @return cURL handle initialized cURL handle
* @access private
*/
private function initCurl() {
if(empty($this->curl)){
$options = array(
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_COOKIEJAR => $this->cookies,
CURLOPT_COOKIEFILE => $this->cookies,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_RETURNTRANSFER => true
);
$this->curl = curl_init();
curl_setopt_array($this->curl, $options);
}
return curl_copy_handle($this->curl);
}
/**
* Get Authenticity Token
*
* get token required in post to create/modify discounts
*
* @return string authenticity token
* @access private
*/
private function getAuthenticityToken() {
if(empty($this->authToken)){
// Load Promotion page
$ch = $this->initCurl();
curl_setopt($ch, CURLOPT_URL, $this->base.'/marketing');
curl_setopt($ch, CURLOPT_HEADER, false);
$response = curl_exec($ch);
curl_close($ch);
// Extract Authenticity Token from the response HTML
$match = array();
preg_match('@name="authenticity_token" type="hidden" value="([^"]*)"@', $response, $match);
if(count($match)<2){
throw new Exception('Parse Error: Could not extract Authenticity Token');
}
// Encode the token for passing in the post
$this->authToken = urlencode($match[1]);
}
return $this->authToken;
}
}
?>
@Eliasam13
Copy link

Hey Jacob! first I would like to thank you for taking the time to write this fine code which I've been desperately trying to find! All i need is just a quick example of how this works or an example of the function call with dummy data etc.. if you can just point me in the write direction I think I'll know what to do from there!

@jmc734
Copy link
Author

jmc734 commented Nov 4, 2012

  1. Create a BatchDiscount object. The arguments for the constructor are:
    • Your username and password for you Shopify storefront
    • The URL of your Shopify admin panel
    • A valid file location to store cookies in
$discount = new MassDiscount('username', 'password', 'https://test-store.myshopify.com/admin', dirname(__FILE__).'/cookies');
  1. Generate discount codes. These can be any string value, just make sure that there are no duplicates. Your going to want to save these to a file because there is no easy way to get them out of Shopify.
// Creates 100 6 hexadecimal digit value in UPPERCASE
// e.g. A5E484, C7EA80, 11734C
$codes = array();
for($i=0;$i<100;$i++){
    do {
        $code = strtoupper(substr(md5(rand()), 0, 6));
    } while(in_array($code, $codes));
    $codes[] = $code;
}
  1. Call create() method with your BatchDiscount object. The parameters include:
    • The discount codes you generated in Step 2
    • The discount type: fixed_amount, percentage, or shipping
    • The value of the discount ($ or %)
    • The start and end date of this discount
    • Number of uses allowed, pass empty string for unlimited uses
    • Resource type (leave blank to allow the discount to be applied to all orders). See Shopify discount form for explanation of this field
    • The minimum order amount
    • The ID of the resource
    The return value is an array of the IDs of the discounts. You should store these somewhere so that you can delete the discounts easily.
$fh = fopen('codes.csv', 'a');
// Create the discounts. For this example, single-use, $50 fixed amount for a minimum order value of $100.
$returns = $discount->create($codes, 'fixed_amount', 50, '', '', 1, 'minimum_order_amount', 100);
// Save these returned IDs to a CSV file
foreach($returns as $r){
    fputcsv($fh, $r);
}
fclose($fh);
  1. Additionally, you can disable, enable, and delete discount codes. Simply pass an array of IDs into the disable(), enable(), or delete() functions. If there were discount codes that could not be modified, their IDs will be returned.
$fh = fopen('codes.csv', 'r');
$temp = array();
// Read in all the IDs from the CSV
for($j=0;$j<100;$j++){
    $temp[] = fgetcsv($fh);
}
// Delete the discounts with those IDs
$discount->delete($temp);
fclose($fh);

Note: You may want to increase your PHP maximum execution time limit. Making all the calls to Shopify can take a while.

@dcworldwide
Copy link

@jmc734 Neat! But does this code still work? Don't you need a Shopify Plus account?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment