yiisoft/yii — pull request #2181 by cheuschober (2013-03-08 17:02:29+00:00)
Title: Update CModel::createValidators() to use variable CValidator class name and enable overrides in child validator classes

Hi. This is a small patch that sets up a variable class name for the createValidator() call inside CModel::createValidators() in order to allow child validator classes to override CValidator::createValidator() for special case event handler attachment.

-- Long version
I had a situation where I needed to attach an event handler from my validator to its parent class. Specifically, this was a complex validation scenario whereby a set of multiple attributes (for explanation, let's call it set $a) were validated together (eg, order attrs a1, a2, and a3 by order of preference). Since a1, a2, and a3 are required attributes it's preferred that they be in the same model where that requirement may be enforced.

There is a 1:1 relationship between rules and validators, however, validateAttribute() is executed $n times where $n is the number of attributes. I don't have to check uniqueness for the full cartesian of $a^2 -- rather, I need only to check the following pairs:

[[a1,a2], [a1,a3], [a2,a3]].

Setting up that subset is simple enough when the validator is constructed, however, since CValidator::validateAttribute() is called once per attribute I am still being forced check for uniqueness $n times which is inefficient and redundant. Relatedly, if I call CModel::validate() and pass it an array of attributes that includes 'a2' but excludes 'a1' and 'a3', I must still check against the values of 'a1' and 'a3' in order to validate 'a2' as unique within set $a. To avoid executing the checks $n times, I could store a combination counter (CValidator->$i). If the number of total combinations to be executed == $i, I would be able forgo any further evaluation. This is a considerably more optimized approach but has the one failing of requiring $i to be reset before each successive run of validate() and in order to that I would need to hook into CModel::beforeValidate().

So, at this stage I could do two things, create a behavior that acts like a validator (unideal because there's a client validation component I would like to add and because it breaks up validation into rules() and behaviors()) or I could try to fit this into a validator. I did a little digging and noticed that CModel::createValidators() uses a hard-referenced call to CValidator::createValidator() even though it also has the validator class name ($rule[1] = $name) also accessible to it. By changing that call to $name::createValidator() existing functionality is largely unchanged but child validator classes can now override createValidator() and put in extra pieces like attaching event handlers to their parent model. If one wanted to go one step further the beforeValidate() / afterValidate() handlers could just be added to the base class but that seems like a lot of extra overhead for a very rare situation.

All, in all, this is a fairly straightforward patch and I hope it gets pulled. If you're interested in the validator class itself, I can provide that as well.