Flourish PHP Unframework
This is an archived copy of the forum for reference purposes

Code-based validation matching, instead of English strings

posted by rirez 8 years ago

fValidate and other similar systems (like exceptions from ORM) currently all use English phrases like "The value specified must be unique, however it already exists". This is great for rough and dirty error messages, but from a UX standpoint it's a terrible option. For example, on a theoretical membership system, if a user is trying to register an account with an email that already exists, the application should provide a helpful link to the "forgot password" page, instead.

This severely limits the functionality of Flourish for UX purposes. One could simply do a string search (strpos) for that full string, but that's very tedious and not future-proof, if one day the phrasing were to change.

I propose providing an option to output validation errors using codes (such as "E_ORM_EXISTS: field"), which would make catching much easier.

Ideally, fValidationException should have a method to output an associative array of fields and error codes. This provides maximal flexibility in displaying and managing errors. The previous, pre-formatted English version should stay, as the simplicity of using the humanized version is one of Flourish's big benefits. There should also be methods to modify this internal exception array; if a particular field is handled manually by the programmer, it shouldn't be displayed afterwards.

Something such as:

array fValidationException::getErrors();
returns Array({name: 'TOO_LONG', email: 'ALREADY_EXISTS'});

array fValidationException::setErrors(array $errors)

Basically that. This is a rough solution, though; there are probably more elegant methods, such as simply allowing array access to the error list.

Edit: See Message #1636 for a more thought out syntax/format.

I'm agree with you. This works nice... if I only want a list of the errors

} catch (fValidationException $e) {
    fMessaging::create('error', $e->getMessage());
}

but sometimes as you said I just want an array or object of the errors and the following code its too much to achive this

try {
    // ...
    
    $errors       = $validator->validate(TRUE);
    $field_errors = fValidationException::removeFieldNames($errors);
    if ($errors) {
        throw new fValidationException(
            'The following problems were found:',
            $errors
        );
    }
    
    // ...
    
} catch (fValidationException $e) {
    // fMessaging allows for creating session-based one-time-use messages
    fMessaging::create('error', $e->getMessage();
    fMessaging::create('field_errors', $field_errors);
}

I would like to hace something like this

$array = fValidationException::getErrors()
$array (
    'email' => array('At least 12 chars', 'Not allowed'),
    'name' => 'Too short'
)
posted by juanitodelcielo 8 years ago

Ideally, there would be an "fFieldValidationException" class that contains the specific details of each exception. So you could do...

try{
   $user->store();
}catch($e fValidationException){

// < general error catching logic >
$errorMessage = "Oops, something went wrong! ";

// get errors
$errors = $e->getErrors();

// get a specific error
if($errors->hasField('email')){
    // get error object
    $emailError = $errors->getField('email');

    // get various properties (as an example)
    $emailError->getValue(); // "foo@example" - the value
    $emailError->getMessage(); // "The value specified must be unique, however it already exists" - the current English message
    $emailError->getCode(); // "ALREADY_EXISTS" - the error code

    // actual logic
    if($emailError->getCode() == "ALREADY_EXISTS"){
        $errorMessage .= 'This email has already been registered - <a href="#">have you forgot your password</a>?'
        // remove field from array
        $errors->removeField('email');
    }
}

// reset exception array, or, alternatively, create a new exception
$e->setErrors($errors);

// other error handling 
$errorMessage .= " These errors were also found: " . $e->getMessage();

// etc etc
fMessaging::create('error', $errorMessage);

}

EDIT: Changed to a more suitable example.

posted by rirez 8 years ago

Why don't you simply throw an exception if the email already exists ?

// submit method called when user tries to register
// the parent class instantiate the User Model object

protected function submit()
{
	try {		
	
		$validator = new fValidation();
		$validator
			->addRequiredFields('email', 'password', 'password_confirm', 'name', 'prename')
			->addEmailFields('email')			
			->addCallbackRule('password', 'hText::checkTrim', 'Password should be at least 6 caracters long')
		/* .. other validation*/
			->validate();

		//Validation passed
		$this->user->params = array(
			'email' => fRequest::get('email', 'string'),
			'password' => fRequest::get('password', 'string'),
			'name' => fRequest::get('name', 'string'),
			/* other passed parameters to the User Model object */
		);

		// if account with provided email doesn't exist already : account creation
		if (!$this->user->emailExists()) {
			$this->user->create();
			$infos = $this->user->getInfos();
			$this->sendEmailConfirm($infos['id']);
			$this->setSession($infos['id'], $persitent = null);	
			
		} else {
			throw new fValidationException(
				'This account already exists'.
				'<a href="'.URL_PUBLIC.'auth/lostpass">Forgot password ?</a>'
			);
		}
		
	}  catch (fException $e) {
		fMessaging::create('error', $this->currentLink, $e->getMessage());
		$this->render();
	}
}
posted by theyouyou 8 years ago

I'd like to able to release more of that functionality towards the ORM automatic validation mechanism. I often find using a manual fValidation object to be overkill; more often than not, I can simply toss the data straight into the ORM functionality, and it will reject the data if the database finds it invalid. There is, however, no easy way to intercept this validation information from ORM - it comes pre-packaged into the English strings, which is what I'm search of an alternative for.

As a start-off point, this is how it usually works:


try{
     $user = new User(); // set up for fORM
     $user->setUsername(fRequest::get('username'));
     $user->setPassword(fRequest::get('password'));
     $user->setEmail(fRequest::get('email')); // skipping generic email validation
     $user->store();
}catch(fValidationException $e){
     fMessaging::create('error', 'path/to/page', $e->getMessage());
     fURL::redirect('path/to/page');
}

That's how it is now, and I think it should be like the one I posted a couple posts back.

It works remarkably well right now - it lets me rapidly develop applications, because I no longer have to mess around with escaping strings, checking lengths, or checking if that primary key (email) is unique - the ORM system automatically handles that. I wish this custom error list mechanism exists because the data is already there - just not in the format I need it to be in. There's no need for the system to do more work, as the validation is complete. There also needs to be a minimal amount of extra code because, again, the validation has already been done - I just need to catch the error and react accordingly.

posted by rirez 8 years ago

I'm practising Flourish ORM these days and I'm facing the same problem. Any solution since :) ?

posted by younes 7 years ago

I'm considering five different options (I also appreciate wbond's input on which option to take):

  1. I could use a message translation array, and translate error messages into parseable strings. Advantage: Not very much code difference, easy to install. Disadvantage: Limited, probably doesn't mix well with other localizations, and not future-proof.
  1. You could also use the fORMValidation::addStringReplacement function to manually change errors for each kind of fORM class we're dealing with. Advantage: Very accurate and specific, if you only need to do this to a select few classes/fields. Disadvantage: Gets complicated and it's inefficient for large applications.
  1. The one I'm currently using is to use fActiveRecord::validate(TRUE,TRUE), which tosses validation errors into an array where the keys are the fields, and the value is the error message. Then I use an array to translate the error messages into error codes (or you can also just detect them by directly comparing strings). This works well, but you'll need to modify all code so it attempts to ::validate() instead of ::store(). It also works for fValidation objects. Probably the best option right now; you can easily just use the default error message when desirable, but you can also easily detect simple field-to-error conditions without convoluted string manipulation.
  1. I'm considering to write this as a sort of "plugin" which does the functionality I wrote about above. I'm not sure about this because there's no plugin architecture or conventions (so far I simply use the xf prefix, i.e. xfAdvValidation). Advantage: relatively easy to install, easy to work with, infinite flexibility due to being a 3rd party script. Drawback: Not being associated with Flourish, separate maintenance, not necessarily future-proof, and it's quite complex and demanding (lots of string manipulation).
  1. Submit changes to Flourish itself, so the fValidationException object also stores a {field: error} array, much like that used in option 3. Advantage: cleanest, fastest implementation. Drawback: need to go through every single file and find where they use fValidationExceptions, and figure out how to reverse engineer their data. Also not sure how Will Bond handles this sort of thing.

While I do wish we had proper error messages that would make extracting the errors easier, the English strings are more or less okay for now, because we can still use regular expressions on them. A roundabout method, yes, but implementing proper error messages everywhere would be quite a challenge.

posted by rirez 7 years ago

I used a registerHookCallback, according to this : http://flourishlib.com/docs/fORM#CustomValidationUsingaHook

<?php
/** Controller: Registering page*/
class Controller_Auth_Register extends Controller_Auth
{ 
	public function index()
	{
		/**/
	}

	protected function submit()
    	try {
			$user = new User();
			$user->create();
			$this->setSession($user->getId());

		} catch (fValidationException $e) {
			fMessaging::create('error', $this->currentLink, $e->getMessage());
			$this->render();
		}
	}
}



/** User Model */
class User extends fActiveRecord
{
    protected function configure()
    {
		fORMColumn::configureEmailColumn($this, 'email');
        fORM::registerHookCallback($this, 'post::validate()', 'User::validateUniqueEmail');
    }

    public function create()
    {
    	$this
    		->setEmail(fRequest::get('email'))
    		->setName(fRequest::get('name'))
			->setHashedPassword(fRequest::get('password'))
			->store();
    }

    public function setHashedPassword($password)
	{
		$this->setPassword(fCryptography::hashPassword($password));
		return $this;
	}

	
	static public function validateUniqueEmail($object, &$values, &$old_values, &$related_records, &$cache, &$validation_messages)
    {
    	$emailExists = fRecordSet::build('User', array('email=' => fRequest::get('email')))->count();
    	
    	if ($emailExists) {

    		// register
    		if (!$object->getId()) { // isn't stored
    			// exception will delete validation array messages
				throw new fValidationException(__('Email already exists. <a>Lost password ?</a>'));

    		// settings
    		} else {
    			// btw, the __("string") is a gettext function
    			$validation_messages['email_new'] = __('New email isnt available');
    		}
    	}

	}
}
posted by younes 7 years ago

That's pretty useful! Too bad there's no easy way to remove the previous offending error message (or is there - can you override the validation messages?)

We've come up with some good solutions for now, but in the long run, everyone benefits if we can get the functionality into Flourish's core.

posted by rirez 7 years ago

Yes throwing an exception will override the validation messages: The Try/Catch will apparently get the last thrown exception.

posted by younes 7 years ago

I mean, just overriding a particular message. Say you're validating that registration form up there; it'd be nice if you can filter and catch other validation errors as well, while replacing the email error with something else.

posted by rirez 7 years ago
$validation_messages['email'] = "hey I'm a new email error message"

will do.

posted by younes 7 years ago