CDbCriteria extensibility / helpers

I think CDbCriteria might need some useful enhancements for quick shortcuts to certain repetitive tasks - ideally, it should be made extensible, so we can add our own helper methods, and perhaps it could ship with a few built-in extensions…

For example, for substring searches, this does not look good:




      if (isset($keyword)) $criteria->mergeWith(array(

        'condition' => '`post`.`body` LIKE ' . $db->quoteValue('%'.$keyword.'%')

      ));



With a shortcut, this could be done much more elegantly:




if (isset($keyword)) $criteria->submatch('`post`.`body`', $keyword);



The simplest solution (and perhaps most in tune with Yii) would be a call-overload, and a public array where you could configure your helper methods - this configuration could be stored statically with your CDbConnection instance to prevent overhead … for example:




$config = array(

  ...

  // application components

  'components'=>array(

    ...

    'db'=>array(

      'class' => 'CDbConnection',

      'schemaCachingDuration' => 3600,

      ...

      'criteriaHelpers' => array(

        'MyCriteriaHelpers'=>array(

          'myHelperMethod',

          'myOtherHelperMethod',

        ),

      ),

    ),

    ...

    ),

  ...

);



In this example, I’m assuming a MyCriteriaHelpers class with two methods.

Any thoughts?

Agree with you. As a first step, I already added CDbCriteria::addCondition.

Could you please submit a ticket for this feature enhancement?

I’m not sure about criteria helpers though. Do you mean they may set to replace CDbCriteria? How would you intend to use them?

Maybe the word "helpers" was not appropriate here.

I’m merely talking about adding some helpful methods to build certain kinds of criteria more easily. So for example, adding a submatch() method to CDbCriteria might look something like this:




class CDbCriteria {


  ...


  function submatch($column, $substring) {

    $this->mergeWith(array(

      'condition' => "{$column} LIKE " . Yii::app()->db->quoteValue('%'.$substring.'%')

    ));

  }


}



So basically, shortcut methods for repetitive criteria construction - it makes for more legible and straightforward code than a lot of repetitive mergeWith() and array() statements.

Even with something like addCondition(), for certain repetitive tasks, this:




$criteria->submatch('body', $keyword);



looks a lot better to me than this:




$criteria->addCondition("{$column} LIKE " . Yii::app()->db->quoteValue('%'.$substring.'%'));



And if you could mix in your own shortcut methods, perhaps by adding behaviors, that would be helpful in projects when you have complex search logic being used across many different models.

For example, an application with lots of full-text search logic, or a system where lots of different models are tagged using a tab-table, could be greatly simplified by such means.

Adding submatch() is quite easy, there might be an additional third parameter $negate, which would represent ‘not like’ queries. I’m not sure, however, that $criteria->submatch(‘column’, ‘value’, true) is very readable, so I’m open to your suggestions. (Maybe adding a new method?)

On the other hand, behaviors would cause a significant impact on the performance. Can’t these cases be solved only by extending criteria class?

I don’t know in how many places the criteria class is instanciated, perhaps not many?

If so, then yes - if there was some way to do [pre]class DbCriteria extends CDbCriteria[/pre] and tell the framework to use your extended DbCriteria whenever CDbCriteria is normally created. You probably don’t really need more than one set of extensions for a single application.

You might want to consider making some classes generally extensible, so that the framework can meet any requirements for customization of certain classes at the core - take a look at what I did for my own mini-framework:

http://code.google.com/p/shortr/source/browse/trunk/shortr.org/boot.php

Look for ShLoader::map near the bottom of the file - certain framework classes, when we declare them, we name the actual class with an extra underscore, for example:

http://code.google.com/p/shortr/source/browse/trunk/shortr.org/class/ShDBResult.class.php

The class declaration is Sh_DBResult, and ShLoader checks, after require()ing the class, to see if the class was declared.




  ...

    if (!class_exists($class_name, false)) {

      eval("class {$class_name} extends {$class_prefix}_{$class_suffix} {}");

    }



Class prefixes are used for namespacing in this framework, but ignore that, and note what’s happening here - the autoloader attempts to load a class extension, e.g. an application-specific extensions with the prototype [pre]class ShDBResult extends Sh_DBResult[/pre] … if no application-specific extension to the framework class is available, it “renames” the loaded class, by simply eval()ing [pre]class ShDBResult extends Sh_DBResult {}[/pre]

The overhead of this tiny eval() call is negligible, and the performance overhead of having an extra class layer around some classes, when performing function calls or resolving properties, is minimal; I benchmarked PHP to make sure of this.

So this is much faster than any method of runtime decoration in PHP (e.g. behaviors or events), and makes for a simple way to open up the framework architecture to one layer of extension on top of the core framework classes.

Note that you can’t use this technique generally to make modules or extensions that beef up the core framework classes, as only a single layer can be applied - it’s not truly decoration in that sense. But it’s suitable for application-specific extensions, as typically you only have one application loaded/running at a time…