Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    primarykey
    data
    text
    <p><strong>Lesson 1</strong>: Don't use PHP's superglobals</p> <ul> <li><code>$_POST</code> = <code>$this-&gt;params['form'];</code></li> <li><code>$_GET</code> = <code>$this-&gt;params['url'];</code></li> <li><code>$_GLOBALS</code> = <code>Configure::write('App.category.variable', 'value');</code></li> <li><code>$_SESSION</code> (view) = <code>$session-&gt;read();</code> (helper)</li> <li><code>$_SESSION</code> (controller) = <code>$this-&gt;Session-&gt;read();</code> (component)</li> <li><code>$_SESSION['Auth']['User']</code> = <code>$this-&gt;Auth-&gt;user();</code></li> </ul> <p>Replacements for <code>$_POST</code>: </p> <pre><code>&lt;?php ... //foreach ($_POST as $key =&gt; $value) { foreach ($this-&gt;params['form'] as $key =&gt; $value) { ... //if (isset($_POST['test_ipn'])) { if (isset($this-&gt;params['form']['test_ipn'])) { ... ?&gt; </code></pre> <p><strong>Lesson 2</strong>: Views are for sharing (with the user)</p> <p>Code documented "Compiles premium info and send the user to Paypal" doesn't send the user to PayPal. Are you redirecting in the view?</p> <pre><code>&lt;?php function redirect($premiumId) { ... $this-&gt;redirect($url . '?' . http_build_query($paypalData), 303); } </code></pre> <p>Redirect at the end of your controller and delete the view. :)</p> <p><strong>Lesson 3</strong>: Data manipulation belongs in model layer</p> <pre><code>&lt;?php class PremiumSite extends AppModel { ... function beforeSave() { if ($this-&gt;data['PremiumSite']['type'] == "1") { $cost = Configure::read('App.costs.premium'); $numberOfWeeks = ((int) $this-&gt;data['PremiumSite']['length']) + 1; $timestring = String::insert('+:number weeks', array( 'number' =&gt; $numberOfWeeks, )); $expiration = date('Y-m-d H:i:s', strtotime($timestring)); $this-&gt;data['PremiumSite']['upfront_weeks'] = $weeks; $this-&gt;data['PremiumSite']['upfront_expiration'] = $expiration; $this-&gt;data['PremiumSite']['cost'] = $cost * $numberOfWeeks; } else { $this-&gt;data['PremiumSite']['cost'] = $cost; } return true; } ... } ?&gt; </code></pre> <p><strong>Lesson 4</strong>: Models aren't just for database access</p> <p>Move code documented "Enables premium site after payment" to PremiumSite model, and call it after payment:</p> <pre><code>&lt;?php class PremiumSite extends AppModel { ... function enable($id) { $transaction = $this-&gt;find('first', array( 'conditions' =&gt; array('PaypalNotification.id' =&gt; $id), 'recursive' =&gt; 0, )); $transactionType = $transaction['PaypalNotification']['txn_type']; if ($transactionType == 'subscr_signup' || $transaction['PaypalNotification']['payment_status'] == 'Completed') { //New subscription or payment ... } elseif ($transactionType == 'subscr-cancel' || $transactionType == 'subscr-eot') { //Subscription cancellation or other problem ... } return $this-&gt;saveAll($data); } ... } ?&gt; </code></pre> <p>You would call from controller using <code>$this-&gt;PaypalNotification-&gt;PremiumSite-&gt;enable(...);</code> but we aren't going to do that, so let's mix it all together...</p> <p><strong>Lesson 5</strong>: Datasources are cool</p> <p>Abstract your PayPal IPN interactions into a datasource which is used by a model.</p> <p>Configuration goes in <code>app/config/database.php</code></p> <pre><code>&lt;?php class DATABASE_CONFIG { ... var $paypal = array( 'datasource' =&gt; 'paypal_ipn', 'sandbox' =&gt; true, 'api_key' =&gt; 'w0u1dnty0ul1k3t0kn0w', } ... } ?&gt; </code></pre> <p>Datasource deals with web service requests (<code>app/models/datasources/paypal_ipn_source.php</code>)</p> <pre><code>&lt;?php class PaypalIpnSource extends DataSource { ... var $endpoint = 'http://www.paypal.com/'; var $Http = null; var $_baseConfig = array( 'sandbox' =&gt; true, 'api_key' =&gt; null, ); function _construct() { if (!$this-&gt;config['api_key']) { trigger_error('No API key specified'); } if ($this-&gt;config['sandbox']) { $this-&gt;endpoint = 'http://www.sandbox.paypal.com/'; } $this-&gt;Http = App::import('Core', 'HttpSocket'); // use HttpSocket utility lib } function validate($data) { ... $reponse = $this-&gt;Http-&gt;post($this-&gt;endpoint, $data); .. return $valid; // boolean } ... } ?&gt; </code></pre> <p>Let the model do the work (<code>app/models/paypal_notification.php</code>)</p> <p>Notifications are only saved if they are valid, sites are only enabled if the notification is saved </p> <pre><code>&lt;?php class PaypalNotification extends AppModel { ... function beforeSave() { $valid = $this-&gt;validate($this-&gt;data); if (!$valid) { return false; } //Minor change to use item_id as premium_site_id $this-&gt;data['PaypalNotification']['premium_site_id'] = $this-&gt;data['PaypalNotification']['item_number']; /* $this-&gt;data['PaypalNotification'] = am($this-&gt;data, // use shorthand functions array('premium_site_id' =&gt; $this-&gt;data['item_number'])); */ return true; } ... function afterSave() { return $this-&gt;PremiumSite-&gt;enable($this-&gt;id); } ... function validate($data) { $paypal = ConnectionManager::getDataSource('paypal'); return $paypal-&gt;validate($data); } ... ?&gt; </code></pre> <p>Controllers are dumb. (<code>app/controllers/paypal_notifications_controller.php</code>)</p> <p>"Are you a post? No? .. then I don't even exist." Now this action just shouts, "I save posted PayPal notifications!"</p> <pre><code>&lt;?php class PaypalNotificationsController extends AppModel { ... var $components = array('RequestHandler', ...); ... function callback() { if (!$this-&gt;RequestHandler-&gt;isPost()) { // use RequestHandler component $this-&gt;cakeError('error404'); } $processed = $this-&gt;PaypalNotification-&gt;save($notification); if (!$processed) { $this-&gt;cakeError('paypal_error'); } } ... } ?&gt; </code></pre> <p><strong>Bonus Round</strong>: Use provided libraries instead of native PHP</p> <p>Refer to previous lessons for examples of the following:</p> <ul> <li><code>String</code> instead of <code>sprintf</code></li> <li><code>HttpSocket</code> instead of <code>fsock</code> functions</li> <li><code>RequestHandler</code> instead of manual checks</li> <li><code>am</code> instead of <code>array_merge</code></li> </ul> <p>These can prevent coding errors, reduce amount of code and/or increase readability.</p>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. VO
      singulars
      1. This table or related slice is empty.
    2. VO
      singulars
      1. This table or related slice is empty.
    3. VO
      singulars
      1. This table or related slice is empty.
    1. COFirst of all, thank you for that excellent post, you have given me a lot to think about. Lession 1: The process() section wasn't written by me, I just made a few changes. I thought it was strange that the person used $_POST rather than $this->data, I will probably rewrite it. Lession 2: Yes, it does redirect the user in the view although not a standard redirect. Take a look at my original post. Lesson 3: I agree, Plus your code is much more elegant then how I would've done it. Lession 4: I will start to use models more, it definately slims down the controller and reads easier.
      singulars
    2. COHit the character limit and the loss of formatting makes it a little hard to read. Lession 5: PayPal IPN really a regular api, the data is sent along with the user which may make using a DataSource difficult to implement. I know the code in the view is quite bad, I couldn't find any other way to accomplish this. Bonus: I really must properly look though the CakePHP api, I've experimented with the HttpSocket component but not the RequestHandler. Thanks again for all your help
      singulars
    3. COThank you for providing good example code, I actually enjoyed thinking about how one might refactoring it. :) In response to your points: 1. I have updated this to $this->params['form'] as $this->data only applies to data submitted by forms created with Cake's FormHelper; 2. I would test if GET works here since the amount of data isn't excessive. If PayPal only accepts POST then I can only suggest you add a submit button for those without JavaScript enabled; 3/4. This has been coined as the "Fat model, skinny controller" methodology by someone, in case you need to put it into a few words;
      singulars
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload