Search code examples
phpexceptioncakephplayoutcakephp-2.4

CakePHP 2.4 Throwing main application exceptions from plugins? (interference with requestAction?)


I'm turning a well-working piece of my CakePHP (v 2.44) application into a plugin, and I'm getting the strangest behavior when throwing exceptions from within a controller in my plugin: the exception handler/renderer is starting to use my main site's layout in app/View/Layouts/mylayout.ctp, and then interrupts it with the default layout from app/View/Layouts/error.ctp. Here's an extract:

<div><ul><li class="jsdnavpopup blog-menu-categories">
<a href='/blog/categories'>All Categories</a><nav>
<!DOCTYPE html PUBLIC "{trimmed for space}">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>CakePHP: the rapid development php framework:Error Page</title>

If you notice where it looks like a new header is sent right in the middle of Cake's composing the layout. Right at the point where my layout is interrupted I load am element, and the element uses requestAction (no caching in my dev environment). If I remove the element, then my layout is rendered even more until the next element using requestAction is encountered. These requestAction calls aren't requesting an action from the plugin that raised the exception.

I'm using all of the default error handlers from app/Config/core.php. The only thing that's different is specifying $this->layout in the error views.

Now if I recreate the original error layouts and views in the plugin's View folder, things work like I would expect. But for experimental purposes I put a renamed copy of my main site's layout there, and the same thing. Any time an element using requestAction is encountered the exception interrupts the layout.

Any ideas?

I'll reiterate that everything works perfectly when throwing exceptions from anywhere in the app that's not a plugin.


Solution

  • Here's how I do it (as requested in the comments on the OP's question)

    Inside my layout, I put the following piece of code where I want the top menu to appear:

    <?php echo $this->element('Menu.top_navigation'); ?>
    

    This is quite simple, and allows the HTML designers in my team to put the menu wherever they want to.
    Of course there is more happening under the hood. Here are the other files you'll need to make this work:

    Plugins/Menu/View/Elements/top_navigation.ctp:

        $thisMenu = ClassRegistry::init('Menu.Menu');
        $menus = $thisMenu->display_menu_widget(array('position'=> 'top'));
    
        echo $this->element('Menu.display_ul', array('menus' => $menus));
    

    Note that this is just a wrapper -- you can make more of these for footer navigation, or even make an element that allows you to pass in the position as an argument.

    Plugins/Menu/View/Elements/display_ul.ctp:

    <?php
        if(empty($depth)){
            $depth = 0;
        }
    
        switch($depth){
            case 0:
                $classes = "dropdown";
            break;
            default:
                $classes = "";
            break;
        }
    ?>
    <ul class="<?php echo $classes ?> depth_<?php echo $depth ?>">
        <?php foreach ($menus as $menu): ?>
            <li>
                <a href="<?php echo $menu['MenuItem']['url']; ?>">
                    <?php echo $menu['MenuItem']['title']; ?>
                </a>
                <?php
                    if(count($menu['children']) > 0){
                        echo $this->element(
                            'Menu.display_ul',
                            array(
                                'menus' => $menu['children'],
                                'depth' => $depth + 1
                            )
                        );
                    }
                ?>
        </li>
    <?php endforeach; ?>
    </ul>
    

    Plugins/Menu/Model/Menu.php:

    /**
     * display_menu_widget method
     *
     * @param array $options
     * @return void
     */
    public function display_menu_widget($options = array()) {
    
        $defaults = array(
            'position' => 'top'
        );
    
        $settings = array_merge($defaults, $options);
    
        $this->recursive = 0;
    
        $menuItems = array();
    
        $conditions = array(
            'Menu.position' => $settings['position']
        );
    
        $menuDetails = $this->find('first', array('recursive' => -1, 'conditions' => $conditions));
    
    
        $menuPosition = $menuDetails[$this->alias]['position'];
        $parentId = $menuDetails[$this->alias][$this->primaryKey];
    
        $conditions = array(
            $this->MenuItem->alias . '.menu_id' => $parentId
        );
    
        $this->MenuItem->recursive = 0;
    
        $cacheName = 'default' . 'ModelMenuItem';
        $cacheKey = $this->generateCacheName(array('type' => 'threaded', 'conditions' => $conditions));
    
        $menuItems = Cache::read($cacheKey, $cacheName);
    
        if(empty($menuItems) || PWFunctions::forceCacheFlush()){
            $menuItems = $this->MenuItem->find('threaded', array('conditions' => $conditions));
    
            Cache::write($cacheKey, $menuItems, $cacheName);
        }
    
        return $menuItems;
    }
    

    There's probably more in that menu function than you need, I'm using some aggressive caching routines to reduce database load, I'd suggest only using the bare bones of that function before worrying about caching.

    This solution works well for me, and I think that it's the solution that violates the MVC concepts the least -- when you, as a designer, are in HTML mode and think "Hey I need to display the top menu" then it's just one line in your view, and you're not having to worry about modifying controllers or models.

    I've used this pattern throughout my current CakePHP projects, and it seems to work quite well.