views:

1182

answers:

7

This situation arises from someone wanting to create their own "pages" in their web site without having to get into creating the corresponding actions.

So say they have a URL like mysite.com/index/books... they want to be able to create mysite.com/index/booksmore or mysite.com/index/pancakes but not have to create any actions in the index controller. They (a non-technical person who can do simple html) basically want to create a simple, static page without having to use an action.

Like there would be some generic action in the index controller that handles requests for a non-existent action. How do you do this or is it even possible?

edit: One problem with using __call is the lack of a view file. The lack of an action becomes moot but now you have to deal with the missing view file. The framework will throw an exception if it cannot find one (though if there were a way to get it to redirect to a 404 on a missing view file __call would be doable.)

+1  A: 

You could use the magic __call() function. For example:

public function __call($name, $arguments) 
{
     // Render Simple HTML View
}
Noah Goodrich
One issue that comes up with this solution is that each action requires a corresponding view file. Thus, if someone tries to access a non-existing action that also does not have a view file, an error is presented (rather than, say, a 404).
gaoshan88
+2  A: 

You have to play with the router http://framework.zend.com/manual/en/zend.controller.router.html

I think you can specify a wildcard to catch every action on a specific module (the default one to reduce the url) and define an action that will take care of render the view according to the url (or even action called)

new Zend_Controller_Router_Route('index/*', 
array('controller' => 'index', 'action' => 'custom', 'module'=>'index')

in you customAction function just retrieve the params and display the right block. I haven't tried so you might have to hack the code a little bit

stunti
This solves my question as I wrote it. I screwed up, though, because in reality there are existing actions that are being used and this wildcard method forces ALL actions to use the "custom" action. Is that clear?
gaoshan88
A: 

stunti's suggestion was the way I went with this. My particular solution is as follows (this uses indexAction() of whichever controller you specify. In my case every action was using indexAction and pulling content from a database based on the url):

  1. Get an instance of the router (everything is in your bootstrap file, btw):

    $router = $frontController->getRouter();

  2. Create the custom route:

    $router->addRoute('controllername', new Zend_Controller_Router_Route('controllername/*', array('controller'=>'controllername')));

  3. Pass the new route to the front controller:

    $frontController->setRouter($router);

I did not go with gabriel's __call method (which does work for missing methods as long as you don't need a view file) because that still throws an error about the missing corresponding view file.

gaoshan88
+1  A: 

If you want to use gabriel1836's _call() method you should be able to disable the layout and view and then render whatever you want.

$this->_helper->layout()->disableLayout();
$this->_helper->viewRenderer->setNoRender(true);
ejunker
That works very well. Thanks!
gaoshan88
A: 
+1  A: 

I needed to have existing module/controller/actions working as normal in a Zend Framework app, but then have a catchall route that sent anything unknown to a PageController that could pick user specified urls out of a database table and display the page. I didn't want to have a controller name in front of the user specified urls. I wanted /my/custom/url not /page/my/custom/url to go via the PageController. So none of the above solutions worked for me.

I ended up extending Zend_Controller_Router_Route_Module: using almost all the default behaviour, and just tweaking the controller name a little so if the controller file exists, we route to it as normal. If it does not exist then the url must be a weird custom one, so it gets sent to the PageController with the whole url intact as a parameter.

class UDC_Controller_Router_Route_Catchall extends Zend_Controller_Router_Route_Module
{
    private $_catchallController = 'page';
    private $_catchallAction     = 'index';
    private $_paramName          = 'name';

    //-------------------------------------------------------------------------
    /*! \brief takes most of the default behaviour from Zend_Controller_Router_Route_Module
        with the following changes:
        - if the path includes a valid module, then use it
        - if the path includes a valid controller (file_exists) then use that
            - otherwise use the catchall
    */
    public function match($path, $partial = false)
    {
        $this->_setRequestKeys();

        $values = array();
        $params = array();

        if (!$partial) {
            $path = trim($path, self::URI_DELIMITER);
        } else {
            $matchedPath = $path;
        }

        if ($path != '') {
            $path = explode(self::URI_DELIMITER, $path);

            if ($this->_dispatcher && $this->_dispatcher->isValidModule($path[0])) {
                $values[$this->_moduleKey] = array_shift($path);
                $this->_moduleValid = true;
            }

            if (count($path) && !empty($path[0])) {
                $module = $this->_moduleValid ? $values[$this->_moduleKey] : $this->_defaults[$this->_moduleKey];
                $file = $this->_dispatcher->getControllerDirectory( $module ) . '/' . $this->_dispatcher->formatControllerName( $path[0] ) . '.php'; 
                if (file_exists( $file ))
                {
                    $values[$this->_controllerKey] = array_shift($path);
                }
                else
                {
                    $values[$this->_controllerKey] = $this->_catchallController;
                    $values[$this->_actionKey] = $this->_catchallAction;
                    $params[$this->_paramName] = join( self::URI_DELIMITER, $path );
                    $path = array();
                }
            }

            if (count($path) && !empty($path[0])) {
                $values[$this->_actionKey] = array_shift($path);
            }

            if ($numSegs = count($path)) {
                for ($i = 0; $i < $numSegs; $i = $i + 2) {
                    $key = urldecode($path[$i]);
                    $val = isset($path[$i + 1]) ? urldecode($path[$i + 1]) : null;
                    $params[$key] = (isset($params[$key]) ? (array_merge((array) $params[$key], array($val))): $val);
                }
            }
        }

        if ($partial) {
            $this->setMatchedPath($matchedPath);
        }

        $this->_values = $values + $params;

        return $this->_values + $this->_defaults;
    }
}

So my MemberController will work fine as /member/login, /member/preferences etc, and other controllers can be added at will. The ErrorController is still needed: it catches invalid actions on existing controllers.

Steve
A: 

@steve how did you implement it in the boostrap? thanks.

Shagenius