Location: PHPKode > projects > Maintainable PHP Framework > vendor/Mad/Controller/Base.php
<?php
/**
 * @category   Mad
 * @package    Mad_Controller
 * @copyright  (c) 2007-2009 Maintainable Software, LLC
 * @license    http://opensource.org/licenses/bsd-license.php BSD
 */

/**
 * Base class for all Controller classes.
 *
 * @category   Mad
 * @package    Mad_Controller
 * @copyright  (c) 2007-2009 Maintainable Software, LLC
 * @license    http://opensource.org/licenses/bsd-license.php BSD
 */
abstract class Mad_Controller_Base
{
    /**
     * Generic data struct for storing overloaded attributes
     * @var array
     */
    private $_data = array();

    /**
     * Name of dir where views are for controller.
     * DocumentListController => DocumentList
     * @var string
     */
    private $_shortName = null;

    /**
     * The path to the view templates
     * @var string
     */
    private $_viewsDir = null;

    /**
     * If we are going to use a layout or not for the action
     * @var boolean
     */
    private $_useLayout = true;

    /**
     * Name of the layout template we're using
     * @var string
     */
    private $_layoutName = 'application';


    /*##########################################################################
    # Request/Response
    ##########################################################################*/

    /**
     * Params is the list of variables set through routes.
     * @var Mad_Support_ArrayObject
     */
    protected $params;

    /**
     * Proxy accessor to flash data in request and response objects.
     * @var Mad_Controller_Proxy_Flash
     */
    protected $flash;
    
    /**
     * Proxy accessor to session data in the request and response objects.
     * @var Mad_Controller_Proxy_Session
     */
    protected $session;

    /**
     * @var Mad_Controller_UrlWriter
     */
    protected $_urlWriter;

    /**
     * The request object we are processing
     * @var Mad_Controller_Request_Http
     * @todo Assign default value.
     */
    protected $_request;

    /**
     * The request object we are returning
     * @var Mad_Controller_Response_Http
     * @todo Assign default value.
     */
    protected $_response;

    /**
     * Have we performed a render on this controller
     * @var boolean
     */
    private $_performedRender = false;

    /**
     * Have we performed a redirect on this controller
     * @var boolean
     */
    private $_performedRedirect = false;

    /**
     * The current action being performed
     * @var string
     * @todo Assign default value.
     */
    private $_action;
    
    /**
     * Is there an action naming conflict with PHP?
     * @var boolean
     */
    private $_actionConflict = false;


    /*##########################################################################
    # Special method/actions
    ##########################################################################*/

    /**
     * Normal methods available as action requests.
     * @var array
     */
    private $_actionMethods = array();

    /**
     * Filters enable controllers to run shared pre and post processing code for its actions.
     * They are stored here by an associative array as follows:
     *  Methods that execute BEFORE the action: $_filters['before']
     *  Methods that execute AFTER the action:  $_filters['after']
     * @var array
     */
    private $_filters = array('before' => array(), 'after' => array());


    /*##########################################################################
    # Construct
    ##########################################################################*/

    /**
     * Construct new instance of the controller
     */
    public function __construct(){}

    /**
     * Only instantiate the template/logger objects if they're called. This allows
     * us to bypass instantiating the template object for a method that will only
     * ever redirect.
     *
     * @param   string  $name
     */
    public function __get($name)
    {
        // Only instantiate view if used
        if ($name == '_view') {
            if (!isset($this->_data['_view'])) {
                $this->_data['_view'] = new Mad_View_Base($this);
                $this->_data['_view']->addBuiltinHelpers();
            }
            return $this->_data['_view'];

        // Only instantiate logger if used
        } elseif ($name == '_logger') {
            if (!isset($this->_data['logger'])) {
                $this->_data['logger'] = $GLOBALS['MAD_DEFAULT_LOGGER'];
            }
            return $this->_data['logger'];
        }
    }

    /**
     * This gets called before action is performed in a controller. 
     * Override method in subclass to setup filters/helpers
     */ 
    protected function _initializeApplication() {}


    /*##########################################################################
    # Instance Methods
    ##########################################################################*/

    /**
     * Process the {@link Mad_Controller_Request_Http} and return
     * the {@link Mad_Controller_Response_Http}. This is the method that is called
     * for every request to be processed. It then determines which action to call
     * based on the parameters set within the {@link Mad_Controller_Request_Http}
     * object.
     *
     * <code>
     *  <?php
     *  ...
     *  $request  = new Mad_Controller_Request_Http();
     *  $response = new Mad_Controller_Response_Http();
     *
     *  $response = $controller->process($request, $response);
     *  ...
     *  ?>
     * </code>
     *
     * @param   Mad_Controller_Request_Http   $request
     * @param   Mad_Controller_Response_Http  $response
     * @return  Mad_Controller_Response_Http
     */
    public function process($request, $response)
    {
        $this->_request   = $request;
        $this->_response  = $response;
        $this->_initParams();
        $this->_initProxies();

        $this->_viewsDir  = 'app/views/';
        $this->_shortName = str_replace('Controller', '', $this->params[':controller']);

        try {
            // templates
            $this->_initActionMethods();
            $this->_initViewPaths();
            $this->_initViewHelpers();

            // Initialize application logic used thru all actions
            $this->_initializeApplication();
            if ($this->_performed()) return $this->_response;

            // Initialize sub-controller logic used thru all actions
            if (is_callable(array($this, '_initialize'))) {
                $this->_initialize();
            }

            // execute before filters, and return if we performed an action
            $this->_executeFilters('before');
            if ($this->_performed()) return $this->_response;

            // execute action & save any changes to sessionData
            $this->{$this->_action}();

            // execute after filters, and return if we performed an action
            $this->_executeFilters('after');
            if ($this->_performed()) return $this->_response;

            // render default if we haven't performed an action yet
            if (!$this->_performed()) {
                $this->render();
            }
        } catch (Exception $e) {
            $this->_rescueAction($e);
        }

        return $this->_response;
    }

    /**
     * Method to inspect the properties of the controller. Mosty useful for
     * unit testing assertions.
     *
     * @return  array
     */
    public function getAttributes()
    {
        $ref   = new ReflectionClass($this);
        $attrs = $ref->getProperties();
        foreach ($attrs as $attr) {
            $name = $attr->getName();
            $attrVals[$name] = $this->$name;
        }
        return $attrVals;
    }

    /**
     * Get an assigned template var. Mostly used for unit testing assertions.
     * 
     * @param   string  $name
     * @return  mixed
     */
    public function getAssigns($name)
    {
        return $this->_view->$name;
    }

    /**
     * Get the list of template used to construct the page. Most useful for
     * functional testing assertions.
     *
     * @return  array
     */
    public function getTemplates()
    {
        foreach ($this->_view->getPaths() as $dir) {
            $path = $dir.$this->_action.'.html';
            if (is_readable($path)) {
                $templates[] = $path;
            }
            $layout = $dir.$this->_layoutName.'.html';
            if ($this->usesLayout() && is_readable($layout)) {
                $templates[] = $layout;
            }
        }

        // funky sorting method to keep these in the same order
        return explode('**', implode('**', $templates));
    }

    /**
     * Get an instance of the view object
     * 
     * @return  Mad_View_Base
     */
    public function getView()
    {
        return $this->_view;
    }

    /**
     * Returns the current request object.
     *
     * @return Mad_Controller_Request_Http
     */
    public function getRequest()
    {
        return $this->_request;
    }

    /**
     * Returns the name of the current controller (e.g. "home" if HomeControler)
     *
     * @return string
     */
    public function getControllerName()
    {
        $params = $this->_request->getPathParams();
        return $params['controller'];
    }

    /** 
     * Returns the name of the current action (e.g. "index" if HomeController::index())
     * 
     * @return string
     */
    public function getActionName()
    {
        $params = $this->_request->getPathParams();
        return $params['action'];
    }

    /*##########################################################################
    # Methods available to Controllers Subclasses
    ##########################################################################*/

    /**
     * Render the response to the user. Actions are automatically rendered if no other
     * action is specified.
     *
     * <code>
     *  <?php
     *  ...
     *  $this->render(array('text'    => 'some text to render'));
     *  $this->render(array('action'  => 'actionName'));
     *  $this->render(array('nothing' => 1));
     *  ...
     *  ?>
     * </code>
     *
     * @see     renderText()
     * @see     renderAction()
     * @see     renderNothing()
     * @param   array  $options
     * @throws  Mad_Controller_Exception
     */
    protected function render($options=array())
    {
        // should not render/redirect more than once.
        if ($this->_performed()) {
            throw new Mad_Controller_Exception("Double render error: \"$this->_action\"");
        }
        
        // validate options
        $valid = array('text', 'nothing', 'action', 'status', 'location', 'xml');
        $options = Mad_Support_Base::assertValidKeys($options, $valid);

        // set response status
        if ($status = $options['status']) {
            $header = $this->interpretStatus($status);
            $this->_response->setStatus($header);
        }

        // set response location
        if ($location = $options['location']) {
            $url = $this->urlFor($location);
            $this->_response->setHeader("Location: $url", $replace=true);
        }

        // render text
        if ($text = $options['text']) {
            $this->renderText($text);

        // render xml
        } else if ($xml = $options['xml']) {
            $this->_response->setContentType('application/xml');

            if (is_object($xml) && method_exists($xml, 'toXml')) {
                $xml = $xml->toXml();
            }

            $this->renderText($xml);

        // render template file
        } else if ($action = $options['action']) {
            $this->renderAction($action);

        // render empty body
        } else if ($options['nothing']) {
            $this->renderText('');

        // render default template
        } else {
            $this->renderAction($this->_action);
        }
    }

    /**
     * Render text directly to the screen without using a template
     *
     * <code>
     *  <?php
     *  ...
     *  $this->renderText('some text to render to the screen');
     *  ...
     *  ?>
     * </code>
     *
     * @param   string  $text
     */
    protected function renderText($text)
    {
        $this->_response->setBody($text);
        $this->_performedRender = true;
    }

    /**
     * The name of the action method will render by default.
     *
     * render 'listDocuments' template file
     * <code>
     *  <?php
     *  ...
     *  $this->renderAction('listDocuments');
     *  ...
     *  ?>
     * </code>
     *
     * @param   string  $name
     */
    protected function renderAction($name)
    {
        // current url
        $this->_view->currentUrl = '/'.$this->_request->getUri();

        // copy instance variables
        foreach (get_object_vars($this) as $key => $value) {
            $this->_view->$key = $value;
        }

        // add suffix
        if ($this->_actionConflict) {
            $name = str_replace('Action', '', $name);
        }
        if (! strstr($name, '.')) {
            $name .= '.html';
        }

        // prepend this controller's "short name" only if the action was
        // specified without a controller "short name".
        // e.g. index           -> Shortname/index
        //      Shortname/index -> Shortname/index
        if (! strstr($name, '/')) {
            $name = $this->_shortName.'/'.$name;
        }

        // respond to Javascript accept header
        if ($this->respondTo()->js) {
            // don't use application layout for JS
            if (strstr($this->_layoutName, 'application')) {
                $this->useLayout(false);
            }
            $this->_response->setHeader('Content-Type: text/javascript; charset=utf-8');
            $name = str_replace('.html', '.js', $name);

            if (! file_exists(MAD_ROOT.'/app/views/'.$name)) {
                throw new Mad_Controller_Exception("Missing template for: <b>$name</b>");
            }
        }
        if ($this->_useLayout) {
            $this->_view->contentForLayout = $this->_view->render($name);
            $text = $this->_view->render($this->_layoutName);
        } else {
            $text = $this->_view->render($name);
        }
        $this->renderText($text);
    }

    /**
     * Render blank content. This can be used anytime you want to send an 200 OK response
     * back to the user, but don't need to actually render any content.
     * This is mostly useful for AJAX requests.
     *
     * <code>
     *  <?php
     *  ...
     *  $this->renderNothing();
     *  ...
     *  ?>
     * </code>
     */
    protected function renderNothing()
    {
        $this->renderText('');
    }

    /**
     * Render a response that has no content (merely headers). The options
     * argument is interpreted to be a hash of header names and values.
     * This allows you to easily return a response that consists only of
     * significant headers:
     *
     *   head('created', array('location' => 'http://foo'))
     *   head(array('status' => 'created', 'location' => 'http://foo'))
     *
     * @param  integer|string|array  $first   Status code or options array
     * @param  array                 $second  Options array
     * @return void
     */
    protected function head($first, $second=array())
    {
        if (is_array($first)) {
            $options = $first;
            if (isset($options['status'])) {
                $status = $options['status'];
                unset($options['status']);
            } else {
                $status = 'ok';
            }
        } else {
            $status  = $first;
            $options = $second;
        }

        $status = $this->interpretStatus($status);
        
        foreach ($options as $key => $value) {
            $dashed = Mad_Support_Inflector::dasherize($key);
            $spaced = str_replace('-', ' ', $dashed);
            $spaced = ucwords($spaced);
            $dashed = str_replace(' ', '-', $spaced);

            $this->_response->setHeader("$dashed: $value", $replace=true);
        }

        $this->_response->setStatus($status);
        $this->render(array('nothing' => true));
    }

    /**
     */
    protected function interpretStatus($status) 
    {
        return Mad_Controller_StatusCodes::interpret($status);
    }

    /**
     * Generate a URL
     * @see Mad_Controller_UrlWriter
     * 
     * @param  string|array  $first   named route, string, or options array
     * @param  array         $second  options array (if not in $first)   
     * @return string                 generated url    
     */
    protected function urlFor($first = array(), $second = array())
    {
        return $this->getUrlWriter()->urlFor($first, $second);
    }

    /**
     * Get an instance of UrlWriter for this controller.
     *
     * @return Mad_Controller_UrlWriter
     */
    public function getUrlWriter()
    {
        // instantiate UrlWriter that will generate URLs for this controller
        if (! $this->_urlWriter) {
            $defaults = array('controller' => $this->getControllerName());
            $this->_urlWriter = new Mad_Controller_UrlWriter($defaults);
        } 
        return $this->_urlWriter;
    }

    /**
     * Redirect to another page. Can redirect in a few different ways. URL redirects will
     * automagically determine the relative path based on which server we're on.
     * On development it will prepend /~{username}/{caseCode}/ to the URL.
     *
     * Redirect directly to an URL.
     * <code>
     *  <?php
     *  ...
     *  $this->redirectTo('/path/to/url');
     *  ...
     *  ?>
     * </code>
     *
     * Redirect to a specific controller/action
     * <code>
     *  <?php
     *  ...
     *  $this->redirectTo(
     *      array('controller' => 'browse',
     *            'action'     => 'briefcases',
     *            'id'         => '3',
     *            'sort'       => 'name'));
     *  ...
     *  ?>
     * </code>
     *
     * Redirect to a action within this same controller
     * <code>
     *  <?php
     *  ...
     *  $this->redirectTo(
     *      array('action'     => 'briefcases',
     *            'id'         => '3',
     *            'sort'       => 'name'));
     *  ...
     *  ?>
     * </code>
     *
     * @param  string|array  $first   named route, string, or options array
     * @param  array         $second  options array (if not in $first)   
     * @return null
     * @throws Mad_Controller_Exception
     */
    protected function redirectTo($first = array(), $second = array())
    {
        // should not render/redirect more than once
        if ($this->_performed()) {
            $msg = "Double render error: <b>$this->_action</b>";
            throw new Mad_Controller_Exception($msg);
        }

        if ($first === 'back') {
            // redirect to previous request
            $url = $this->_request->getServer('HTTP_REFERER');
            if (empty($url)) {
                $msg = "No HTTP_REFERER was set in the request to this action, ".
                       "so redirectTo('back') could not be called successfully. ".
                       " If this is a test, make sure to specify [\"HTTP_REFERER\"]";
                throw new Mad_Controller_Exception($msg);
            }
        } else {
            // generate the url
            $url = $this->getUrlWriter()->urlFor($first, $second);
        }

        $this->_response->setBody('Redirecting...');
        $this->_response->redirect($url);
        $this->_performedRedirect = true;
    }

    /**
     * Send a string containing binary data to the client. Typically the browser
     * will use a combination of the content type and the disposition, both set in
     * the options, to determine what to do with this data
     *
     * Options
     *  - filename: A suggestion to the browser of default filename to use when saving
     *  - type: The content type, defaulting to application/octet-stream
     *  - disposition: Suggest to the browser that the file should be displayed inline
     *    (option 'inline') or downloaded and saved (option 'attachemnt', the default)
     *
     * Additional headers can be set using $this->_response->setHeader()
     *
     * <code>
     *  <?php
     *  ...
     *  $data = $this->_generateReport();
     *  $this->sendData($data, array('filename'    => 'BriefcaseReport.csv',
     *                               'type'        => 'application/ms-excel',
     *                               'disposition' => 'attachment'));
     *  ...
     *  ?>
     * </code>
     *
     * @see     Mad_Controller_Response_Http::setHeader()
     * @param   string  $filepath
     * @param   array   $options
     */
    protected function sendData($data, $options=null)
    {
        $options['length'] = strlen($data);

        $this->_sendFileHeaders($options);
        $this->renderText($data);
    }

    /**
     * Send the contents of a file to the client.
     * Options
     *  - filename: A suggestion to the browser of default filename to use when saving
     *  - type: The content type, defaulting to application/octet-stream
     *  - disposition: Suggest to the browser that the file should be displayed inline
     *    (option 'inline') or downloaded and saved (option 'attachemnt', the default)
     *
     * The method sets these headers automatically:
     *  - Content-Length
     *  - Content-Type
     *  - Content-Transfer-Encoding
     *
     * Additional headers can be set using $this->_response->setHeader()
     *
     * <code>
     *  <?php
     *  ...
     *  // simple download
     *  $this->sendFile('/path/to.zip');
     *
     *  // Show a JPEG in a browser
     *  $this->sendFile('/path/to.jpeg', array('type'        => 'image/jpeg',
     *                                         'disposition' => 'inline'));
     *  ...
     *  ?>
     * </code>
     *
     * @see     Mad_Controller_Response_Http::setHeader()
     * @param   string  $filepath
     * @param   array   $options
     */
    protected function sendFile($filepath, $options=null)
    {
        // make sure the file exists
        if (!file_exists($filepath)) {
            throw new Mad_Controller_Exception("The file $filepath does not exist to send");
        }

        // default length/filename
        $options['length'] = filesize($filepath);
        if (!isset($options['filename'])) {
            $options['filename'] = basename($filepath);
        }

        $this->_sendFileHeaders($options);
        $this->renderText(file_get_contents($filepath));
    }
    
    /**
     * Very very hacked together simple implementation of http-accept headers
     * 
     * <code>
     *  $wants = $this->respondTo();
     *  if ($wants->html) { $this->redirectTo('back'); } 
     *  if ($wants->js)   { $this->render(array('nothing' => true)); }
     * </code>
     * 
     * @todo    a real implementation of mime-types
     * @return  object
     */
    protected function respondTo()
    {
        return new Mad_Controller_Responder($this->_request);
    }

    /*##########################################################################
    # Session Data Methods
    ##########################################################################*/


    /**
     * Check if this is a GET http request
     *
     * <code>
     *  <?php
     *  ...
     *  // only render form on GET requests
     *  if ($this->isGet()) {
     *      $this->render();
     *  }
     *  ...
     *  ?>
     * </code>
     *
     * @return  boolean
     */
    protected function isGet()
    {
        return $this->_request->getMethod() == 'GET';
    }

    /**
     * Check if this is a POST http request
     *
     * <code>
     *  <?php
     *  ...
     *  // form was submitted - update data
     *  if ($this->isPost()) {
     *      Folder::update($this->params('id'), $this->params('folder'));
     *
     *  // display form
     *  } else {
     *      $this->render();
     *  }
     *  ...
     *  ?>
     * </code>
     *
     * @return  boolean
     */
    protected function isPost()
    {
        return $this->_request->getMethod() == 'POST';
    }

    /*##########################################################################
    # Template Methods
    ##########################################################################*/

    /**
     * Specifiy if we want to use the layout for this controller. This allows us
     * to specifiy a layout for an entire controller and then selectively tell
     * certain actions to not use the layout.
     *
     * <code>
     *  <?php
     *  ...
     *  protected function _initialize()
     *  {
     *      $this->setLayout('application');
     *  }
     *
     *  // tell this method to not use the layout
     *  public function getMyData()
     *  {
     *      $this->useLayout(false);
     *  }
     *  ...
     *  ?>
     * </code>
     *
     * @param   boolean $useLayout
     */
    protected function useLayout($useLayout)
    {
        $this->_useLayout = $useLayout;
    }

    /**
     * Check if the action uses a layout.
     * @return  boolean
     */
    protected function usesLayout()
    {
        return $this->_useLayout;
    }

    /**
     * Set the layout template for the controller. Specify the name of the file in
     * the /app/views/layouts directory without the .html extension
     *
     * <code>
     *  <?php
     *  ...
     *  public function _initialize()
     *  {
     *      $this->setLayout('application');
     *  }
     *  ...
     *  ?>
     * </code>
     *
     * @param   string  $layoutName
     */
    protected function setLayout($layoutName)
    {
        $this->_useLayout  = true;
        $this->_layoutName = $layoutName;
    }

    /**
     * Get the name of the layout in use, or False if the layout is disabled.
     *
     * @return  string|false
     */
    protected function getLayout()
    {                                 
        return $this->_useLayout ? $this->_layoutName : false;
    }

    /**
     * Add helper(s) for use in this controller
     * 
     * When the argument is a string, the method will provide the "Helper" 
     * suffix, require the file and include the module in the template class.  
     * 
     * <code>
     *  <?php
     *  ...
     *  $this->helper('Foo', 'Bar');
     *  ...
     *  ?>
     * </code>
     *
     * @param   string  $helperName
     */
    protected function helper($args)
    {
        foreach (func_get_args() as $helper) {
            $helperName = $helper.'Helper';
            $this->_view->addHelper(new $helperName($this->_view));
        }
    }


    /*##########################################################################
    # Filter Methods
    ##########################################################################*/

    /**
     * Perform these methods before the action is called. This is useful for
     * performing such operations as authentication which must be run before
     * every action.
     *
     * There are two options for filters.
     * - only: Only run the filter method before the given list of actions
     * - except: Run the filter methods before all actions except the given list
     *
     * <code>
     *  <?php
     *  ...
     *  function _initialize()
     *  {
     *      // Run loadSomeData() and doSomething() before every action
     *      $this->beforeFilter('loadSomeData', 'doSomething');
     *
     *      // Run loadCache() only before listDocs() action
     *      $this->beforeFilter('loadCache', array('only' => array(
     *                                             'DocumentController::listDocs')));
     *
     *      // Run authenticate() before all actions except login()
     *      $this->beforeFilter('authenticate', array('except' => array(
     *                                                'LoginController::login')));
     *  }
     *  ...
     *  ?>
     * </code>
     *
     * @param   string  $method
     * @param   array   $options
     */
    protected function beforeFilter($args)
    {
        $values = func_get_args();
        $last = end($values);
        $options = is_array($last) ? array_pop($values) : array();

        foreach ($values as $method) {
            $this->_addFilter('before', $method, $options);
        }
    }

    /**
     * Perform these methods after the action is called.  This is useful for
     * performing such operations as cleanup which must be run after every action.
     *
     * There are two options for filters.
     * - only: Only run the filter method after the given list of actions
     * - except: Run the filter methods after all actions except the given list
     *
     * <code>
     *  <?php
     *  ...
     *  function _initialize()
     *  {
     *      // Run saveCache() and cleanUp() after every action
     *      $this->afterFilter('saveCache', 'cleanUp');
     *
     *      // Run updateCache() only after insertDoc action
     *      $this->afterFilter('updateCache', array('only' => array(
     *                                              'DocumentController::insertDoc')));
     *
     *      // Run cleanSession() after all actions except index()
     *      $this->afterFilter('cleanSession', array('except' => array(
     *                                               'LoginController::index')));
     *  }
     *  ...
     *  ?>
     * </code>
     *
     * @param   string  $method
     * @param   array   $options
     */
    protected function afterFilter($args)
    {
        $values = func_get_args();
        $last = end($values);
        $options = is_array($last) ? array_pop($values) : array();

        foreach ($values as $method) {
            $this->_addFilter('after', $method, $options);
        }
    }
    
    /**
     * Skip the given before filter method(s) for the controller. 
     *
     * There are two options for skipping filters.
     * - only: Only skip the filter method before the given list of actions
     * - except: Skip the filter methods before all actions except the given list
     *
     * <code>
     *  <?php
     *  ...
     *  function _initialize()
     *  {
     *      // Skip running loadSomeData() before filter for the current controller
     *      $this->skipBeforeFilter('loadSomeData');
     *
     *      // Skip running loadCache() filter only before listDocs() action
     *      $this->skipBeforeFilter('loadCache', array('only' => array(
     *                                             'DocumentController::listDocs')));
     *
     *      // Skip running authenticate() filter before all actions except login()
     *      $this->skipBeforeFilter('authenticate', array('except' => array(
     *                                                    'LoginController::login')));
     *  }
     *  ...
     *  ?>
     * </code>
     *
     * @param   string  $method
     * @param   array   $options
     */
    protected function skipBeforeFilter($args)
    {
        $values = func_get_args();
        $last = end($values);
        $options = is_array($last) ? array_pop($values) : array();

        foreach ($values as $method) {
            $this->_removeFilter('before', $method, $options);
        }
    }

    /**
     * Skip the given after filter method(s) for the controller. 
     * 
     * There are two options for filters.
     * - only: Only skip the filter method after the given list of actions
     * - except: Skip the filter methods after all actions except the given list
     *
     * <code>
     *  <?php
     *  ...
     *  function _initialize()
     *  {
     *      // Skip running saveCache() after filter for this controller
     *      $this->skipAfterFilter('saveCache');
     *
     *      // Skip running updateCache() only after insertDoc action
     *      $this->skipAfterFilter('updateCache', array('only' => array(
     *                                                  'DocumentController::insertDoc')));
     *
     *      // Skip running cleanSession() after all actions except index()
     *      $this->skipAfterFilter('cleanSession', array('except' => array(
     *                                                   'LoginController::index')));
     *  }
     *  ...
     *  ?>
     * </code>
     *
     * @param   string  $method
     * @param   array   $options
     */
    protected function skipAfterFilter($args)
    {
        $values = func_get_args();
        $last = end($values);
        $options = is_array($last) ? array_pop($values) : array();

        foreach ($values as $method) {
            $this->_removeFilter('after', $method, $options);
        }
    }

    /*##########################################################################
    # Rescues
    ##########################################################################*/

    /**
     * An exception has occurred inside an action.  Rescue it.
     *
     * @param  Exception $exception
     */
    protected function _rescueAction($exception)
    {
        if (MAD_ENV == 'development') {
            $this->_rescueActionLocally($exception);
        } else {
            throw $exception;
        }
    }

    /**
     * Rescue an exception locally.
     * 
     * @param  Exception $exception
     */
    protected function _rescueActionLocally($exception)
    {
        $renderer = new Mad_Controller_Rescue_Renderer();
        $html = $renderer->render($exception, $this->_request, $this->_response);

        $this->render(array('text' => $html, 'status' => 500));
    }

    /*##########################################################################
    # Private Methods
    ##########################################################################*/

    /**
     * Each variable set through routing {@link Mad_Controller_Route_Path} is
     * availabie in controllers using the $params array.
     *
     * The controller also has access to GET/POST arrays using $params
     *
     * The action method to be performed is stored in $this->params[':action'] key
     */
    private function _initParams()
    {
        $hash = $this->_request->getParameters();
        $this->params = new Mad_Support_ArrayObject($hash);

        $this->_action = $this->params->get(':action');
    }

    /**
     * Initialize proxy accessors.  Each proxy object connects the request and
     * and response object to the controller through an ArrayAccess interface
     * that allows convenient access to session, flash, and cookies.
     */
    private function _initProxies()
    {
        $this->session = new Mad_Controller_Proxy_Session($this->_request, $this->_response);
        $this->flash   = new Mad_Controller_Proxy_Flash($this->_request, $this->_response);
        $this->cookie  = new Mad_Controller_Proxy_Cookie($this->_request, $this->_response);
    }

    /**
     * Set the list of public actions that are available for this Controller.
     * Subclasses can remove methods from being publicly called by calling
     * {@link hideAction()}.
     *
     * @throws  Mad_Controller_Exception
     */
    private function _initActionMethods()
    {
        // Perform reflection to get the list of public methods
        $reflect = new ReflectionClass($this);
        $methods = $reflect->getMethods();
        foreach ($methods as $m) {
            if ($m->isPublic() && !$m->isConstructor() && !$m->isDestructor()  &&
                $m->getName() != 'process' && substr($m->getName(), 0, 1) != '_') {
                $this->_actionMethods[$m->getName()] = 1;
            }
        }

        // try action suffix. 
        if (!isset($this->_actionMethods[$this->_action]) && 
            isset($this->_actionMethods[$this->_action.'Action'])) {
            $this->_actionConflict = true;
            $this->_action = $this->_action.'Action';
        }
        // action isn't set, but there is a methodMissing() catchall method
        if (!isset($this->_actionMethods[$this->_action]) &&
            isset($this->_actionMethods['methodMissing'])) {
            $this->_action = 'methodMissing';

        // make sure we have an action set, and that there is no methodMissing() method
        } elseif (!isset($this->_actionMethods[$this->_action]) &&
                  !isset($this->_actionMethods['methodMissing'])) {
            $msg = 'Missing action: '.get_class($this)."::".$this->_action;
            throw new Mad_Controller_Exception($msg);
        }
    }

    /**
     * Initialize the view paths where the templates reside for this controller.
     * These are added in FIFO order, so if we do $this->renderAction('foo'),
     * in the BarController, the order it will search these directories will be:
     *  1. /views/Bar/foo.html
     *  2. /views/shared/foo.html
     *  3. /views/layouts/foo.html
     *  4. /views/foo.html (the default)
     *
     * We can specify a directory to look in instead of relying on the default order
     * by doing $this->renderAction('shared/foo').
     */
    private function _initViewPaths()
    {
        $this->_view->addPath($this->_viewsDir.'layouts');
        $this->_view->addPath($this->_viewsDir.'shared');
        $this->_view->addPath($this->_viewsDir.$this->_shortName);
    }

    /**
     * Initialize the default helpers for use in the views
     */
    private function _initViewHelpers()
    {
        $controllerHelper = Mad_Support_Inflector::classify($this->_shortName.'Helper');
        $this->_view->addHelper(new $controllerHelper($this->_view));
    }

    /**
     * Send file headers for {@link sendFile()} and {@link sendData()}.
     *
     * @see     sendData()
     * @see     sendFile()
     * @param   array   $options
     */
    protected function _sendFileHeaders($options)
    {
        // validate options
        $valid = array('filename', 'type', 'disposition', 'length');
        $options = Mad_Support_Base::assertValidKeys($options, $valid);

        // default type/disposition/filename
        if (empty($options['type'])) {
            $options['type'] = 'application/octet-stream';
        }
        if (empty($options['disposition'])) {
            $options['disposition'] = 'attachment';
        }
        if (!empty($options['filename'])) {
            $options['disposition'] .= '; filename='.$options['filename'];
        }

        $this->_response->setHeader('Expires: 0');
        $this->_response->setHeader('Content-Length: '.$options['length']);
        $this->_response->setHeader('Content-Type: '.$options['type']);
        $this->_response->setHeader('Content-Disposition: '.$options['disposition']);
        $this->_response->setHeader('Content-Transfer-Encoding: binary');

        // allow for byte serving of pdf docs
        if (strstr($options['filename'], 'pdf')) {
	        $this->_response->setHeader("Accept-Ranges: bytes");
	        $this->_response->setHeader("bytes: 0-");
        }
    }

    /**
     * Add either an except/only filter
     * @param   string  $type
     * @param   string  $method
     * @param   array   $options
     * @throws  Mad_Controller_Exception
     */
    private function _addFilter($type, $method, $options)
    {
        // validate options
        $valid = array('except', 'only');
        $options = Mad_Support_Base::assertValidKeys($options, $valid);

        // make sure the method exists before adding as a filter
        if (!method_exists($this, $method)) {
            $msg = "The method \"$method\" can't be used as a filter because it does not exist";
            throw new Mad_Controller_Exception($msg);
        }

        // add to array of filters
        $this->_filters[$type][$method] = $options;
    }

    /**
     * Remove either an except/only filter
     * @param   string  $type
     * @param   string  $method
     * @param   array   $options
     */
    private function _removeFilter($type, $method, $options) 
    {
        // validate options
        $valid = array('except', 'only');
        $options = Mad_Support_Base::assertValidKeys($options, $valid);

        $thisAction = $this->params[':controller'] . '::' . $this->params[':action'];

        // only skip filter for specified controller::actions
        if ($options['only']) {
            $only = $this->_parseFilterController($options['only']);
            if (in_array($thisAction, $only)) {
                unset($this->_filters[$type][$method]);
            }

        // skip filter for all but the specified controller::actions
        } elseif ($options['except']) {
            $except = $this->_parseFilterController($options['except']);
            if (!in_array($thisAction, $except)) {
                unset($this->_filters[$type][$method]);
            }

        // skip for all methods in this controller
        } else {
            unset($this->_filters[$type][$method]);
        }
    }

    /**
     * Execute all filters to be run before the action.
     *
     * @todo    Make it so that we default to current controller for only/except options
     * @see     beforeFilter()
     * @see     afterFilter()
     * @param   string  $type
     * @return  boolean If we performed an action
     */
    private function _executeFilters($type)
    {
        // return if there are no filters
        if (!isset($this->_filters[$type])) return;

        // execute each filter. Kill the loop if an action redirects/renders
        foreach ($this->_filters[$type] as $methodName => $options) {
            $thisAction = $this->params[':controller'] . '::' . $this->params[':action'];

            // only execute filters for specific controller::actions
            if ($options['only']) {
                $options['only'] = $this->_parseFilterController($options['only']);
                if (!in_array($thisAction, $options['only'])) continue;

            // don't execute the filter for specific controller::actions
            } elseif ($options['except']) {
                $options['except'] = $this->_parseFilterController($options['except']);
                if (in_array($thisAction, $options['except'])) continue;
            }
            $this->$methodName();
            if ($type == 'before' && $this->_performed()) return true;
        }
        return $this->_performed();
    }

    /**
     * Each filter option should be an array of Controller::actions. If only
     * an action is given, set filter controller to the current controller
     *
     * @param   array   $actions
     * @return  array
     */
    protected function _parseFilterController($actions)
    {
        foreach ($actions as &$action) {
            if (!strstr($action, '::')) {
                $action = $this->params[':controller'].'::'.$action;
            }
        }
        return $actions;
    }

    /**
     * Check if a render or redirect has been performed
     * @return  boolean
     */
    protected function _performed()
    {
        return $this->_performedRender || $this->_performedRedirect;
    }
}
Return current item: Maintainable PHP Framework