An idea for hacking core helpers

Published on and tagged with cakephp  helper  ideas

In a recent comment by John I got asked whether there is a way to override a method of a core helper like HtmlHelper::link() without affecting the variables in the views (i.e. you can still use $html->link() instead of $myHtml->link()). There are at least two approaches to accomplish this (without touching core files).

The first approach is to copy the HtmlHelper from the core (cake/libs/view/helpers/html.php) to the helpers folder of your application (app/views/helpers). And then you can modify this file as you like. Easy. The problem of this approach is the code duplication. If something in the core HtmlHelper is changed by the developers you have to reapply the changes in your own HtmlHelper…

The second approach is to replace the HtmlHelper with your own helper that extends the HtmlHelper. For this purpose you have to create a custom view class which overrides the _loadHelpers() method of the View. It’s probably best if I do a simple example to show the idea.

First we create our own helper:

// app/views/helpers/example.php
App::import('Helper', 'Html');
class ExampleHelper extends HtmlHelper {
    public function link($title, $url = null, $htmlAttributes = array(), $confirmMessage = false, $escapeTitle = true) {
        return '<a href="http://example.com">example</a>';
    }
}

Next, we create our custom view class:

// app/views/example.php
class ExampleView extends View {
    public function &_loadHelpers(&$loaded, $helpers, $parent = null) {
        $loaded = parent::_loadHelpers($loaded, $helpers, $parent);
        $helper = 'Html';
		
        if (isset($loaded[$helper])) {
            App::import('Helper', 'Example');
            $loaded[$helper] = new ExampleHelper();
	
            // everything from here on is copied from View::_loadHelpers()
		
            $vars = array('base', 'webroot', 'here', 'params', 'action', 'data', 'themeWeb', 'plugin');
            $c = count($vars);

            for ($j = 0; $j < $c; $j++) {
                $loaded[$helper]->{$vars[$j]} = $this->{$vars[$j]};
            }

            if (!empty($this->validationErrors)) {
                $loaded[$helper]->validationErrors = $this->validationErrors;
            }

            if (is_array($loaded[$helper]->helpers) && !empty($loaded[$helper]->helpers)) {
                $loaded =& $this->_loadHelpers($loaded, $loaded[$helper]->helpers, $helper);
            }
        }
		
        return $loaded;
    }
}

You probably wonder why I override _loadHelpers() and not _loadHelper(). Well, the reason is simple, there is no _loadHelper() method in the View class. And so I have to copy the code to setup a helper from the parent method. Not very clean, I know…

The last step is to specify that we want to use the view class we created above. We can do this in the AppController with:

// app/app_controller.php
class AppController extends Controller {
    public $view = 'Example';
}

Now, all links in our views should point to example.com.

The obvious disadvantage of this approach is that it is a hack ;-) And I’m not sure whether it works to 100%. Use it on your own risk!

Personally, I would like to see the approach described by grigri (see his comment) in the core (or something similar): “It would be great if the cake helpers were really named ‘BaseHtmlHelper’ with an empty class ‘HtmlHelper’ which is loaded from the cake folder if not present in the app folder.”

21 comments baked

  • rafaelbandeira3

    @cakebaker:
    I think it would be easier to override View::_render wich is the method that calls View::_loadHelpers, or tottally override View::_loadHelpers, wich then would be much more elegant too.
    About grigri’s approach, although interesting, I think that it would be really messy…

    I think that it would be nice if you could include a warning or a note saying how something like this can tottally kill your app’s readability, causing lots of confusion about methods that were not supposed to exist in a determined helper.

  • Fred

    Yup, that’s exactly how I did it back in 2006: http://viewvc.svn.mozilla.org/vc/addons/trunk/site/app/views/addons.php?view=markup

    It works quite well indeed!

  • Matt Curry

    I could have sworn I remember one of the core devs saying that someday you’d be able to define your helpers as a keyed array, where the key was the helper name and the value was the class to be used.

    So instead of var $helpers = array(‘Html’) you could do array(‘Html’ => ‘MyHtmlHelper’).

  • Abhimanyu Grover

    Looks like a good approach. Thanks

  • cakebaker

    @all: Thanks for your comments!

    @Rafael: Hm, to me it would be wrong to override View::_render() because I don’t want to change anything in the rendering process but in the loading process of helpers. So _loadHelpers() is the right method to override. It is an option to override _loadHelpers() completely, but I don’t see an advantage in doing so.

    @Fred: Cool to see this approach is already used!

    @Matt: Yes, I also have seen such a statement somewhere, but I don’t know when, and if, this feature really comes.

  • cakebaker

    @Rafael: Oops, I forgot to answer the second part of your comment ;-)

    I think grigri’s approach doesn’t affect the readability. It is the same approach we already have with the Controller or the Model class. Of course, at first people would be confused because they are used to the current approach, but that’s normal if something changes.

  • Dardo Sordi

    Well, here is how it looks overriding _render(), thanks to master jedi AD7Six:

    http://trac.assembla.com/mi/browser/branches/base/views/mi.php#L183

  • CakePHP. Как создать модуль без привязки к таблице в базе данных

    […] An idea for hacking core helpers […]

  • cakebaker

    @Dardo: Thanks for this link!

  • grigri

    It’s a shame none of the core devs have commented on this – I (and I doubt I’m alone) would like to hear if this issue is ever to be resolved within the core; if the idea of renaming core helpers is anathema to cake’s design principles; what methods the core devs use to customize helpers themselves, etc…

    Maybe the post wasn’t inflammatory enough? :)

  • John

    @grigri – I really like your idea from the previous post – to create base helpers that can then be extended would be a nice way to go – it feels like quite an elegant solution if you know what I mean – and I would think be relatively straight forward to implement – plus it wouldn’t need break backward compatability.

    Matt’s suggestion (array(’Html’ => ‘MyHtmlHelper’)) is very similar in a way (although the other way around) , I suppose MyHtmlHelper could either extend or just override – giving the developer the responsibility / flexibility

    @cakebaker – Unfortunately I haven’t had a chance to try this out yet, I’ve got an exciting week of server migration ahead of me…

  • grigri

    This has actually been discussed on the google groups, with no definitive solution.

    http://groups.google.com/group/cake-php/browse_thread/thread/ecdb16cb39f89ed5

    (there are others too)

    Nate said that he had implemented it (the alias thing), but not yet put in in the core because it wasn’t test-covered. This was in February ’08. I wonder what happened?

  • cakebaker

    @John: Good luck with your migration!

    @grigri: I don’t know what happened with this feature, it probably got forgotten :|

  • ADmad

    How about this

    // app/app_controller.php
    class AppController extends Controller {
        public $view = 'Example';
        public $helpers = array('Example');
    }
    // app/views/example.php
    class ExampleView extends View {
        public function &_loadHelpers(&$loaded, $helpers, $parent = null) {
            $loaded = parent::_loadHelpers($loaded, $helpers, $parent);
    
    		if(isset($loaded['Example'])) {
    			$loaded['Html'] = $loaded['Example'];
    			unset($loaded['Example']);
    		}
    		
            return $loaded;
        }
    }

    I would prefer specifying my helper in the $helpers array instead of all that extra code in overridden _loadHelpers function.

  • cakebaker

    @ADmad: Thanks for your comment and for sharing your approach! It is a bit more cleaner than what I described in the article.

  • John

    @cakebaker – re migration – cheers all went smoothly and we don’t switch off the old server till the end of the month anyway. It’s just the sheer bloody tedium of it… It is really boring and you have to concentrate really hard too… luckily most of the domains emails are managed elsewhere which makes it a lot easier…

    But anyway, finally got the chance to do a bit of speculative baking and try this out – very easy and very straight forward. I’m going to play around with all the various solutions people have suggested.

    Anyway one of these will be a good fit with my original idea. ( which was to see if there was a way of testing the permissions on internal links (i.e. in the admin side of site) so you only see the links to actions / views you are actually allowed to… easy to do with a custom helper but modifying $html->link means you could retro fit it to any site with very little fuss.)

  • Заменяем стандартные хелперы не трогая ядро CakePHP - Программируем на CakePHP

    […] стандартных хелперов, но сегодня наткнулся на интересный пост на блоге cakebaker’а и в его обсуждении Admad привел замечательный пример как […]

  • David Cournoyer

    What I prefer (for a component but I think its the same for a helper).

    class AppController extends Controller {
    	var $components = array('Session', 'MySession');
    	function constructClasses() {
    		parent::constructClasses();
    		$this->Session = $this->MySession;
    	}
    class MySessionComponent extends SessionComponent {
    	function setFlash($message, $layout = 'default', $params = array(), $key = 'flash') {
    		// my override;
    	}
    }
    ?>
  • cakebaker

    @David: Clever idea. Thanks for sharing!

  • Furkan Tunalı

    For php5.
    Funny and dirty.. but works neat.

    <?php
    namespace CAKE {
    
    	foreach( $this->helpers as $node => $path ) {
    		$path .= 'form.php';
    		if( file_exists($path) ) {
    			break;
    		}
    	}
    
    	$lines = file ($path);
    	array_unshift($lines,
    		'
    		namespace CAKE;
    		use \Configure, \Inflector, \ClassRegistry, \Set, \AppHelper;
    		?>
    	');
    	$string = implode(chr(13).chr(10),$lines);
    	eval($string);
    
    }
    
    namespace {
    	use \CAKE\FormHelper as CakeFormHelper;
    
    	class FormHelper extends CakeFormHelper {
    		// damn php4 cake !!
    	}
    }
  • cakebaker

    @Furkan: Thanks for sharing your PHP 5.3 approach!

Bake a comment




(for code please use <code>...</code> [no escaping necessary])

© daniel hofstetter. Licensed under a Creative Commons License