Saturday, October 6, 2012

Using Twilio for SMS with PHP


Twilio provides a relatively painless SMS interface.  But the PHP documentation is fragmented and takes some effort to aggregate.  So, to make it easier for others, I threw together a quick outline of how you go about implementing a Twilio interface in PHP. The send interface is very simple:

   const SMS_PREFIX =     '+1';
   const MAX_SEND_CHARS = 160;
   const TWILIO_SMS_APP_ID = '15 digit appId';
   const TWILIO_SMS_APP_SECRET = '32 character app Secret';

    /**
     *send an SMS message
     *
     * @param string $smsNum
     * @param string $smsBody
     *
     * @return string
     */   
   public function sendSms($smsNum, $smsBody) {
     
      $cliTwilio = new Services_Twilio(self::TWILIO_SMS_APP_ID,       
                                       self::TWILIO_SMS_APP_SECRET);
      $smsNum = self::SMS_PREFIX . $smsNum;
      $smsFrom = self::SMS_PREFIX . $this->twilioNumber;
      if (strlen($smsBody) > self::MAX_SEND_CHARS) {
         throw new Exception ('Error: attempting to send message of length (' .    
                            strlen($smsBody) . ') but max allowed chars is (' .
                            self::MAX_SEND_CHARS . ') for SMS number: ' . $smsNum);
      } else {
         $smsResponse = $cliTwilio->account->sms_messages->create($smsFrom, $smsNum,     
                               $smsBody);
         if ($smsResponse->sid != null) {
            return $smsResponse->sid;
         } else {
            throw new Exception ('Error: SMS send returned invalid (NULL) sid for ' .
                                 'sms number: ' . $smsNum);
         }
      }
   }


The “Services_Twilio” class referenced here is in the Twilio library provided by Twilio.  You need to download it from Twilio.

To be able to receive SMS messages, you need to go the Twilio website, login, select “Numbers”, and click into the number you have setup for your Twilio account.  Go to the SMS section, and set the “SMS Request URL” to point to your receiving web page (https is recommended but not required to make it work which is handy for testing).  The receiving web page on your site needs something similar to the following in it:

class SMSMessageParser {
  
   /**
    * Parse the incoming SMS message
    */
   public function parse() {
     
      if(!isset($_SERVER['HTTPS']) && $this->config->twilio->useSSL !="NO") {             
         header("HTTP/1.1 301 Moved Permanently");
         header("Location: https://" . $_SERVER["SERVER_NAME"] .  
                                       $_SERVER["REQUEST_URI"]);
         exit();
      }

      if (empty($_REQUEST)) {
         die();
      } else {
         $smsNum = substr($_REQUEST['From'],-10);
         $smsMsg = $_REQUEST['Body'];
         try {
            $smsService = new SmsService();
            $response = $smsService->parseSms($smsNum, $smsMsg);
         } catch (Exception $e) {
            $response = "We are sorry, but we experienced an unexpected error " .
  "processing your request.";
         }
         header("content-type: text/xml");
         echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
         echo "<Response>";
         echo "<Sms>" . $response . "</Sms>";
         echo "</Response>";
         exit;
      }
   }
}


$parser = new SMSMessageParser();
$parser->parse();

And finally, you need a class to process the incoming text:

class SmsService {

   private $smsReservedVerbs = array('stop','unsubscribe','cancel','quit',
                                     'start','yes','help');
  
   /**
    * parse incoming SMS message
    *
    * @param string $smsNum
    * @param string $smsMsg
    *
    * @return string
    */  
   public function parseSms($smsNum, $smsMsg) {
      $smsNum = trim($smsNum);
      $smsMsg = trim($smsMsg);
     
      if(strlen($smsNum) != 10) {
         return false;
      }
      if ($smsMsg == "Whatever you are looking for") {
         // Do something with message

      } else if (in_array(strtolower($smsMsg), $this->smsReservedVerbs)) {
         switch(strtolower($smsMsg)) {
            case "unsubscribe":
            case "stop":
            case "cancel":
            case "quit":
               // User unsubscribed so take appropriate action
               break;
            case "start":
            case "yes":
               // User resubscribed.  Act appropriately.
               break;
            case "help":  
               // send some sort of appropriate information.
               break;
            default:
               throw new Exception ('Error: We should not be able to reach ' .
                        'this statement.  Message was: ' . $smsMsg);
         }
      } else {
         $responseString = 'We are sorry.  Your message "' . $smsMsg . '" ' .
                           'was not understood.' ;
      }
      return $responseString;
   }
}

And that’s all there is to it. 

Sunday, September 2, 2012

Fix for Magento’s Shipping API’s “sales_order_shipment.list” call


When searching for shipments, the most likely parameter we’d want to search by is “order_increment_id” i.e. what shipments are attached to a given order.  However, this is not possible with the way the API is written.  If we attempt to do a search of the form:

$params = array(array('order_increment_id' => $value));
$shipments = $this->call('sales_order_shipment.list', $params);


we get back this error:

SQLSTATE[42S22]: Column not found: 1054 Unknown column 'order_increment_id' in 'where clause'

The reason for this lies in:
/core/Mage/Sales/Model/Order/Shipment/Api.php

This call does not work correctly:
$collection = Mage::getResourceModel('sales/order_shipment_collection')
   ->addAttributeToSelect('increment_id')
   ->addAttributeToSelect('created_at')
   ->addAttributeToSelect('total_qty')
   ->joinAttribute('shipping_firstname', 'order_address/firstname', 'shipping_address_id', null, 'left')
    ->joinAttribute('shipping_lastname', 'order_address/lastname', 'shipping_address_id', null, 'left')
    ->joinAttribute('order_increment_id', 'order/increment_id', 'order_id', null, 'left')
    ->joinAttribute('order_created_at', 'order/created_at', 'order_id', null, 'left');

If you go search for this on the net, you will find in various places where it says that “joinAttribute” will join the “order” table using the “using the “order_id” field as the join ON column and map “increment_id” to “order_increment_id”.   But after much effort trying to figure out what parameters you can pass to this call to access said “order_increment_id” field, you will eventually have to give up in frustration.  The reason for this lies in the “joinAttribute” function.

Here it is in all its glory
From:  app/code/core/Mage/Sales/Model/Mysql4/Collection/Abstract.php

/**
 * Backward compatibility with EAV collection
 * @todo implement join functionality if necessary
 */
 public function joinAttribute($alias, $attribute, $bind, $filter=null,
                                $joinType='inner', $storeId=null)
 {
    return $this;
 }

Note the “todo”.   This function currently does absolutely nothing but return “$this” so as to not break the chaining.  So if we want to be able to search for shipments by “order_increment_id”, we are going to have to modify either “joinAttribute” or the API. 

I opted to go the route of modifying the API.  By adding one line, we can join in the order shipment grid table ‘sales_flat_order_shipment_grid’ and pick up the field we are after.  But of course, we don’t want to modify the core file itself, so we move it to:
app/code/local/Mage/Sales/Model/Mysql4/Collection/Abstract.php first.

$collection = Mage::getResourceModel('sales/order_shipment_collection')
   ->addAttributeToSelect('increment_id')
   ->addAttributeToSelect('created_at')
   ->addAttributeToSelect('total_qty')
   ->join('sales/shipment_grid',
          '`main_table`.`increment_id`=`sales/shipment_grid`.`increment_id`',
          'order_increment_id')
   ->joinAttribute('shipping_firstname', 'order_address/firstname',
                   'shipping_address_id', null, 'left')
   ->joinAttribute('shipping_lastname', 'order_address/lastname',
                   'shipping_address_id', null, 'left')
   ->joinAttribute('order_increment_id', 'order/increment_id',
                   'order_id', null, 'left')
   ->joinAttribute('order_created_at', 'order/created_at', 'order_id', null, 'left');  


Note that I opted not to remove the useless “joinAttribute” method calls.  It will be easier to figure out what to do with this if and when Magento finally fixes this if we leave them in so it’s more obvious what we’ve done.