提高CakePHP中的代码质量

时间:2009-07-03 08:43:48

标签: php cakephp refactoring

我已经使用CakePHP几个星期了,这是一次很棒的体验。我设法以惊人的速度快速移植了一个网站,我甚至添加了一些我计划但从未实现过的新功能。

查看以下两个控制器,它们允许用户将优质状态添加到链接到其帐户的其中一个站点。他们感觉不是很'蛋糕',他们能以任何方式得到改善吗?

PremiumSites控制器处理注册过程,最终会有其他相关内容,例如历史记录。

class PremiumSitesController extends AppController {

    var $name = 'PremiumSites';

    function index() {
        $cost = 5;

        //TODO: Add no site check

        if (!empty($this->data)) {
            if($this->data['PremiumSite']['type'] == "1") {
                $length = (int) $this->data['PremiumSite']['length'];
                $length++;
                $this->data['PremiumSite']['upfront_weeks'] = $length;
                $this->data['PremiumSite']['upfront_expiration'] = date('Y-m-d H:i:s', strtotime(sprintf('+%s weeks', $length)));
                $this->data['PremiumSite']['cost'] = $cost * $length;
            } else {
                $this->data['PremiumSite']['cost'] = $cost;
            }

            $this->PremiumSite->create();
            if ($this->PremiumSite->save($this->data)) {
                $this->redirect(array('controller' => 'paypal_notifications', 'action' => 'send', $this->PremiumSite->getLastInsertID()));
            } else {
                $this->Session->setFlash('Please fix the problems below', true, array('class' => 'error'));
            }
        }

        $this->set('sites',$this->PremiumSite->Site->find('list',array('conditions' => array('User.id' => $this->Auth->user('id'), 'Site.is_deleted' => 0), 'recursive' => 0)));
    }

}

class PremiumSitesController extends AppController { var $name = 'PremiumSites'; function index() { $cost = 5; //TODO: Add no site check if (!empty($this->data)) { if($this->data['PremiumSite']['type'] == "1") { $length = (int) $this->data['PremiumSite']['length']; $length++; $this->data['PremiumSite']['upfront_weeks'] = $length; $this->data['PremiumSite']['upfront_expiration'] = date('Y-m-d H:i:s', strtotime(sprintf('+%s weeks', $length))); $this->data['PremiumSite']['cost'] = $cost * $length; } else { $this->data['PremiumSite']['cost'] = $cost; } $this->PremiumSite->create(); if ($this->PremiumSite->save($this->data)) { $this->redirect(array('controller' => 'paypal_notifications', 'action' => 'send', $this->PremiumSite->getLastInsertID())); } else { $this->Session->setFlash('Please fix the problems below', true, array('class' => 'error')); } } $this->set('sites',$this->PremiumSite->Site->find('list',array('conditions' => array('User.id' => $this->Auth->user('id'), 'Site.is_deleted' => 0), 'recursive' => 0))); } }

PaypalNotifications控制器处理与Paypal的交互。

class PaypalNotificationsController extends AppController {

    var $name = 'PaypalNotifications';

    function beforeFilter() {
        parent::beforeFilter();
        $this->Auth->allow('process');
    }

    /**
     * Compiles premium info and send the user to Paypal
     * 
     * @param integer $premiumID an id from PremiumSite 
     * @return null
     */
    function send($premiumID) {

        if(empty($premiumID)) {
            $this->Session->setFlash('There was a problem, please try again.', true, array('class' => 'error'));
            $this->redirect(array('controller' => 'premium_sites', 'action' => 'index'));
        }

        $data = $this->PaypalNotification->PremiumSite->find('first', array('conditions' => array('PremiumSite.id' => $premiumID), 'recursive' => 0));

        if($data['PremiumSite']['type'] == '0') {
            //Subscription
            $paypalData = array(
                'cmd' => '_xclick-subscriptions',
                'business'=> '',
                'notify_url' => '',
                'return' => '',
                'cancel_return' => '',
                'item_name' => '',
                'item_number' => $premiumID,
                'currency_code' => 'USD',
                'no_note' => '1',
                'no_shipping' => '1',
                'a3' => $data['PremiumSite']['cost'],
                'p3' => '1',
                't3' => 'W',
                'src' => '1',
                'sra' => '1'
            );

            if($data['Site']['is_premium_used'] == '0') {
                //Apply two week trial if unused
                $trialData = array(
                    'a1' => '0',
                    'p1' => '2',
                    't1' => 'W',
                );
                $paypalData = array_merge($paypalData, $trialData);
            }
        } else {
            //Upfront payment

            $paypalData = array(
                'cmd' => '_xclick',
                'business'=> '',
                'notify_url' => '',
                'return' => '',
                'cancel_return' => '',
                'item_name' => '',
                'item_number' => $premiumID,
                'currency_code' => 'USD',
                'no_note' => '1',
                'no_shipping' => '1',
                'amount' => $data['PremiumSite']['cost'],
            );
        }

        $this->layout = null;
        $this->set('data', $paypalData);
    }

    /**
     * IPN Callback from Paypal. Validates data, inserts it
     * into the db and triggers __processTransaction()
     * 
     * @return null
     */
    function process() {
        //Original code from http://www.studiocanaria.com/articles/paypal_ipn_controller_for_cakephp
        //Have we been sent an IPN here...
        if (!empty($_POST)) {
            //...we have so add 'cmd' 'notify-validate' to a transaction variable
            $transaction = 'cmd=_notify-validate';
            //and add everything paypal has sent to the transaction
            foreach ($_POST as $key => $value) {
                $value = urlencode(stripslashes($value));
                $transaction .= "&$key=$value";
            }
            //create headers for post back
            $header = "POST /cgi-bin/webscr HTTP/1.0\r\n";
            $header .= "Content-Type: application/x-www-form-urlencoded\r\n";
            $header .= "Content-Length: " . strlen($transaction) . "\r\n\r\n";
            //If this is a sandbox transaction then 'test_ipn' will be set to '1'
            if (isset($_POST['test_ipn'])) {
                $server = 'www.sandbox.paypal.com';
            } else {
                $server = 'www.paypal.com';
            }
            //and post the transaction back for validation
            $fp = fsockopen('ssl://' . $server, 443, $errno, $errstr, 30);
            //Check we got a connection and response...
            if (!$fp) {
                //...didn't get a response so log error in error logs
                $this->log('HTTP Error in PaypalNotifications::process while posting back to PayPal: Transaction=' .
                    $transaction);
            } else {
                //...got a response, so we'll through the response looking for VERIFIED or INVALID
                fputs($fp, $header . $transaction);
                while (!feof($fp)) {
                    $response = fgets($fp, 1024);
                    if (strcmp($response, "VERIFIED") == 0) {
                        //The response is VERIFIED so format the $_POST for processing
                        $notification = array();

                        //Minor change to use item_id as premium_site_id
                        $notification['PaypalNotification'] = array_merge($_POST, array('premium_site_id' => $_POST['item_number']));
                        $this->PaypalNotification->save($notification);

                        $this->__processTransaction($this->PaypalNotification->id);
                    } else
                        if (strcmp($response, "INVALID") == 0) {
                            //The response is INVALID so log it for investigation
                            $this->log('Found Invalid:' . $transaction);
                        }
                }
                fclose($fp);
            }
        }
        //Redirect
        $this->redirect('/');
    }

    /**
     * Enables premium site after payment
     * 
     * @param integer $id uses id from PaypalNotification
     * @return null
     */
    function __processTransaction($id) {
        $transaction = $this->PaypalNotification->find('first', array('conditions' => array('PaypalNotification.id' => $id), 'recursive' => 0));
        $txn_type = $transaction['PaypalNotification']['txn_type'];

        if($txn_type == 'subscr_signup' || $transaction['PaypalNotification']['payment_status'] == 'Completed') {
            //New subscription or payment
            $data = array(
                'PremiumSite' => array(
                    'id' => $transaction['PremiumSite']['id'],
                    'is_active' => '1',
                    'is_paid' => '1'
                ),
                'Site' => array(
                    'id' => $transaction['PremiumSite']['site_id'],
                    'is_premium' => '1'
                )
            );

            //Mark trial used only on subscriptions
            if($txn_type == 'subscr_signup') $data['Site']['is_premium_used'] = '1';

            $this->PaypalNotification->PremiumSite->saveAll($data);

        } elseif($txn_type == 'subscr-cancel' || $txn_type == 'subscr-eot') {
            //Subscription cancellation or other problem
            $data = array(
                'PremiumSite' => array(
                    'id' => $transaction['PremiumSite']['id'],
                    'is_active' => '0',
                ),
                'Site' => array(
                    'id' => $transaction['PremiumSite']['site_id'],
                    'is_premium' => '0'
                )
            );

            $this->PaypalNotification->PremiumSite->saveAll($data);
        }


    }

    /**
     * Used for testing
     * 
     * @return null
     */
    function index() {
        $this->__processTransaction('3');
    }
}

/views/paypal_notifications/send.ctp

将用户发送到Paypal以及所有必要的数据

class PaypalNotificationsController extends AppController { var $name = 'PaypalNotifications'; function beforeFilter() { parent::beforeFilter(); $this->Auth->allow('process'); } /** * Compiles premium info and send the user to Paypal * * @param integer $premiumID an id from PremiumSite * @return null */ function send($premiumID) { if(empty($premiumID)) { $this->Session->setFlash('There was a problem, please try again.', true, array('class' => 'error')); $this->redirect(array('controller' => 'premium_sites', 'action' => 'index')); } $data = $this->PaypalNotification->PremiumSite->find('first', array('conditions' => array('PremiumSite.id' => $premiumID), 'recursive' => 0)); if($data['PremiumSite']['type'] == '0') { //Subscription $paypalData = array( 'cmd' => '_xclick-subscriptions', 'business'=> '', 'notify_url' => '', 'return' => '', 'cancel_return' => '', 'item_name' => '', 'item_number' => $premiumID, 'currency_code' => 'USD', 'no_note' => '1', 'no_shipping' => '1', 'a3' => $data['PremiumSite']['cost'], 'p3' => '1', 't3' => 'W', 'src' => '1', 'sra' => '1' ); if($data['Site']['is_premium_used'] == '0') { //Apply two week trial if unused $trialData = array( 'a1' => '0', 'p1' => '2', 't1' => 'W', ); $paypalData = array_merge($paypalData, $trialData); } } else { //Upfront payment $paypalData = array( 'cmd' => '_xclick', 'business'=> '', 'notify_url' => '', 'return' => '', 'cancel_return' => '', 'item_name' => '', 'item_number' => $premiumID, 'currency_code' => 'USD', 'no_note' => '1', 'no_shipping' => '1', 'amount' => $data['PremiumSite']['cost'], ); } $this->layout = null; $this->set('data', $paypalData); } /** * IPN Callback from Paypal. Validates data, inserts it * into the db and triggers __processTransaction() * * @return null */ function process() { //Original code from http://www.studiocanaria.com/articles/paypal_ipn_controller_for_cakephp //Have we been sent an IPN here... if (!empty($_POST)) { //...we have so add 'cmd' 'notify-validate' to a transaction variable $transaction = 'cmd=_notify-validate'; //and add everything paypal has sent to the transaction foreach ($_POST as $key => $value) { $value = urlencode(stripslashes($value)); $transaction .= "&$key=$value"; } //create headers for post back $header = "POST /cgi-bin/webscr HTTP/1.0\r\n"; $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; $header .= "Content-Length: " . strlen($transaction) . "\r\n\r\n"; //If this is a sandbox transaction then 'test_ipn' will be set to '1' if (isset($_POST['test_ipn'])) { $server = 'www.sandbox.paypal.com'; } else { $server = 'www.paypal.com'; } //and post the transaction back for validation $fp = fsockopen('ssl://' . $server, 443, $errno, $errstr, 30); //Check we got a connection and response... if (!$fp) { //...didn't get a response so log error in error logs $this->log('HTTP Error in PaypalNotifications::process while posting back to PayPal: Transaction=' . $transaction); } else { //...got a response, so we'll through the response looking for VERIFIED or INVALID fputs($fp, $header . $transaction); while (!feof($fp)) { $response = fgets($fp, 1024); if (strcmp($response, "VERIFIED") == 0) { //The response is VERIFIED so format the $_POST for processing $notification = array(); //Minor change to use item_id as premium_site_id $notification['PaypalNotification'] = array_merge($_POST, array('premium_site_id' => $_POST['item_number'])); $this->PaypalNotification->save($notification); $this->__processTransaction($this->PaypalNotification->id); } else if (strcmp($response, "INVALID") == 0) { //The response is INVALID so log it for investigation $this->log('Found Invalid:' . $transaction); } } fclose($fp); } } //Redirect $this->redirect('/'); } /** * Enables premium site after payment * * @param integer $id uses id from PaypalNotification * @return null */ function __processTransaction($id) { $transaction = $this->PaypalNotification->find('first', array('conditions' => array('PaypalNotification.id' => $id), 'recursive' => 0)); $txn_type = $transaction['PaypalNotification']['txn_type']; if($txn_type == 'subscr_signup' || $transaction['PaypalNotification']['payment_status'] == 'Completed') { //New subscription or payment $data = array( 'PremiumSite' => array( 'id' => $transaction['PremiumSite']['id'], 'is_active' => '1', 'is_paid' => '1' ), 'Site' => array( 'id' => $transaction['PremiumSite']['site_id'], 'is_premium' => '1' ) ); //Mark trial used only on subscriptions if($txn_type == 'subscr_signup') $data['Site']['is_premium_used'] = '1'; $this->PaypalNotification->PremiumSite->saveAll($data); } elseif($txn_type == 'subscr-cancel' || $txn_type == 'subscr-eot') { //Subscription cancellation or other problem $data = array( 'PremiumSite' => array( 'id' => $transaction['PremiumSite']['id'], 'is_active' => '0', ), 'Site' => array( 'id' => $transaction['PremiumSite']['site_id'], 'is_premium' => '0' ) ); $this->PaypalNotification->PremiumSite->saveAll($data); } } /** * Used for testing * * @return null */ function index() { $this->__processTransaction('3'); } }

3 个答案:

答案 0 :(得分:28)

第1课:不要使用PHP的超级全局

  • $_POST = $this->params['form'];
  • $_GET = $this->params['url'];
  • $_GLOBALS = Configure::write('App.category.variable', 'value');
  • $_SESSION(查看)= $session->read();(帮助者)
  • $_SESSION(控制器)= $this->Session->read();(组件)
  • $_SESSION['Auth']['User'] = $this->Auth->user();

$_POST的替换:

<?php
    ...
    //foreach ($_POST as $key => $value) {
    foreach ($this->params['form'] as $key => $value) {
    ...
    //if (isset($_POST['test_ipn'])) {
    if (isset($this->params['form']['test_ipn'])) {
    ...
?>

第2课:视图用于分享(与用户共享)

代码记录“编译高级信息并将用户发送到Paypal”不会将用户发送到PayPal。你在视图中重定向吗?

<?php
    function redirect($premiumId) {
        ...
        $this->redirect($url . '?' . http_build_query($paypalData), 303);
    }

在控制器末尾重定向并删除视图。 :)

第3课:数据操作属于模型层

<?php
class PremiumSite extends AppModel {
    ...
    function beforeSave() {
        if ($this->data['PremiumSite']['type'] == "1") {
            $cost = Configure::read('App.costs.premium');
            $numberOfWeeks = ((int) $this->data['PremiumSite']['length']) + 1;
            $timestring = String::insert('+:number weeks', array(
                'number' => $numberOfWeeks,
            ));
            $expiration = date('Y-m-d H:i:s', strtotime($timestring));
            $this->data['PremiumSite']['upfront_weeks'] = $weeks;
            $this->data['PremiumSite']['upfront_expiration'] = $expiration;
            $this->data['PremiumSite']['cost'] = $cost * $numberOfWeeks;
        } else {
            $this->data['PremiumSite']['cost'] = $cost;
        }
        return true;
    }
    ...
}
?>

第4课:模型不只是用于数据库访问

将代码记录为“付款后启用高级网站”移至PremiumSite模型,并在付款后拨打电话:

<?php
class PremiumSite extends AppModel {
    ...
    function enable($id) {
        $transaction = $this->find('first', array(
            'conditions' => array('PaypalNotification.id' => $id),
            'recursive' => 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->saveAll($data);
    }
    ...
}
?>

您可以使用$this->PaypalNotification->PremiumSite->enable(...);从控制器拨打电话,但我们不打算这样做,所以让我们将它们混合在一起......

第5课:数据源很酷

将您的PayPal IPN交互抽象为模型使用的数据源。

配置进入app/config/database.php

<?php
class DATABASE_CONFIG {
    ...
    var $paypal = array(
        'datasource' => 'paypal_ipn',
        'sandbox' => true,
        'api_key' => 'w0u1dnty0ul1k3t0kn0w',
    }
    ...
}
?>

数据源处理Web服务请求(app/models/datasources/paypal_ipn_source.php

<?php
class PaypalIpnSource extends DataSource {
    ...
    var $endpoint = 'http://www.paypal.com/';
    var $Http = null;
    var $_baseConfig = array(
        'sandbox' => true,
        'api_key' => null,
    );

    function _construct() {
        if (!$this->config['api_key']) {
            trigger_error('No API key specified');
        }
        if ($this->config['sandbox']) {
            $this->endpoint = 'http://www.sandbox.paypal.com/';
        }
        $this->Http = App::import('Core', 'HttpSocket'); // use HttpSocket utility lib
    }

    function validate($data) {
       ...
       $reponse = $this->Http->post($this->endpoint, $data);
       ..
       return $valid; // boolean
    }
    ...
}
?>

让模型完成工作(app/models/paypal_notification.php

通知只有在有效时才会保存,只有在保存通知时才启用网站

<?php
class PaypalNotification extends AppModel {
    ...
    function beforeSave() {
        $valid = $this->validate($this->data);
        if (!$valid) {
            return false;
        }
        //Minor change to use item_id as premium_site_id
        $this->data['PaypalNotification']['premium_site_id'] = 
            $this->data['PaypalNotification']['item_number'];
        /*
        $this->data['PaypalNotification'] = am($this->data, // use shorthand functions
            array('premium_site_id' => $this->data['item_number']));
        */
        return true;
    }
    ...
    function afterSave() {
        return $this->PremiumSite->enable($this->id);
    }
    ...
    function validate($data) {
        $paypal = ConnectionManager::getDataSource('paypal');
        return $paypal->validate($data);
    }
    ...
?>

控制器很笨。 (app/controllers/paypal_notifications_controller.php

“你是一个帖子吗?不是吗?那我甚至都不存在。”现在这个动作只是喊道,“我保存了发布的PayPal通知!”

<?php
class PaypalNotificationsController extends AppModel {
    ...
    var $components = array('RequestHandler', ...);
    ...
    function callback() {
        if (!$this->RequestHandler->isPost()) { // use RequestHandler component
            $this->cakeError('error404');
        }
        $processed = $this->PaypalNotification->save($notification);
        if (!$processed) {
            $this->cakeError('paypal_error');
        }
    }
    ...
}
?>

奖金回合:使用提供的库而不是本机PHP

有关以下内容的示例,请参阅之前的课程:

  • String代替sprintf
  • HttpSocket代替fsock个功能
  • RequestHandler而非人工检查
  • am代替array_merge

这些可以防止编码错误,减少代码量和/或提高可读性。

答案 1 :(得分:5)

除了deizel指出的所有内容(很棒的帖子顺便提一下),请记住一个基本的蛋糕原则:胖模特,瘦小的控制器。您可以查看this example,但基本的想法是将所有数据修改的内容放入模型中。您的控制器(大多数)应该只是模型和视图之间的链接。你的PremiumSitesController :: index()是一个完美的例子,应该在你的模型中的某个地方(正如deizel所指出的那样)。

Chris Hartjes也写了一个book about refactoring,如果你真的想学习它,你可能想看看它(它不是免费的,但它很便宜)。另外,Matt Curry有一个名字很酷:Super Awesome Advanced CakePHP Tips,而且完全免费下载。两者都是一个很好的阅读。

我还想插一篇关于蛋糕的文章,我认为这对蛋糕中的代码质量非常重要:Code formatting and readability。虽然我明白人们是否不同意......: - )

答案 2 :(得分:1)

我会指出这两件事:

  1. 你有很多硬编码配置的东西......使用cake的Configure来做...就像第一个控制器中的$cost变量,或$ paypalData ...你可以从其他地方获得它(例如flash应该来自语言文件),但不要将配置与实现混合......这将使类更易读,维护更容易...... < / LI>
  2. 将所有套接字内容封装到一个新的帮助程序类中......你可能需要它在某个地方...而且真的,它会混淆发生的事情......同时,考虑移出你的那个boa控制器的其他部分......例如,只是把它下面的一些其他类放在一起,这就是实现......你应该总是尝试使用小而简洁的前端控制器,因为它更容易理解发生了什么......如果有人关心实现细节,他可以看看相应的班级......
  3. 这就是我认为的蛋糕......

    格尔茨

    back2dos