An idea for hacking core helpers

Published on November 07, 2008 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.”

19 comments baked

  • rafaelbandeira3 November 07, 2008 at 15:32

    @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 November 07, 2008 at 18:30

    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 November 07, 2008 at 21:25

    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 November 08, 2008 at 18:23

    Looks like a good approach. Thanks

  • cakebaker November 08, 2008 at 19:53

    @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 November 08, 2008 at 20:11

    @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 November 08, 2008 at 23:36

    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. Как создать модуль без привязки к таблице в базе данных November 09, 2008 at 14:48

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

  • cakebaker November 10, 2008 at 18:25

    @Dardo: Thanks for this link!

  • grigri November 11, 2008 at 12:10

    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 November 11, 2008 at 15:02

    @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 November 11, 2008 at 16:19

    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 November 13, 2008 at 19:02

    @John: Good luck with your migration!

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

  • ADmad November 18, 2008 at 09:33

    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 November 18, 2008 at 18:24

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

  • John November 21, 2008 at 17:03

    @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 December 05, 2008 at 06:41

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

  • David Cournoyer September 03, 2009 at 06:01

    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 September 04, 2009 at 16:37

    @David: Clever idea. Thanks for sharing!

Bake a comment




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

© daniel hofstetter. Licensed under a Creative Commons License