How to override Yii core classes

Sometimes i’m not happy with Yii - especially if something is hardcoded to a core class which you can not override. For example i want to use Bootstrap CSS in my project and therefore i need my labels to wrap up my checkboxes or radio buttons. But CHtml is not capable of this. So suddenly rendering a simple form can become a pain as a lot of useful helpers can’t be used. How nice would it be if i could have my own custom CHtml class?

Such a solution should meet some requirements:

[b] 1. It should not alter framework files

  1. It should be easy to upgrade to another Yii version[/b]

To solve this, i used this approach:

1. Override CHtml

CHtml is a core class which gets autoloaded by Yii. So in order to prevent Yii from using its own class, i just have to load my custom CHtml class before Yii does. Fortunately this is very simple and Yii even provides a way how to do is. The key is to add your custom class to the import configuration:




<?php

    'import'=>array(

        'application.components.CHtml',

        ...



So now you can have your custom CHtml class in components/CHtml.php. So far so good. But …

2. Inherit from original CHtml

I don’t want to copy the original CHtml.php from Yii and alter it. I would have to do the same changes again, each time a new version is released and it’s very likely that i miss to redo some of my changes. So i want some kind of inheritance. My first idea was to use namespaces inside components/CHtml.php like this:




<?php

/* BELOW CODE DOES NOT WORK: */


// Load CHtml into yii namespace to have it under yii\Chtml

namespace yii {

    include(Yii::getPathOfAlias('system.web.helpers.CHtml').'.php');

}


// Create CHtml in global namespace

namespace yii {

    class CHtml extends yii\CHtml { /* override things here */ }

}



But this does not work! When you include a file from inside a namespace, PHP will not use the same namespace for the included file. Or to put it in other words: Every PHP file must define its own namespace. If it does not, then it will always be importet to the global namespace. Yii’s files do not declare a namespace, so every code inside them will be in the global namespace as soon as you include it.

So i ended up with an acceptable workaround to reach my above goals. The pattern goes like this:




 <?php

namespace yii {

    use \Yii as Yii;  // Required for every core class used inside the code below

    class CHtml

    {

        /* copy the original CHtml code here. If Yii gets upgraded, replace the code in here */

    }

}


namespace {

    class CHtml extends yii\CHtml

    {

        /* override CHtml methods here */

    }

}

The solution is not perfect, but at least it works and still i can upgrade without too much fuss.

If someone has a better idea for how to solve (2) i’m glad to hear it. Maybe we can find the perfect pattern and have a nice wiki article for it.

Your solution involves the copying of the original CHtml code, that’s why I don’t any benefit compared to the following way:

  1. Copy CHtml.php into protected/components/Html.php (also need to rename the class).

  2. Create CHtml.php under protected/components and extend it from Html class.

  3. When the new Yii version is released, update Html.php.

No namespaces magic is involved :)

As i explained above, this would have a drawback: Say, you change CHtml in 10 places. Then Yii gets upgraded. Instead of just copy&paste the original CHtml code into the namespace section above, you now would have to go through the original code again and apply all 10 changes again. Very error prone. I want inheritance instead. Thus the namespaces.

If that’s not an issue for you, you can stop after step 1 above :).

EDIT: Sorry, didn’t read carefully enough (like so often ). Your solution also seems to work fine.

Actually I didn’t use any of these solutions. If you’ll take a look at the Bootstrap extension, you’ll see it has it’s own methods to render form elements. But several times I wanted to extend CHtml, and this class is kinda evil, because either you need to use it “as is”, or create your own :D

Yes, i’ve seen the extension - but i need a more customized solution and don’t like that it basically provides customized versions of CActiveForm etc. Therefore the solution above.

I’ll probably provide a pull request soon, which gives more flexibility for rendering labels from CHtml.

How about not using specialization, but a more aggregation like approach?




class MyHtml

{

  /// As of PHP 5.3.0

  public static function __callStatic($name, $arguments) {

    return call_user_func_array( array('CHtml',$name), $arguments );

  }


  public static function checkBox( $name, $checked=false, array $htmlOptions=array() ) {

    // custom implementation

  }

}



If you require a class that works with earlier versions of php, you could provide a factory method and use the magic method __call instead of __callStatic:




/**

 * Use it like this:

 * MyHtml::i()->checkBox(...);

 * MyHtml::i()->checkBoxList(...);

 */

class MyHtml

{

  private $_instance = NULL;


  public static function i() {

    if (MyHtml::$_instance === NULL) {

      MyHtml::$_instance = new MyHtml();

    }

    return MyHtml::$_instance;

  }

  

  public function __call($name, $arguments) {

    return call_user_func_array( array('CHtml',$name), $arguments );

  }


  public function checkBox( $name, $checked=false, array $htmlOptions=array() ) {

    // custom implementation

  }

}



The problem is, that the extended class needs to be called CHtml. And you can’t have 2 classes with the same name in PHP (without namespaces). So how would you call stuff from the original CHtml in your own CHtml class?

Just do it like you showed in your first post, but programatically instead of manually. This could be done using eval() or maybe by building and including the namespace wrapped CHtml helper from memory (using php://memory stream wrapper).

See http://www.yiiframework.com/forum/index.php/topic/21693-view-and-helper/page__p__106627__hl__autoloader__fromsearch__1#entry106627 for reference.

I also considered using eval() first - but i find this a very hackish solution. I don’t like that one of the core classes has to be read in and the eval’ed all the time. You also have to alter the source code and insert the namespace statement.

All these solutions are awkward, it is much less messy to simply extend the classes you need. Your requirement of a class effectively extending itself is self imposed, find a simpler way :)

… then show one :)

class Html extends CHtml {

}

Simple! Ok it doesn’t meet your requirements of having the same name as CHtml,but imho that is EVIL and will trip you up in future. It’s confusing to have a class called CHtml that is not actually the real CHtml

You do know, that CHtml is used a lot all across the framework? So if you use a CActiveForm, it will use CHtml to render tags. You can not use your custom Html class there. So the whole point of this topic is, how to achieve this.

And my point is that there is no elegant way to achieve what you’re looking for apart from:

  1. Override every class that uses CHtml directly (tedious, but this is the proper way to do it)

  2. Edit the framework code to your needs and keep patching when CHtml changes upstream.

I can see that you really want to avoid both of these, but using eval and/or namespace trickery is far worse in my opinion. If you’re going down that route your might as well use runkit.

I just need it for one of my projects. I don’t want to abandon CActiveForm and i pointed out above, why i don’t want to alter framework files. And that i still want to be able to upgrade with least possible impact. I think, the solution does exactly this. I’m still interested to hear a better alternative.

Just copying over CHtml and doing local changes is not what i wanted. This way i can’t easily find my changes anymore. Therefore the namespace trick. All i have to do now on upgrade is copy & paste the CHtml code into my custom CHtml class into the right section (unless my pull request for CHtml gets accepted ;) ).

But i still think, the described method could come in handy in several situations. E.g. what do you do if CActiveFinder needs an important fix in your project but yii does not have it in it’s official repo?

Can’t you use git for this? Fork the yii repo, add your changes and commit. When upstream changes, pull and resolve the merge conflicts. At least it’s automatic rather than copy and paste

That’s not really applicable to the project i have. And i also don’t want to maintain my own fork of yii just for this project. If yii upgrades you have the same problem again: You have to reapply your changes, maybe fix merge conflicts. The advantage of my solution above is, that you can use inheritance instead. So your changes are separated out and you just have to copy in the original CHtml code.

Don’t get me wrong, i don’t see this as good practice. It’s just a workaround for situations where you want to keep your project code totally separated of Yii - but still need to alter core classes somehow. It should hopefully be an exception.

Mike,

Here are my two cents. I’ve been trying to extend CHtml for some time now but this is pretty much impossible. I know what you mean by not wanting to maintain a huge chunk of code just to restructure a few tags, but since they are all static method any time a method you didn’t extend gets called if it ever calls other methods, it will do so via self:: and even if you extended the next called function it will not be called (self:: is not $this, and there is no this:: yet). So what happens is that you start extending more and more methods just to keep the callstack within your class and it just becomes a nightmare.

I gave up trying to do this. I’m grabbing the whole CHtml class and making it my b*tch own. I’ll just keep patch files handy for future Yii releases.

I found your post looking for "how to replace core classes", brilliant and easy :slight_smile:

Thanks!

JG

Can anybody see this being possible with Traits in PHP 5.4 combined with namespaces maybe.

EG:




<?php

class Base {

    public function sayHello() {

        echo 'Hello ';

    }

}


trait SayWorld {

    public function sayHello() {

        parent::sayHello();

        echo 'World!';

    }

}


class MyHelloWorld extends Base {

    use SayWorld;

}


$o = new MyHelloWorld();

$o->sayHello();

?>



http://php.net/manual/en/language.oop5.traits.php

We Are Doing In These Way

[color="#1C2837"][size="2"]STEP 1 ) Copy CHtml.php into protected/components/Html.php [/size][/color]

[color="#1C2837"][size="2"]STEP 2 ) Create CHtml.php under protected/components and extend it from Html class.[/size][/color]

[color=#1C2837][size=2]STEP 3) When the new Yii version is released, update Html.php[/size][/color]