Thursday, January 22, 2009

Site Improvements

Last night I finished migrating www.corywiles.com to a complete Zend Framework MVC app.  Now that is done there should be significant site improvements mostly due to enhanced caching.

Saturday, January 10, 2009

PayPal Zend Framework Validator

I was presented with a project where the client wanted a form that would accept credit card payments via paypal.  I could either write it in PHP or Java. While I am a big fan of both languages I felt in this instance that PHP was the most time efficient route.   However, PayPal doesn't offer an API for PHP. Only Java and .NET.  As to not be discouraged I decided to spend a little time trying to come with a custom validator that would handle the validation of the credit card validation.

::Warning::
My use case was very specific.  I only needed to authorize credit card sales.  Basically their simpliest form of transations.

Since all the applications that I write now are us the Zend Framework I was able to reduce "inline-code" by creating a custom validator that I assign to my preValidation method inside of the model.  :: I worked on this concept with Jeremy Kendall ::

There is definitely room for improvement, but this is might be helpful to others.  For example, the paypal options should really not be declared as Zend_Config instance, but checked for an instance of Zend_config and then set as an array with the toArray method.


// Validator

/**
* @see Zend_Validate_Abstract
*/
require_once 'Zend/Validate/Abstract.php';

/**
* @see Zend_Http_Client
*/
require_once 'Zend/Http/Client.php';

class SJCRH_Validate_PayFlow extends Zend_Validate_Regex {

const CHARGE_FAILED = "ppChargeFailed";

const CHARGE_EXCEPTED_CODE = "0";

/**
* Error message to display to the user if the credit card transaction fails
*
* @access protected
* @var array
*/
protected $_messageTemplates = array(
self::CHARGE_FAILED => "There was an error processing your card. Error code: '%code%' Error message: '%msg%'"
);

/**
* @var array
*/
protected $_messageVariables = array(
'code' => '_code',
'msg' => '_msg'
);

/**
* Error Code value
*
* @var mixed
*/
protected $_code;

/**
* Error Message
*
* @param Zend_Config $options
*/
protected $_msg;


/**
* CC validation using PayFlow
*
* @param Zend_Config $options
* @param string $ccnum
* @param string $exp
* @param array $billing
* @param string $amount
* @param string $country
*/
public function __construct(Zend_Config $paypaloptions, $ccnum, $amount, $exp, $billing = array(), $country = 'US') {

if (!$paypaloptions instanceof Zend_Config) {
throw new Exception("Options must be an instance of Zend Config");
} else {
$options = $paypaloptions->toArray();
}

/**
* Random string used to error checking with payflow - DUPLICATION TRANSACTIONS
*/
$requestId = md5(date('YmdGis'));

$zfClient = new Zend_Http_Client($options['url']);

$zfClient->setHeaders(array('X-VPS-Request-ID' => $requestId));

/**
* Recommended from their documentation to change the timeout from 30 seconds (default)
* to 45
*/
$zfClient->setConfig(array('timeout' => 45));

$zfClient->setMethod(Zend_Http_Client::POST);

$zfClient->setParameterPost(array(
'USER' => $options['username'],
'VENDOR' => $options['vendor'],
'PARTNER' => $options['partner'],
'PWD' => $options['password'],
'FIRSTNAME' => $billing['firstname'],
'LASTNAME' => $billing['lastname'],
'STREET' => $billing['street'],
'ZIP' => $billing['zip'],
'TENDER' => 'C',
'TRXTYPE' => 'S',
'ACCT' => $ccnum,
'EXPDATE' => $exp,
'AMT' => $amount,
'CURRENCY' => 'USD',
'COUNTRY' => $country,
'CLIENTIP' => $_SERVER['REMOTE_ADDR'],
'VERBOSITY' => 'MEDIUM'
));

/**
* The response key/value pairs are seperated by ampersands.
*
* I extract the RESULT and RESPMSG
*
* If anything but a RESULT=0 is found then the code and error message are
* set to the validator's variables to display to the form user.
*/

$payFlowResponse = explode("&", $zfClient->request()->getBody());
$regex = "/(\w*)=(.*)/i";

@preg_match($regex, $payFlowResponse[0], $matches);

$this->setCode($matches[2]);

if ($this->getCode() !== self::CHARGE_EXCEPTED_CODE) {
@preg_match($regex, $payFlowResponse[2], $matches);
$this->setMsg($matches[2]);
}
}

public function isValid($value) {

$this->_setValue($value);

if ($this->getCode() !== self::CHARGE_EXCEPTED_CODE) {
$this->_error();
return false;
}

return true;
}

public function setCode($code) {
$this->_code = $code;
}

public function setMsg($msg) {
$this->_msg = $msg;
}

public function getCode() {
return $this->_code;
}

public function getMsg() {
return $this->_msg;
}
}

Using Zend View for Email Message Body in Your Model

Lately in the PHP/Zend Framework blog world there has been much discussion concerning what constitutes a model in an MVC app.  In the current implementation of the MVC apps at work our ZF Form implementations are processed in a corresponding model class as well as a 'notify' method which handles emailing a response after successful submission.  I was able to abstract all aspects of the email properties except for the message body.

Most of the time the requirement is to have, basically, the same form view but with the values populate.  That usually means creating some long and kludgy looking heredoc or worse a huge string of crazy html intermingled with escapes.  Neither solution was very appealing to me.  I kept thinking more and more that the email body was really a view.  So I treated as such.  So my solution was to create a global views directory and a view script that was the email body template and passed it's render method to Zend_Mail instance.

This allows for further separation of models and views.  I know that technically the email body should be apart of the model, but far too many times I have to change how the body of the mail looks, not the data, so it lends itself to more of a view.

Please feel free to comment.

::NOTE::
Forms as model architecture taken from http://weierophinney.net/matthew/archives/200-Using-Zend_Form-in-Your-Models.html

//Example Code
private function _notifyGroup() {
 
  $data = array();
  $data = $this->getForm()->getValues();
 
  /**
   *  Setup view instance to pass to notifier
   */
  $view = new Zend_View();

  /**
   * Assign all form data to view property
   */
  $view->assign("formvalues", $data);

 
 /**
   * Location of view scripts
   */
  $view->addScriptPath(dirname(__FILE__).'/../../../../views');

  $body = $view->render('email-templates/mir.phtml');

  /**
   *  Custom notify class that abstracts different 'notify' methodologies.  For example you can notfiy
   *  by writing to a log or email.  Email can be Zend_Mail, PEAR or plain ol' php mail() 
   */
  $notifier = CW_Notify::factory('mail_zend', $body, $options['email']);

  $notifier->notify();

}