We help IT Professionals succeed at work.

How can I write some rules in my request that apply only if fields have been filled out?

Bruce Gust
Bruce Gust asked
on
I've got a page that updates an administrator with the option of creating a new password for them.

The  "password" and "confirm password" fields are visible only after hitting a button that toggles their visibility.

Like this:

screenshot
The code looks like this:

@extends('../layouts.satellite')
@section('content')
<!-- Section: intro -->
<style>
	a.pButton {
		color:#fff;
		text-decoration:none; 
		color:#fff;
	}
	a.pButton:hover {
		color:#fff;
		text-decoration:underline; 
		color:#fff;
	}
</style>
<section id="intro" class="intro">
	<div class="satellite-content">
		<div class="container">
			<div class="row">
				<div class="col-xs-12 satellite_row">
					<h5>NOMAS<sup>&reg;</sup> International Admin Display User Page</h5>
					<div class="row">
						<div class="col-xs-12">
						Hello, {{ Auth::user()->name }}!
						<br><br>
						To make any changes to the user that you've just selected, simply edit the content of the fields below and click on "submit."
						<br><br>
						To return to the List of Users, click <a href="{{ route('adminListUsers') }}">here</a>.
						<br><br>
						If you have any questions or need any assistance, contact Bruce Gust at <a href="mailto:bruce@brucegust.com">bruce@brucegust.com</a>.
						<br><br>
						Thanks!
						</div>
					</div>
					<div class="row">
						<div class="col-xs-12"><hr></div>
					</div>
					@if($errors->any())
						<div class="alert alert-danger">
							<ul>
								@foreach($errors->all() as $error)
									<li>{{ $error }} </li>
								@endforeach
							</ul>
						</div>
					@endif
					<form action="{{ route('adminUpdateUser') }}" method="POST">
						<div class="form-group">
							<input type="hidden" value="{{csrf_token()}}" name="_token">
							<div class="row">
								<div class="col-xs-12" style="margin-top:5px; margin-bottom:10px;">
								<b>Admin Permissions</b>: yes 
								@if($user->admin==1)
									<input type="checkbox" name="admin_yes" value="Y" checked>
								@else
									<input type="checkbox" name="admin_yes" value="Y">
								@endif
									&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;no 
								@if($user->admin==1)
									<input type="checkbox" name="admin_no" value="Y">
								@else	
									<input type="checkbox" name="admin_no" value="Y" checked>
								@endif
								</div>
							</div>
							<div class="row">
								<div class="col-xs-12">
									<label for="title">Name</label>
									<input type="text" class="form-control" name="name" id="name" value="{{ $user->name }}">
								</div>
							</div>
							<div class="row">
								<div class="col-xs-12" style="text-align:center; padding-top:10px; padding-bottom:10px;">
									<div style="background-color:#838383; width:90%; margin-top:10px; margin:auto; height:auto; padding:5px; text-align:center; color:#fff; border-radius:10pt;" id="password_button"><a id="show_password" class="pButton">click here to assign a user a new password</a></div>
								</div>
							</div>
							<div id="password_row" style="display:none;">
								<div class="row">
									<div class="col-md-6 col-xs-12">
										<label for="title">Password</label>
										<input type="text" class="form-control" name="password" id="password">
									</div>
									<div class="col-md-6 col-xs-12">
										<label for="title">Confirm Password</label>
										<input type="text" class="form-control" name="confirm_password" id="confirm_password">
									</div>
								</div>
							</div>
							<div class="row">
								<div class="col-xs-12">
									<label for="title">Email</label>
									<input type="text" class="form-control" name="email" id="email" value="{{ $user->email }}">
								</div>
							</div>
							<div class="row">
								<div class="col-xs-12" style="text-align:center;"><input type="hidden" name="user_id" value="{{Auth::user()->id}}"><input type="hidden" name="the_id" value="{{ $user->id }}"><br>
									<input type="image" name="submit" src="{{ asset('assets/img/nomas_submit.jpg') }}" style="width:150px;">
								</div>
							</div></form>
						</div>
					</form>
				</div>
			</div>
		</div>
	</div>
<script>
	$('#show_password').click(function() {
		$('#password_row').toggle(500, function() {
			if($('#password_row').is(':visible')) {
				$('#show_password').text('click here to close password fields');
			}
			else {
				$('#show_password').text('click here to assign a user a new password');
			}
		});
	});
</script>
</section>
@endsection

Open in new window


In my Controller, I want to use a validation request that checks the "password" and "confirm password," but only if those fields have been populated.

Here's my Controller:

   
public function update(UpdateUserRequest $request)
    {
       $user=User::findOrFail($request->the_id);
	   if($request->input('admin_yes')=="Y") {
			$user->admin=1;
		}
		else {
			$user->admin=0;
		}
		$user->name=$request->input('name');
		$user->email=$request->input('email');
		$user->user_id=$request->input('user_id');
		$success='User was successfully updated!';
		
		if($user->save()) {
			return View::make('/admin/displayUser')
			->with('user', $user)
			->with('newUser', 'Here\'s the user you just edited!');
		}
    }

Open in new window


..and here's my "UpdateUserRequest..."

    public function rules()
    {
        return [
            //
	    'name' => 'required | string | max:255',
           [b] 'password' => 'nullable | required | string | min:8 | confirmed'[/b]
        ];
    }
	
	public function messages()
    {
        return [
            'name is.required' => 'name is required!',
			'password is.confirmed' => 'make sure both your "password" and your "confirm password" fields match!'
        ];
    }

Open in new window


I've tried "nullable" and "sometimes" and with every attempt, I get this:

screenshot of edit page
I only want to "Password" and "confirm password" fields checked if they've been populated. How do I do that?
Comment
Watch Question

ZvonkoSystems architect
Top Expert 2006

Commented:
ZvonkoSystems architect
Top Expert 2006

Commented:

And here the documentation:
https://laravel.com/docs/5.4/validation#conditionally-adding-rules

Same page:

$validator = Validator::make($request->all(), [ 
    'person.*.email' => 'email|unique:users', 
    'person.*.first_name' => 'required_with:person.*.last_name', 
]);
Most Valuable Expert 2018
Distinguished Expert 2019
Commented:
Hey Bruce,

Firstly, I'll explain why your current rules won't work.

The 'sometimes' rule will add validation if the property exists in the request. In your case, the 'password' property exists (it's in the form), regardless of whether it's empty or not, so your 'sometimes' rule is always added.

In older versions of Laravel, you could combine nullable and required, but that's been changed. Logically, it makes no sense to require a value and then accept NULL. Plus if you're updating the password, then NULL is not going to be acceptable. so that won't work.

In Laravel, the Validator class has a method called sometimes(). This method accepts a Closure which allows you to add rules based on a condition. If the closure returns true, the rule will be added, if it returns false, it won't be added. You can use this method in your UpdateUserRequest class. You just need to add a new function called validator (this will override the default implementation of it):

public function validator(Factory $factory) {
    $validator = $factory->make($this->input(), $this->rules());

    $validator->sometimes('password', 'required|min:5', function($input) {
        return $input->update_pass == "1";
    });

    return $validator;
}

Open in new window

Basically, that's saying that if your request->input_pass field has a value of "1", then add the password validation rule. Now all you need to do is add that to your form:

<input type="hidden" name="update_pass" value="0"/>

And when you click the "Show Password" button, update the value. When you close the Passwords, change it back.

if($('#password_row').is(':visible')) {
    $('#show_password').text('click here to close password fields');
    $('input[name="update_pass"]').val("1");
} else {
    $('#show_password').text('click here to assign a user a new password');
    $('input[name="update_pass"]').val("0");
}

Open in new window

Bruce GustPHP Developer

Author

Commented:
Chris, I smell what you're cooking and I'm digging it!

First of all, I want to run some concepts by you to make sure I'm tracking with you.

The Request object is invoked by default. It's what allow Laravel to process incoming information. When you create a custom Request, it's then that you're designing some custom validation rules. You're still calling the Request object, you're just now adding a layer of originality to it.

That's significant because I am going to be using a custom Request in this instance. That "UpdateUserRequest" is going to look like this:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Factory;

class UpdateUserRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            //
			'name' => 'required | string | max:255',
            'password' => 'nullable | required | string | min:8 | confirmed'
        ];
    }
	
	public function messages()
    {
        return [
            'name is.required' => 'name is required!',
			'password is.confirmed' => 'make sure both your "password" and your "confirm password" fields match!'
        ];
    }
	
	[b]public function validator(Factory $factory) {
			$validator = $factory->make($this->input(), $this->rules());

			$validator->sometimes('password', 'required | string | min:8 | confirmed', function($input) {
				return $input->update_pass == "1";
			});

    return $validator;[/b]
	}
}

Open in new window


What's in bold is your new code.

When I pop the hood on your new "validation" method, I'm seeing a couple of new dynamics one of which is "Factory." That's a method within the Request object that allows you create to validation rules on the fly, which is what we're doing here.

The custom validation rules are invoked based on the value of a hidden field. The value of the hidden field is altered depending on whether or not the user clicks on the button that reveals the "password" and "confirm password" fields.

Here's my "showUser.blade.php" page:

@extends('../layouts.satellite')
@section('content')
<!-- Section: intro -->
<style>
	a.pButton {
		color:#fff;
		text-decoration:none; 
		color:#fff;
	}
	a.pButton:hover {
		color:#fff;
		text-decoration:underline; 
		color:#fff;
	}
</style>
<section id="intro" class="intro">
	<div class="satellite-content">
		<div class="container">
			<div class="row">
				<div class="col-xs-12 satellite_row">
					<h5>NOMAS<sup>&reg;</sup> International Admin Display User Page</h5>
					<div class="row">
						<div class="col-xs-12">
						Hello, {{ Auth::user()->name }}!
						<br><br>
						To make any changes to the user that you've just selected, simply edit the content of the fields below and click on "submit."
						<br><br>
						To return to the List of Users, click <a href="{{ route('adminListUsers') }}">here</a>.
						<br><br>
						If you have any questions or need any assistance, contact Bruce Gust at <a href="mailto:bruce@brucegust.com">bruce@brucegust.com</a>.
						<br><br>
						Thanks!
						</div>
					</div>
					<div class="row">
						<div class="col-xs-12"><hr></div>
					</div>
					@if($errors->any())
						<div class="alert alert-danger">
							<ul>
								@foreach($errors->all() as $error)
									<li>{{ $error }} </li>
								@endforeach
							</ul>
						</div>
					@endif
					<form action="{{ route('adminUpdateUser') }}" method="POST">
						<div class="form-group">
							<input type="hidden" value="{{csrf_token()}}" name="_token">
							<div class="row">
								<div class="col-xs-12" style="margin-top:5px; margin-bottom:10px;">
								<b>Admin Permissions</b>: yes 
								@if($user->admin==1)
									<input type="checkbox" name="admin_yes" value="Y" checked>
								@else
									<input type="checkbox" name="admin_yes" value="Y">
								@endif
									&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;no 
								@if($user->admin==1)
									<input type="checkbox" name="admin_no" value="Y">
								@else	
									<input type="checkbox" name="admin_no" value="Y" checked>
								@endif
								</div>
							</div>
							<div class="row">
								<div class="col-xs-12">
									<label for="title">Name</label>
									<input type="text" class="form-control" name="name" id="name" value="{{ $user->name }}">
								</div>
							</div>
							<div class="row">
								<div class="col-xs-12" style="text-align:center; padding-top:10px; padding-bottom:10px;">
									<div style="background-color:#838383; width:90%; margin-top:10px; margin:auto; height:auto; padding:5px; text-align:center; color:#fff; border-radius:10pt;" id="password_button"><a id="show_password" class="pButton">click here to assign a user a new password</a></div>
								</div>
							</div>
							<div id="password_row" style="display:none;">
								<div class="row">
									<div class="col-md-6 col-xs-12">
										<label for="title">Password</label>
										<input type="text" class="form-control" name="password" id="password">
									</div>
									<div class="col-md-6 col-xs-12">
										<label for="title">Confirm Password</label>
										<input type="text" class="form-control" name="confirm_password" id="confirm_password">
									</div>
								</div>
							</div>
							<div class="row">
								<div class="col-xs-12">
									<label for="title">Email</label>
									<input type="text" class="form-control" name="email" id="email" value="{{ $user->email }}">
								</div>
							</div>
							<div class="row">
								<div class="col-xs-12" style="text-align:center;"><input type="hidden" name="user_id" value="{{Auth::user()->id}}"><input type="hidden" name="the_id" value="{{ $user->id }}"><input type="hidden" name="update_pass" value="0"/><br>
									<input type="image" name="submit" src="{{ asset('assets/img/nomas_submit.jpg') }}" style="width:150px;">
								</div>
							</div></form>
						</div>
					</form>
				</div>
			</div>
		</div>
	</div>
<script>
	$('#show_password').click(function() {
		$('#password_row').toggle(500, function() {
			if($('#password_row').is(':visible')) {
				$('#show_password').text('click here to close password fields');
				$('input[name="update_pass"]').val("0");
			}
			else {
				$('#show_password').text('click here to assign a user a new password');
				$('input[name="update_pass"]').val("1");
			}
		});
	});
</script>
</section>
@endsection

Open in new window


...and here's my User Controller:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\User;
use App\Http\Requests\CreateUserRequest;
use App\Http\Requests\UpdateUserRequest;
use Illuminate\Support\Facades\Hash;
use View;
use Redirect;

class UserController extends Controller
{
	
	  public function __construct() {
        $this->middleware('checkRole: admin');
    }
	
    public function index()
    {
        $users = User::all();
		return view('admin/listUsers', compact('users'));
    }

   public function insertUser() {
        return view('admin.insertUser');
    }

   public function store(CreateUserRequest $request)
    {
		$user = new User;
		if($request->input('admin_yes')=="Y") {
			$user->admin=1;
		}
		else {
			$user->admin=0;
		}
		$user->user_id = $request->input('user_id');
		$user->name=$request->input('name');
		$user->email=$request->input('email');
		$user->password = Hash::make($request->input('password'));
		$success='User was successfully created!';
		
		if($user->save()) {
			return View::make('/admin/displayUser')
			->with('user', $user)
			->with('newUser', 'Here\'s the user you just entered!')
			->with('adminPermissions', 'yes');
		}
    }

    /**
     * Display the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function show($id)
    {
        $user=User::findOrFail($id);
		return view('/admin/showUser', compact('user'));
    }

    /**
     * Show the form for editing the specified resource.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function edit($id)
    {
        //
    }

    /**
     * Update the specified resource in storage.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function update(UpdateUserRequest $request)
    {
       $user=User::findOrFail($request->the_id);
	   if($request->input('admin_yes')=="Y") {
			$user->admin=1;
		}
		else {
			$user->admin=0;
		}
		$user->name=$request->input('name');
		$user->email=$request->input('email');
		$user->user_id=$request->input('user_id');
		$success='User was successfully updated!';
		
		if($user->save()) {
			return View::make('/admin/displayUser')
			->with('user', $user)
			->with('newUser', 'Here\'s the user you just edited!');
		}
    }

    /**
     * Remove the specified resource from storage.
     *
     * @param  int  $id
     * @return \Illuminate\Http\Response
     */
    public function destroy($id)
    {
        //
		$user=User::findOrFail($id);
		if($user->delete()) {
			return back();
		}
    }
}

Open in new window


Right now I'm getting the same error, as far as the password field can't be null.

A couple of things I'm noticing that I either need to understand better or I'm doing incorrectly.

When I click on the button to reveal the password fields, the hidden value of "update_pass" doesn't change. Not sure I should be able to see that in real time, but I would thing so.

Secondly, the fact that we're returning the "update_pass" value in the "validator" factory seems odd in that it feels like the whole function should be tailored around an IF clause. In other words, IF update_pass = 1, then go ahead fired up. On the other hand, IF it's not equal to 1, it seems like it shouldn't fire at all. Right now it just looks it's returning / dictating a value rather than functioning according to the value.

What do you think?

Thanks!
Most Valuable Expert 2018
Distinguished Expert 2019

Commented:
Hey Bruce.

Right. Quite a few moving parts to your question, so I'll do my best to explain what's going on :)

By type-hinting the Custom Request (public function store(CreateUserRequest $request)), Laravel knows to automatically inject an instance of that class into your method instead of a standard Request instance.

In this new class, we're effectively over-riding the method that Laravel uses to 'get' the Validator. We're passing an instance of the Validation/Factory class into it, which means we can create our own copy of the Validator, manipulate it to our needs, and then pass it on. Now when Laravel needs the validator, it will get our 'custom' copy of it.

A Factory is just a way of creating an instance of an Object. At the top of the class, we're importing Validation/Factory, and then we're type-hinting our validator() method, so Laravel knows to give us a copy of the Validator Factory.

Now when we use the Factory to create that custom copy, we pass in the $rules and $messages, but then we say - hang on a minute. I need to add a new rule, but only if a certain condition is met. We do this by calling on the sometimes() method of the Validator. This method takes a rule definition and a Closure (think of a Closure as an inline function). If that Closure returns true, then the rule gets added to the validator. If it returns false, then it won't get added to the validator. We need to return true or false from that Closure based on whether our request contains a value of 1 or 0 on the update_pass field:

$validator->sometimes('password', 'required | string | min:8 | confirmed', function($input) {
    // return true; -- this would ALWAYS add the rule
    // return false; -- this would NEVER ad the rule
    return $input->update_pass == "1"; // return true if update_pass is 1, otherwise return false;
});

Open in new window

We're only returning the result of ($input->update_pass == "1") to the closure because that needs TRUE/FALSE.

DON'T add the password rule to the $rules array - we need to add it dynamically.

In your blade file, you have the values of the update_pass field the wrong way around. If you're showing the password, it should be set to 1 (to force the password rule to be added). If you're hiding the password fields, then it needs setting to 0 so that the rule is not added.

One other thing of note. Regardless of whether your password is validated or not, the request will still contain the password field. What this means is that if you don't fill in the password (and it skips the validation part), then your request will be valid, but the input->password will be NULL. You obviously don't want to be inserting that into the database:

$user->password = Hash::make($request->input('password'));

What you should be doing is looking at the validated data, and not the request data. In your controller, add this line:

$validated = $request->validated();

Now the $validated array will only contain data that passed validation. If you skipped validating the password, then it won't exist in that array:

if (isset($validated['password'])) {
    $user->password = Hash::make($validated['password']);
}

Hope that all makes sense. Like I said, quite a few moving parts to this, including some Laravel 'magic'.
Bruce GustPHP Developer

Author

Commented:
Taking things one at a time...

I understand the Request object and how by invoking my own "custom" request, I'm given the opportunity to change the rules a little bit to meet my needs.

Got it!

#2 The Validator is a method within the Request object (or it's accessible within the Request object [https://stackoverflow.com/questions/47334353/why-is-validate-method-accessible-via-request]). in this example that we're working on, we're overriding that so we can create a validation rule on the fly depending on whether or not the Closure within our function returns true.

Question: When we override the Validator object, is that something that applies to things in a global fashion? In other words, do I have to put all of my rules within the new validator function including the name and email in order for everything to be checked, or is it just something that applies to the field I specify?

I looked up Closure and from what I can gather it's a PHP function that behaves in a way that's very similar to a JavaScript callback - at least from the standpoint that it's an anonymous function and it's result affects the function that it's a part of (https://www.youtube.com/watch?v=usRo_p1exBQ).

The part that had me confused was the fact that you were returning a value within the validator method not realizing that the "truth-y" aspect of what was being returned would dictate whether or not the validation rule would be applied. Once I grasped that part of it, it made total sense.

As of right now it's working! However, even when I am supremely confident that both the "password" and the "confirm password" fields match, I keep getting an alert to say that they don't match.

Any idea why?

Thanks so much for your time and expertise, Chris...!
Most Valuable Expert 2018
Distinguished Expert 2019

Commented:
Hey Bruce,

Sounds like you're getting a handle on it.

Regarding your question about over-riding the validator() method. You don't need to specifically add your other rules. If you take a look at the Factory method we use to create the Validator instance:

$validator = $factory->make($this->input(), $this->rules());

Open in new window

You'll see that the new instance ($validator) has had your rules passed into the make() method, so it will already have your rules applied.

Regarding the Closure, yes it's used as a callback. In the case of the sometimes() method, it is expected that the callback returns a boolean (true/false), and you're correct in identifying the truthiness of the return vale. Basically, return $input->update_pass == "1" is just shorthand for:

if ($input->update_pass == "1") {
    return true;
} else {
    return false;
}

Open in new window

Not sure why your password confirmation is not working. May need to dig a little deeper into that one.

Just out of interest, what version of Laravel are you running?
Most Valuable Expert 2018
Distinguished Expert 2019

Commented:
Hey Bruce,

Something else I just realised. When we create a new instance of the validator, we're passing in the input and the rules, but we're not passing in the messages. We need to pass those in when we call the Factory method, so that needs changing to:

$validator = $factory->make($this->input(), $this->rules(), $this->messages());

Also, your messages look off. Don't know where you're getting the is.confirmed syntax from, but pretty sure it's wrong. The syntax for the message key should be attribute.rule, so like this:

public function messages() {
    return [
        'name.required' => 'name is required!',
        'password.confirmed' => 'make sure both your "password" and your "confirm password" fields match!',
    ];
}

Open in new window

Bruce GustPHP Developer

Author

Commented:
Chris, I was able to figure out why my password confirmation alert kept popping up.

Bottom line: The "confirmed" portion of the rules array expects the "password_confirmation" field to be named "password_confirmation." If it isn't, then it's going to throw that error.

One last question...

When you pass your messages into your validator function, how is that going to look?

I tried this:

	public function validator(Factory $factory) {
			$validator = $factory->make($this->input(), $this->rules(), $this->messages());

			$validator->sometimes('password', 'required | string | min:8 | confirmed', 
			'password.confirmed', 'make sure both your "password" and your "confirm password" fields match!',
			function($input) {
				return $input->update_pass == "1";
			});

    return $validator;
	}

Open in new window


...and it threw an error:

Argument 3 passed to Illuminate\Validation\Validator::sometimes() must be callable, string given, called in C:\wamp64\www\new_nomas\app\Http\Requests\UpdateUserRequest.php on line 49

How do I accurately incorporate the messages into my validator function?
Most Valuable Expert 2018
Distinguished Expert 2019

Commented:
Hey Bruce,

Strangely enough, I just had a password confirmation problem on my own system, even though the passwords were a match. Just checked your code and I'd done exactly the same as you (I think I've read your code too much!)

Basically, for the password confirm rule to work, your confirmation field has to be named password_confirmation. Yours is called confirm_password!

<input type="text" class="form-control" name="confirm_password" id="confirm_password">

Should be

<input type="text" class="form-control" name="password_confirmation" id="confirm_password">
Most Valuable Expert 2018
Distinguished Expert 2019

Commented:
Haha! Looks like we figured that out at the same time :)

The error you're getting is because you've changed the arguments you pass to the sometimes() method. There was no need to change that:

You need to call it with ($attribute, $rule, $callback), like so:

$validator->sometimes('password', 'required | string | min:8 | confirmed', function($input) {
    return $input->update_pass == "1";
});

Open in new window

We're passing the messages into the make() method, so they already get set based on whatever your messages method returns:

$validator = $factory->make($this->input(), $this->rules(), $this->messages());

And if you look at the messages() method, you'll see this:

public function messages() {
    return [
        'name.required' => 'name is required!',
        'password.confirmed' => 'make sure both your "password" and your "confirm password" fields match!',
    ];
}

Open in new window

Bruce GustPHP Developer

Author

Commented:
Zvonko, thanks for weighing in!

Chris, your time and expertise is appreciated more than you know! I wanted to do you the courtesy of wrapping this question up and giving you the credit you're due, but I do have another question which is at https://www.experts-exchange.com/questions/29171124/Why-is-my-password-field-value-not-being-accurately-processed.html. I ran into a problem with "$user->password = Hash::make($validated('password'));" and you'll see it explained in the question itself.

Thanks, again!