Link to home
Start Free TrialLog in
Avatar of Bruce Gust
Bruce GustFlag for United States of America

asked on

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

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:

User generated image
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:

User generated image
I only want to "Password" and "confirm password" fields checked if they've been populated. How do I do that?
Avatar of Zvonko
Zvonko
Flag of North Macedonia image

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', 
]);

Open in new window

ASKER CERTIFIED SOLUTION
Avatar of Chris Stanyon
Chris Stanyon
Flag of United Kingdom of Great Britain and Northern Ireland image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of Bruce Gust

ASKER

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!
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'.
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...!
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?
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

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?
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">
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

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!