View and helper

This is actually an old topic. We had some discussion before (http://code.google.com/p/yii/issues/detail?id=121), but still haven’t reached a conclusion yet.

Basically, using static classes as helpers have two main drawbacks:

  1. Because in Yii 2.0, we will use namespace, it will be troublesome to use static classes as helpers. For example, instead of writing ‘CHtml::textField’ in Yii 1.x, we will need to write ‘yii\web\helpers\Html::textField’ unless we use the namespace beforehand in the view script.

  2. Static classes are difficult to be customized and extended.

Related with this we also have a design decision to make regarding “view”. Currently, views in Yii are just PHP scripts. There’s no view object (note that view renderer is a different thing). Questions are:

  1. Should we create a view object in Yii 2.0?

  2. If so, how can we maintain the current simplicity and efficiency in view usage?

About helpers:

Well, I think the way the helpers are working right now is great, however, if views are objects this functions can be moved to the View object.

About views:

I think it is a good idea, however, we need to get a way to keep the “views” as PHP-free files as much as possible IMHO, most of the designers I’ve been working with love the way Yii handles views.

Idea:

In MyController class we could call a MyView class that handles the views… someting like:


MyView::render('index', $params)

Well, this is the ‘old-style’ call… now we could be able to do something like:


MyView->index(array($params));

Best regards,

Ricardo

I agree

However, I do not like the sintax you proposed, something like this would fit better




//class CBaseController

public $viewClass='CView';

//controller

$this->render('MyView',array('params'),array('return'=>true,'processOutput'=>false));

//or

$this->render(array(

  'viewFile'=>'myView',

  'viewData'=>array( 

 	'params',

  ),

  'processOutput'=>false,

  'return'=>true,

));



the 1st implementation, render will create an instance of CView using the 3rd parameter to override class default properties, the first parameter to define the viewFile and the 2nd parameter to define the viewData

the 2nd one pass 1 argument that will be used by createComponent, in addition to ‘class’=>$this->viewClass

I would go for the 1st one, as it is simpler

As for the helpers, I agree with Ricardo

but I haven’t reached a conclusion about how the implementation should be done, as it is need be used by widgets, views, and sometimes, outside those 2

Most likely that the way Y!! proposed fits better


Yii::app()->html->encode($sometext);

and create a shortcut for it in the views and widgets


$helper->html->encode($sometext);

and you can define more helpers for a specific view like




//controller

$this->render(

   'myView',

   array('param'=>'value'),

   array(

 	'helpers'=>array('myHtml'=>'MyHtmlClass')//as string=class, as array = parameters to create a component

   )

);

//view class, before render

if(isset($this->viewData['helper']))

   throw new CException('helper is a reserved word');

// view file

$helper->myHtml->myHtmlMethod($doSomething);



also, to access controllers/widgets in view files you define its owner when creating and use it like


echo $this->getOwner()->layout;

Hope this helps

Gustavo

after re-read the post you sent and think a little more, this way is better

stay with app helpers


Yii::app()->html->encode($sometext);

most like you proposed in the post, but instead of put it in the controller put it under CView

define ‘getHelpers’, ‘setHelpers’ and also ‘helpers’ as default class helpers and override CView __get


//not tested

function __get($name){

  if(isset($this->_helpers[$name])){

      if(is_object($this->_helpers[$name]))

          return $this->_helpers[$name];

      return $this->_helpers[$name]=Yii::createComponent($this->_helpers[$name]);

  }

  if(Yii::app()->hasComponent($name))

      return Yii::app()->getComponent($name);

  return parent::__get($name); 

}



use it in view files like




$this->html->encode($text);



the problem with this implementation

defined helpers are not singletons across view files (unless you define it as an object) , only application helpers are

We could use Yii::app()->setComponent($name,$value) instead, which would lead to conflicts between view defined helpers and application components

Still, this is a way to go

Gustavo

Will YiiBase and Yii be still available as static classes?

If yes, they could be used to delegate requests for helpers to the application instance, which in turn could be used as factory for the requested helpers.

Usage would then be somewhat like the following:




  Yii::html()->whatever();



This could be realized by implementing the magic method __callStatic (available since PHP 5.3).

Implementation (not tested):




class YiiBase

{

  // treat the unknown static method as request for a configured helper class

  public function __callStatic( $name, $arguments )

  {

    // call application's factory method with the name of the requested helper as argument

    return self::app()->getHelper( $name );


    // or maybe this way to allow

    // @code

    // Yii::html($special)->whatever()

    // @code

    //

    //return self::app()->getHelper($name,$arguments);

  }

}


class CApplication

{

  // helper configurations.

  // possibility to override in config.php

  public $helpers = array(

    'html' => array(

      'class' => '@yii/helpers/CHtml.php',

      'style' => 'xhtml',

      [...]

    ),

  );

  

  // factory for lazy instantiation of helper objects

  public function getHelper($name /* ,$arguments */)

  {

    if (isset($this->helpers[$name]))

    {

      // if the object hasn't already been created, do it now.

      if (!is_object($this->helpers[$name]))

        $this->helpers[$name] = $this->createHelper( $name, $this->helpers[$name] );


      // return helper object

      return $this->helpers[$name];

    }

    

    // unknown helper

    return null;

  }


  protected function createHelper( $name, $config )

  {

    // create and initialize the helper object based on its configuration

  }

}



This should provide helpers that are lazy instantiated singletons which could easily be customized and are easy to access.

[font=arial, verdana, tahoma, sans-serif][size=2]@Ben

Good idea, thumbs up. OR what if we use a syntax like this:[/size][/font]


Yii::Html::func();

or something like this:


Yii::Html->func();

Is it possible, I’ve not tested these things on PHP 5.3.

Regards.

Ricardo

@Ben

I like the idea of __callStatic, that could also be used to get components, and helpers could be components, as there is no difference between a component and a helper, correct me if I’m wrong

@Ricardo

This is not possible, as there is no such method as __getStatic

Hm, the first one won’t work, because Html is neither a function nor a property. The second one could possibly work if written as “Yii::$Html->func();” (accessing a static property). But I’m not sure if __get works in a static context…

However, your first example looks like calling a plain function in c/c++ using namespaces. Maybe this would be the easiest solution for helpers: simply a collection of functions (and maybe also variables) defined in their own namespace (are static helper classes any more than that?).

Namespaces could be extended with new functions by the user. But on the other hand, autoloading and method overloading wouldn’t be possible…

Well, probably my message won’t help much, but I’m against any new syntax which will make me write more code than CHtml::encode().

I like the idea of view objects, because inside views I can write: $this->encode($var); easy and logical. But I can’t suggest the best realisation of this… by now :P

I think having a View object could make mach sense and make the overal OO design more clean. Such a View class could act as container for the rendered output and also take away lot of rendering and post processing logic from the controller. Thus i don’t think it would affect efficiency much (but can’t really tell). The View object could also keep all view related properties: variables, layout, registered clientscripts, Css, etc. And of course contain the HTML helpers mentioned above.

About keeping simplicity, how about something like this:




// In Controller action:


// We could obtain a view object in controller:

$view=$this->createView('edit');


// ...  render() 

$view->render(array( /* view data */ );


// ... OR: renderPartial() it:

 $view->renderPartial(array( /*... */ );




// For convenience we could provide a backward compatible method:

$this->render('edit', array( /*... */ ));   // implicit call to the methods above



@andy_s

I agree, That’s why I don’t like Symfony (and some another reasons :P) We need to keep CHtml implementation as simple and short as possible.

@Mike

I’d prefer something like this:




MyView::render('edit', array(/*... */));



What do you think?

Best regards,

Ricardo

I guess this is true. I can’t see any difference between those new, no longer static helpers and other application components.

So basically, no new syntax would be needed at all. Simply configure helpers as components. Done.


About the views:

I really like the current script-like views. And I believe the simpler they become, the better. I’m not sure what the new view-objects are intended to do. If they are only meant as containers around the view files like Mike suggested, then they’re certainly a good idea (taking care of layout assignment, rendering, …).

But the actual view itself should remain a simple html (xml, xforms, …) script with as little php code inside as possible.

I’ve argued against the static helper classes before, and I know from building several large projects with Yii that attempting to extend or override functionality of the helpers is a pain. Static helpers, in my opinion, need to be eliminated - helpers need to be concrete classes so we can extend/override as needed.

So the question is where/how to configure the helpers.

I’m with the school of not introducing a bunch of new concepts - but rather use what Yii already provides. So adding your helpers as application components gets my vote - it’s the simplest approach, and it’s already supported and well-understood by anyone using Yii.

The question then is how to access the helpers most conveniently. We should benchmark the proposed solution by comparing to the existing solution.

What we have currently looks like this:


Hello, <?=CHtml::encode($name)?>

If the HTML helper was an application component, it would look like this:


Hello, <?=Yii::app()->html->encode($name)?>

Clearly, that’s ugly and cumbersome, and involves a lot of overhead in terms of resolving the clasname, static method, application component and then finally the encode() method, on every single invocation. So that’s not acceptable.

Someone pointed out that you could add the helpers to the controller, so then you’d have something like this:


Hello, <?=$this->html->encode($name)?>

That’s slightly better, but still looks cumbersome and messy compared to the static method-call.

In terms of performance, the shortest path to the html helper would be via a local variable:


Hello, <?=$html->encode($name)?>

This is better in terms of performance, as it’s just calling a method on an object - it’s also shorter and easier to type than any of the other options.

In my opinion, that would be the best approach.

And I’m aware of the issues with this approach - like the fact that the helpers are now occupying the same namespace as the view-data, so that view-data could collide with your helpers. I don’t see this as being a really big problem, and you can work around that, depending on how the helpers are introduced into the view-data.

The other problem is the fact that you would need to construct and initialize any required view-helper application components prior to rendering the view. Typically, that’s going to mean one or two components, which would be required by the majority of views anyway, so I don’t see that being a drastic issue either - and we’re only talking about constructing these components once during any request that renders a view, so it doesn’t seem like that’s going to cause any real performance impact.

As far as how to get the helpers into local variables in the view, I would suggest adding a CViewRenderer::$helpers property - which by default might contain something like array(‘htmlHelper’ => ‘html’), e.g. a map where application component id => view-data variable name.

Since this means that collisions between view-data and helpers could occur, I would suggest throwing an exception if controller/action code attempts to overwrite a helper with another value.

On a note of backwards compatibility - I would say, don’t worry too much about it. This being a major framework update, we should feel free to architect as needed, without worrying too much about backwards compatibility - we should favor comfort, speed and good practices of new applications and new developers, over migrations of legacy applications. What’s much more interesting is the new possibilities and improvements, paving the way for the future - building a new app should be less effort and more joy, and if that means that migrating a legacy app takes more effort, so be it. Just my personal opinion :slight_smile:

If somebody does need to upgrade a project, this can be done quite easily by doing a replace-in-files on their view-folders, replacing "CHtml::" with "$html->".

That’s all I’ve got on this subject at the moment.

CakePHP allows you to configure helpers in the controller, like this:


var $helpers = array('Html','Ajax','Javascript');

Then they can be accessed like this:


$ajax->link(whatever);


$html->method(whatever);

I think that’s both flexible and succinct.

I wonder if it was possible to configure “accessor classes”… Don’t know how to name them. Classes that only serve the purpose to access data from elsewhere. Imagine if we could use the helpers like now:




echo CHtml::textField(...);



But without an CHtml class being actually declared. So the autoloader fires. It checks:




if (isset(Yii::app()->accessor['CHtml']))



And if such an accessor is defined, it creates an empty class that only implements the __callStatic based on application configuration:




  /* Accessors configuration:

   * [id] => [access code]

   *

   * [id]          := name of the class you want to use statically

   * [access code] := code that delegates the method call to an existing object

   *                  ($method and $arguments are valid variables in its context)

   */

  'accessor' => array(

    'CHtml' => 'return Yii::app()->getComponent( "HtmlHelper" )->$method( $arguments );',

  ),



I don’t know if or how this could be realized. Maybe by using eval():




public static function autoload($className)

{

  [...]

  if (isset(Yii::app()->accessor[$className]))

  {

    $accessCode = Yii::app()->accessor[$className];


    eval(

      class $className {

        public function __callStatic($method, $arguments) {

          $accessCode;

        }

      }

    );

  }

}



Or maybe by including the template from php://memory

Basically:

[list=1]

[*]generate code

[*]eval/ include code in current context

[/list]

So if this worked, it could be a solution which provides all the features that are requested plus would be backward compatible.

You use it like a static object method, what happens behind the scenes is, that a factory creates an object based on the name of your "static class" and passes the method call to it.

On the negative side:

  • lots of dark magic

  • possibly class name clashes instead of variable name clashes

  • runtime overhead

////////////////////////////////////////////////////////////////////

//EDIT:

I cant believe it, it really works! :lol:

Here is what I added to my test project.

application configuration:




	// application components

	'components'=>array(


    'accessor'=>array(

      'class' => 'application.components.Accessor',

      'accessor' => array(

        // through "MyTest::", call methods from the accessor component itself

        'MyTest'  => 'return call_user_func_array( array(Yii::app()->getComponent(\'accessor\'),$method), $arguments );',

        // through "MyTest2::", call methods from the CWebUser instance

        'MyTest2' => 'return call_user_func_array( array(Yii::app()->getComponent(\'user\'),$method), $arguments );',

        // through "NewHtml::", utilize current static CHtml helper

        'NewHtml' => 'return call_user_func_array( array(\'CHtml\',$method), $arguments );',

      ),

    ),



The accessor component:




class Accessor extends CApplicationComponent

{

  public $accessor = array();


  public function test()

  {

    echo "test from Accessor";

  }


  public function test2( $param1, $param2 )

  {

    echo "test2 from Accessor. param1: '$param1', param2: '$param2'";

  }


}



Quick’n’dirty in a view:




function accessorAutoLoader( $className )

{

  $accessorComponent = Yii::app()->getComponent('accessor');


  if (isset($accessorComponent->accessor[$className]))

  {

    $accessCode = $accessorComponent->accessor[$className];


    $classTemplate = "

    class $className

    {

      public static function __callStatic( \$method, \$arguments )

      {

        $accessCode

      }

    }";


    eval( $classTemplate );

    return true;

  }

  else

    return false;

}


YiiBase::registerAutoloader('accessorAutoLoader');


MyTest::test();

echo '<br />';

MyTest::test2( 'Hello ', 'World!' );

echo '<br />';

echo MyTest2::getName();

echo '<br />';

echo NewHtml::htmlButton('PHP rocks!');



Output:

I would strongly discourage the use of a "black magic" auto-loader with eval(), but I do find certain aspects of this idea to be interesting.

I see this being mostly useful for backwards compatibility purposes - for instance, if you look at what I described above, having the helpers available simply as application components, you could do this:




class CHtml

{

  public function __callStatic($method, $arguments) {

    return call_user_func_array(array(Yii::app()->htmlHelper, $method), $arguments);

  }

}



Assuming the instance-methods of the new HTML-helper are compatible with the static methods of the old CHtml class, this thin class should have CHtml working just the way it always did, but now with the added benefit of being able to extend/replace the underlying helper-implementation.

I would not recommend using this as the primary/recommended approach - HTML helper methods get called many times during every request, and this will add overhead. It also makes it less obvious how to reach the HTML helper instance, in case the new instance-based helper has properties or components you need to access - the static calls only facilitate method-calls.

So as mentioned, I find this interesting mainly for backwards compatibility purposes, not as a general pattern for a 2.0 approach that would get my recommendation.

But I could see you including this thin compatibility class - perhaps marking it as deprecated. Even if it’s not 100% compatible with ye olde CHtml, it still might help speed up application migrations.

I agree. Using the component-style approach is far better, because it is well known, suits most our needs (don’t like the lengthy call if used as a helper) and is easier to understand. Also a big problem with such generated classes would be, that they wouldn’t be supported by IDEs. It’s just that I had this idea and wanted to know if it’s possible. ;)

A set of legacy facades could be provided as a package, so they don’t need to be in the core.

But I’m still thinking about a way how the helpers could be made accessible without the danger of variable name collisions, and without a lengthy syntax…




// ultimate goal: 7 chars (or less) to reach the method

CHtml::encode()


// avoid clashes by convention (yii prefix reserved), using __get: 16 chars

$this->yiiHtml->encode()


// using components: 18 chars

Yii::app()->html->encode()



jacmoe’s solution is something that anyone can do if he wishes.




// in controller

$view->render(array(

 $model,

 'html' => Yii::app()->html,

));


// in view (7 chars! finally!)

$html->encode()



But I don’t want to type that over and over again. I think there should be general way of accessing them with short syntax.

I really like Mike’s approach but I would make a slight difference:




$view = $this->getView('index');

$view->render(parametersarray);

// or

$this->view->render(parametersarray);



One of the things i like from Yii is its clean and easy code design… Yii 2.0 should maintain that…

Edit:

By the way, static classes are fast, non-static able to extend/override… is Yii going to loose speed in order to satisfy some needs? Can we find a way where both are satisfied? I truly won’t like to loose speed…

I really like the idea of __callStatic, why not using from the CHtml object itself? can it be part of the CComponent? Would it be confusing? Maybe a CHelper base class extended from CComponent could have that __callStatic and then the rest of the helper extend from CHelper having __callStatic within… so It will depend on the programmer whether to use static methods or not

Second!

Me neither, so I did a quick benchmark - check the results for yourself, and check my code, but from what I could gather, static method-calls are about 25-30% slower than instance method-calls.

Reality in PHP is often the opposite of what you might logically assume :slight_smile:

My guess is, the introduction of namespaces in recent version of PHP has added some overhead to static method-calls…

EDIT: of course, if you have a static compatibility class as I proposed, that will add overhead due to two method-calls and magic being invoked on every call. But as I emphasized, this was meant as a helpful means of getting through a migration, not an end-solution.