Jump to content
Juergen

Custom form field validation via hook in init.php or ready.php (How to)?

Recommended Posts

Hello @ all,

I have studied several posts in this forum (fe. https://processwire.com/talk/topic/969-input-validation-in-back-end-fields/?do=findComment&comment=58931), but I am not able to perform a custom validation of one of my input fields

Goal: Check if the date of one field is higher than the other, output an error if the condition doesnt match

Here is what I have tried so far:

$form->addHookAfter("processInput",function($event) {
    $form = $event->object;
    $firstdate = $form->get("date_end");//date field 1
    $lastdate = $form->get("recurringdateend");// date field 2
    if($firstdate >= $lastdate){ //check if the date of field 1 is higher than the value of field 2
        $lastdate->error("Last date must be higher than the first date");
    }
});

I have tried to insert it in init.php and also in ready.php but without success

Has somebody tried to achieve something similar and know how to deal with this situation?

Best regards

Share this post


Link to post
Share on other sites

Oh sorry there was a copying mistake. First line must be

$pages->addHookAfter("processInput",function($event) {

but this doesnt work too.

Share this post


Link to post
Share on other sites

Ok, I have read the docs and I put the following code into ready.php

$pages->addHookBefore('saveReady', function($event){
$form = $event->object;
$page = $event->arguments[0];
 if($page->template == "events"){
        $lastdate = $page->getUnformatted("recurringdateend");
        $firstdate = $page->getUnformatted("date_end");
        if($firstdate > $lastdate){
            $form->error("First date could not be after the last date.");
        }
 }
});

The validation works, but:

  1. The page will be saved, but it should not because there is an error
  2. The error message is not next at the form field. It is only at the top bar. I know that form->error has no relation with the field itself but using fe
    $form->get("recurringdateend")->error....

    doesnt work.

Share this post


Link to post
Share on other sites

That was the wrong way. Do not try to use a hook on Pages, but rather look at ProcessPageEdit and it's hooks. 

Share this post


Link to post
Share on other sites

I took a look at https://github.com/ryancramerdesign/ProcessWire/blob/master/wire/modules/Process/ProcessPageEdit/ProcessPageEdit.module

My code is

$pages->addHookAfter("ProcessPageEdit::processInput", function($event){
$form = $event->object;
$firstdate = $form->get("date_end");//date field 1
$lastdate = $form->get("recurringdateend");//field 2
$this->message("Datum: {$firstdate} ");
});

But if I add this to my init.php or ready.php I dont get the value of date field 1 in the message. So it seems that after submission of the form the value will not be grabed. I only put the message line to the code to see if the hook works.

I dont know what I should do: I use a hook after, I use processInput which is for processing input data, I use the form as my object and I try to grab the values of the form fields with the get() method - In my opinion this should be the correct way.:undecided:

 

Share this post


Link to post
Share on other sites

Try Hooking into the inputfield instead like so. Note, example is from module context. See also notes below:

public function init() {
	$this->pages->addHookAfter("InputfieldDatetime::processInput", $this, "hookAfter");
}

public function hookAfter(HookEvent $event) {

	$field = $event->object;

	if($field->name == 'recurringdateend'){// @note: just a quick example; you might want to hook into 'date_end' as well
		$page = $this->wire('modules')->ProcessPageEdit->getPage();
		#$this->message($field->name);// @note: testing only
		#$this->message($page->id);// @note: testing only		

		/*
		// @note: testing only + FOR INFO
		$oldDate = $page->get($field->name);// existing DB value
		$newDate = $field->value;// incoming/NEW data
		$this->message("old date is: $oldDate");// returns timestamp
		$this->message("new date is: $newDate");// returns timestamp
		*/
		// @note: quick example, not useful to you but a good to know 'how-to'
		// @note: what you really want I suppose is to compare incoming values (from input)
		// @see above for that ($oldDate and $newDate)
		$firstdate = $page->get("date_end");//date field 1
		$lastdate = $page->get("recurringdateend");// date field 2

		if($firstdate >= $lastdate){ //check if the date of field 1 is higher than the value of field 2
			// error here; @note $lastdate is not a field!
			// error example:
			$field->error('Some Error');
		}
	}


}

@Note that in my example we are comparing existing DB values (not very useful). In your case, you want to compare incoming values, right? See the $oldDate and $newDate example. However, you would need to Hook into both 'date_end' and 'recurringdateend' in that case (i.e. run your Hook code if either is the field name). 

Edit2: See complete answer in next post.

Edited by kongondo
more info
  • Like 2

Share this post


Link to post
Share on other sites

Played a bit more with this. The following works for me. I am not sure if it is foolproof and/or if there are better approaches. Please note:

  1. From what I can tell, when saving a page, inputs (fields) will be sent in the order they appear in a template. 
  2. date1 refers to the first date and date2 to the last date.
  3. We need to temporarily store the value of date1 in order to later compare it to date2. I stored the value in a session. You could do it differently, e.g. cache it.
  4. The code doesn't prevent a save of the 2 date fields. You can do that if you wish.
  5. This code is necessarily verbose in places; just to make things clear.
public function init() {
	$this->pages->addHookAfter("InputfieldDatetime::processInput", $this, "hookAfter");
}

public function hookAfter(HookEvent $event) {
	$field = $event->object;

	if($field->name == 'date1' || $field->name == 'date2') {

		// @note: from what I can tell, inputs are sent in the order of the fields in the page
		// ...so, date1 will be hooked into before date2
		$date1 = '';// first date
		$date2 = '';// last date
		$session = $this->wire('session');
		
		// save date1 to session; we'll retrieve it to compare against date2
		if($field->name == 'date1') {
			$date1 = $field->value;
			$session->set('date1', $date1);
		}
		elseif($field->name == 'date2') {
			$date2 = $field->value;
			$date1 = $session->get('date1');
			
			// compare the first and last dates
			if($date1 >= $date2) $field->error("Last date must be greater than the first date");
			//else $this->message("Those dates are fine matey!");// @note: for testing only
			// delete the temporary session date1
			$session->remove('date1');
		}

	}

}

 

Edited by kongondo
  • Like 3

Share this post


Link to post
Share on other sites

Thanks @kongondo, I will give it a try. To me it seems a bit complex using sessions for this. I guess there will be another way, which works like an $input->post validation, but I am struggeling with this problem for a while.

Its also unclear to me why

$form->get("date_end");

doesn´t grab the value of the input field "date_end" after form submission

 

Many thanks for your solution.

Share this post


Link to post
Share on other sites
On 25/01/2017 at 5:53 AM, Juergen said:

Its also unclear to me why


$form->get("date_end");

doesn´t grab the value of the input field "date_end" after form submission

If it's not grabbing it and the fact that we know there is a value sent, it means it is not the correct implementation. In other words, that is not how you grab it :).

On 25/01/2017 at 5:53 AM, Juergen said:

To me it seems a bit complex using sessions for this. I guess there will be another way, which works like an $input->post validation, but I am struggeling with this problem for a while.

The session is only temporary (it is deleted immediately after) since you need to be able to store the value of the first date somewhere in order to do the comparison.  Below is a different approach. We directly check the values in the $input->post. Seems a bit hackish (?) but it works. Here, we make sure 'erroneous' values are not saved. We revert to older values in the database or blanks if there were no saved dates and show a field error on the first date input. Also note that if no first date is input, the field error will be displayed. You can play around with the if() logic to change the behaviour.

public function init() {
	$this->pages->addHookAfter("InputfieldDatetime::processInput", $this, "hookAfter");
}

public function hookAfter(HookEvent $event) {
	$post = $this->wire('input')->post;
	// change date string to timestamp	
	$firstdate = strtotime($post->date);
	$lastdate = strtotime($post->date2);

	// if first date greater than or equal to last date
	if($firstdate >= $lastdate) {
		$field = $event->object;			
		if($field->name == 'date' || $field->name == 'date2') {
			// get the page being edited
			$page = $this->wire('modules')->ProcessPageEdit->getPage();				
			// ensure we don't save 'erroneous' dates; we revert to older saved (db) dates or blank
			$oldDate = $page->get($field->name);
			$field->value = $oldDate;
			// only show error on first date field
			if($field->name == 'date') $field->error("Last date must be greater than the first date");
		}
	}

}

 

  • Like 1

Share this post


Link to post
Share on other sites

Hello @kongondo

thanks for the explanation. I have implemented your first version and it works well, means that it shows the error message at the top bar and next to the field. My previous attempts showed only the error message at the top bar and not next to the field. So using a session is not a problem. I only thought that the same process could be achived with simplier methods. Anyway, it works very well.

I think it will be a good feature for PW if every field has a custom validation field in its field settings where you can add additional php code for validation and you will have access to the api variables.:rolleyes:

Share this post


Link to post
Share on other sites

Hello,

I'm trying to make users email unique and for this I try the following code:

Can someone tell me why the form still saving values even if the form display the email error "Email already in use" ?  I need to block form submission until the user enter a valid email.

<?php namespace ProcessWire;

/**
 *
 * ProcessWire 3.x
 *
 */
class GeneralTools extends WireData implements Module {
  
  public function init() {
    // Ensure Uniqueness of Emails across site
    $this->pages->addHookAfter("InputfieldEmail::processInput", $this, "validEmail");
  }

  /**
  * Ensure Uniqueness of Emails across site
  *
  * @param \ProcessWire\HookEvent $event
  *
  */
  public function validEmail(HookEvent $event) {
    $field = $event->object;

    if($field->name == 'email') {
       // for test: always display field error
    	$field->error("Email already in use");
    }
  }
}

Thanks Guys :D!

Share this post


Link to post
Share on other sites
1 hour ago, Elkin Angulo said:

Can someone tell me why the form still saving values even if the form display the email error "Email already in use" ?

Your module should be autoload in order to use that hook. See the autoload options for the getModuleInfo() method: https://processwire.com/api/ref/module/

  • Like 2

Share this post


Link to post
Share on other sites

Hi Robin thanks for your answer,

Actually my module is on autoload mode (below my module GeneralTools.info.php):

<?php

/**
 * GeneralTools.info.php
 * 
 * Return information about this module.
 *
 */

$info = array(

	// Your module's title
	'title' => "General Tools",

	// A 1 sentence description of what your module does
	'summary' => "General helper functions.",

	// Module version number: use 1 for 0.0.1 or 100 for 1.0.0, and so on
	'version' => 100,

	// Icon to accompany this module (optional), uses font-awesome icon names, minus the "fa-" part
	'icon' => 'cogs',

	// URL to more info: change to your full modules.processwire.com URL (if available), or something else if you prefer
	'href' => '',
  'autoload' => true,
  'singular' => false,
  'requires' => "ProcessWire>=3.0",
);

The after hook "InputfieldEmail::processInput" is triggered well because when I submit the User add/edit form I can see the error message "Email already in use" set by the hook, that part work fine, the problem is that even if the form contains errors (this one ---> $field->error("Email already in use") it update the values. I expect that when I set the field error the form will display the error and the it will not update User values because an error is displayed and the it must be resolved.

Have you a idea why the form is not waiting until the error "$field->error("Email already in use")" is resolve before updating User values?

Thank you so much :).

Share this post


Link to post
Share on other sites
9 minutes ago, Elkin Angulo said:

Have you a idea why the form is not waiting until the error "$field->error("Email already in use")" is resolve before updating User values?

This is deliberate and Ryan has talked about it but I don't have a link handy. Probably because there can be situations where a field contains a lot of data in which case the user should be notified of a problem but not have all their work thrown out.

See this example from @Soma where he shows how you can set a field back to its previous value when you want to reject a submission:

 

  • Like 3

Share this post


Link to post
Share on other sites

:o Processwire never stop to impress me! I love it :D ! Thank you @Robin S for your explications and your time! 

  • Like 1

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...