laracasts / Validation

Easy form validation.
https://packagist.org/packages/laracasts/validation
MIT License
150 stars 37 forks source link

Validating Unique Values on Update #6

Closed adunk closed 10 years ago

adunk commented 10 years ago

For example, on updating user account information. In the docs it looks like you can add a id to a unique column to ignore validation like so:

'email' => 'unique:users,email_address,10'

Does this package support this functionality? If so how would one implement this?

Thanks!

carvefx commented 10 years ago

we do it somewhat simplistically, but it works.

First of all you have two options: 1 class per action i.e. UpdateUserForm and CreateUserForm, or something similar to what we did below, where the rules var is set when calling a certain method.

if you look at the validateUpdate($id, array $params) method, you will notice that we simply append the id to the $update_rules and then send it off to @JeffreyWay package ;)

class UserForm extends FormValidator{
  /**
   * @var array
   */
  protected $create_rules =
    [
      'name'                  => 'Required|Min:3|Max:255|Regex:/^[a-z-.]+( [a-z-.]+)*$/i',
      'email'                 => 'Required|Between:3,255|Email|unique:users',
      'password'              => 'Required|Between:8,32|Confirmed|Regex:/^([a-z0-9!@#$€£%^&*_+-])+$/i',
      'password_confirmation' => 'Required|Between:8,32'
    ];

  /**
   * @var array
   */
  protected $update_rules =
    [
      'name'                  => 'Required|Min:3|Max:255|Regex:/^[a-z-.]+( [a-z-.]+)*$/i',
      'email'                 => 'Required|Between:3,255|Email|unique:users',
      'password'              => 'Between:8,32|Confirmed|Regex:/^([a-z0-9!@#$€£%^&*_+-])+$/i',
      'password_confirmation' => 'Required_With:password|Between:8,32'
    ];

  /**
   * @var array
   */
  protected $rules = [];

  /**
   * @param array $input
   * @return mixed
   */
  public function validateCreate(array $input)
  {
    $this->rules = $this->create_rules;
    return $this->validate($input);
  }

  /**
   * @param int $id
   * @param array $input
   * @return mixed
   */
  public function validateUpdate($id, array $input)
  {
    $this->update_rules['email'] = "Required|Between:3,255|Email|unique:users,email,$id";
    $this->rules = $this->update_rules;
    return $this->validate($input);
  }
} 
drehimself commented 10 years ago

Thanks @carvefx your code works great :)

carvefx commented 10 years ago

If anybody finds a more elegant solution, I'd love to see it in action, I feel dirty about appending strings directly, seems so low-level :P

adunk commented 10 years ago

Thanks! This helped me a lot. Glad to also see someone else was looking for this functionality.

matyhaty commented 10 years ago

Is this really closed.

Seems messy to me, and obviously is over riding methods which really should be able to cope with this....

@JeffreyWay is this something you could look at and maybe address is Laracasts (Which is excellent)

Could the validator function accept a second param, array which has key (name of form element which has unique requirements) => $id.

From here, the $rules could be editted???

matyhaty commented 10 years ago

Maybe a slight improvement..

<?php

namespace Teamleaf\Depots\Forms;

use Laracasts\Validation\FormValidator;

class DepotForm extends FormValidator
{
    protected $rules = [];
    /*
     * Validation rules for the status form
     * @var Array
     */
    protected $create_rules = [
        'title'         => 'required|unique:depots',
        'code'         => 'required|unique:depots',
        'address'       => 'required'
    ];

    protected $update_rules = [
        'title'         => 'required|unique:depots',
        'code'         => 'required|unique:depots',
        'address'       => 'required'
    ];

    /**
     * @param array $input
     * @return mixed
     */
    public function validateCreate(array $input)
    {
        $this->rules = $this->create_rules;
        return $this->validate($input);
    }

    /**
     * @param int $id
     * @param array $input
     * @return mixed
     */
    public function validateUpdate(array $input, $id)
    {
        $this->update_rules['title'] .= ',title,'.$id;
        $this->update_rules['code'] .= ',code,'.$id;
        $this->rules = $this->update_rules;
        return $this->validate($input);
    }
} 

and

public function update($id)
    {
        $inputs = Input::get();
        $inputs['userId'] = Auth::id();
        $inputs['id'] = $id;

        $this->depotForm->validateUpdate($inputs, $id);

        $depot = $this->execute(UpdateDepotCommand::class, $inputs);

        Flash::message("Your depot has been updated!");

        return Redirect::to('/depots');
    }

Realistically the passed $id needs to be $overrides which should be an array for multiple options, e.g. two unique ids in different tables etc.

laracasts commented 10 years ago

Hey, guys -

Firstly, I very quickly scanned this thread, so if I missed something, let me know. (Sorry, in a hurry.)

I'm not sure if the package needs to provide a solution for this. You can add a helper method to your form object to set an excludes on the appropriate column. Something like:

public function excludeUserId($id)
{
    $this->rules['username'] = "required|unique:users,username,$id"

    return $this;
}

Then, you can do $this->updateAccountForm->excludeUserId(Auth::id())->validate();

schulzefelix commented 10 years ago

Hi, at first I definitely would love to see that implemented.

I extend your FormValidator class like this (maybe not the best way to do the replacement)

public function setCurrent($id)
    {
        $this->currentId = $id;
        return $this;
    }

    public function validate($formData)
    {
        $formData = $this->normalizeFormData($formData);

        if( is_numeric($this->currentId))
        {
            $rules = $this->getValidationRules();
            foreach($rules as $key => $value){
                $rules[$key] = str_replace(':current', $this->currentId, $rules[$key]);
            }
            $this->rules = $rules;

        } else
        {
            $this->rules = $this->getValidationRules();
        }

        $this->validation = $this->validator->make(
            $formData,
            $this->rules,
            $this->getValidationMessages()
        );

[...]

Then you can use in your rules

'email' => 'required|email|unique:users,email,:current',

and in your Controller:

$this->updateUserForm->setCurrent($id)->validate($input);

I hope you will include something like that in one update, also if its only with one id at the moment.

Felix

ShaneG commented 10 years ago

This project has the functionality and seems to work pretty neatly, albeit doing a few too many tasks in a single class:

https://github.com/philipbrown/magniloquent

huglester commented 10 years ago

@JeffreyWay I think it would be good to implement something like this into the package.

for example me personnaly, can't remember any project, where this unique thing would not be required...

for now I just created something like this

<?php namespace App\Forms;

use Laracasts\Validation\FormValidator;

class CarColorForm extends FormValidator {

    protected $rules = [
        'name' => 'required|unique:car_colors,name,{id}',
        'hex'  => 'required|unique:car_colors,hex,{id}',
    ];

    public function validateUpdate($id, array $formData)
    {
        $search = [',{id}'];
        $replace = [''];

        if ($id)
        {
            $replace = [','.$id];
        }

        foreach ($this->rules as $k => $rules)
        {
            $this->rules[$k] = str_replace($search, $replace, $rules);
        }

        return parent::validate($formData);
    }

}

and I just call $this->$this->carColorForm->validateUpdate($id, $input);

but probably funtionality like this out of the box would be perfect :)

Thanks

gravitano commented 9 years ago

I think the $rules property would be great if replaced with the rules method. As same as the Form Request class in laravel 5.

If you define the validation rules in a method. You can do something like this.

public function rules()
{
     return [
         'username' => 'required|unique:users,username,' . Auth::id()
     ];
}

This is only a suggestion. But, this is a great package.

leothelocust commented 9 years ago

@JeffreyWay how would you now do this with Laravel 5? Since the "YourCustomFormRequest" has no knowledge of the input data, how might you pass an $id to the Request?

Particularly, when it's not something you can conveniently grab like @gravitano suggested above.

What am I missing?

leothelocust commented 9 years ago

I mean, this works:

public function rules()
    {
        $id = Request::segment(2);
        return [
            'name' => 'required',
            'slug' => 'required|unique:departments,slug,'.$id.'|alpha_dash'
        ];
    }

but to me that just seems hacky and probably not the best way of doing it.

Other suggestions are greatly appreciated!

TomCawthorn commented 9 years ago

It doesn't sound that hack to me! To avoid breaking stuff that's already setup (and I haven't got round to updating yet), I use the following in FormValidator:


    /**
     * @return array
     */
    public function getValidationRules()
    {
        if (method_exists($this, 'rules')) {
            return $this->rules();  
        }
        return $this->rules;    
    }

Then I reference the request like @leothelocust

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
                extract(\Request::only('name'));
        $userClubID = \Auth::User()->club->id;
        return [
            'name' => 'required|unique:signups,name,'. $name .',id,club_id,'. $userClubID,
            'description' => 'required',            
        ];
    }
JacobBennett commented 9 years ago

@leothelocust @TomCawthorn Laravel 5 has no need of this package as it is implementing its own validation using Form Requests which you can read more about over at Matt Staufers blog http://mattstauffer.co/blog/laravel-5.0-form-requests

JacobBennett commented 9 years ago

Also have my own solution to the multiple sets of rules for a single resource. You can check it out in my fork of the repo over at https://github.com/JacobBennett/Validation.

The idea is essentially that you set nested rules in the rules array like so


$rules = [
   'store' => [
      'user' => 'required',
      'password' => 'required'
   ],
   'update' => [
      'password' => 'required'
   ]
];

Then before validating, you simply set a context on the validator like so


$this->validator->setContext('update')->validate();

Easy solution, and tests are included. Has been working great for me.

parweb commented 9 years ago

my extends of Laracasts\Validation\FormValidator

namespace Acme\Core;

use Laracasts\Validation\FormValidator as _FormValidator;

class FormValidator extends _FormValidator {

    protected $rule = null;

    public function getValidationRules() {

        // si on passe en arg un array
        if ( is_array( $this->rule ) ) {

            // recupere toutes les values
            $rule_val = current( array_values( $this->rule ) );
// print_r($rule_val);exit;
            // si l'array contien qu'un seul élément et que les values sont un array
            if ( count( $this->rule ) == 1 && is_array( $rule_val ) ) {

                // récupere la premiere key de l'array
                $rule_key = current( array_keys( $this->rule ) );

                // si la premiere key de l'array est présent dans la class Acme/Forms/{$name}Form.php dans $this->rules ( add || edit || whatever || ... ).
                if ( in_array( $rule_key, array_keys( $this->rules ) ) ) {

                    // on fusionne les 2 array
                    return array_merge( $this->rules[$rule_key], $rule_val );

                }

                //si 1er key pas présente on return les nouvelles rules passé en arg en enlevant le parents ( ex: edit => [ $rule1, $rule2, ... ] )
                return $rule_val;
            }

            // si array plus que 1 éléments on return directement les rules
            return $this->rule;
        }

        // si on passe en arg un string
        if ( is_string( $this->rule ) ) {

            // on return la bonne rules
            return $this->rules[$this->rule];

        }

        // sinon rien de tout cela comportement par default
        return $this->rules;

    }

    public function validate ( array $formData, $rule = null ) {

        $this->rule = $rule;

        // dd($this->rules);

        return parent::validate( $formData );

    }

}

example of a FormValidator

namespace Acme\Forms;

use Acme\Core\FormValidator;

class UserForm extends FormValidator {

    protected $rules = [

        'add' => [

            'login' => 'required|alpha_dash|unique:user,login,NULL,id,deleted_at,NULL',
            'email' => 'required|email|unique:user,email,NULL,id,deleted_at,NULL',
            'password' => 'required|confirmed'

        ]

    ];

}

Using in a controller


// ....

public function store( $id = null ) {

    $user = User::firstOrNew(['id' => $id]);

    if ( $id === null ) {
        $this->userForm->validate(Input::all(), 'add');

        // or merging with the existing one ( in Acme\Form\UserForm )
        // if rule "add" doesn't existe juste apply the  [ 'rule1', 'rule2'  ] 
        $this->userForm->validate(Input::all(), [ 'add' => [ 'rule1', 'rule2'  ] ]);

        $user->password = Input::get( 'password' );

        Flash::success("L'utilisateur a été ajouté avec succès !");     
    }
    else {
        $rules = [
            'login' => 'required|alpha_dash|unique:user,login,'.$id.',id,deleted_at,NULL',
            'email' => 'required|email|unique:user,email,'.$id.',id,deleted_at,NULL'
        ];

        $this->userForm->validate( Input::all(), $rules );

        Flash::success("L'utilisateur a été modifié avec succès !");
    }

    $user->login = htmlspecialchars(Input::get( 'login' ));
    $user->email = htmlspecialchars(Input::get( 'email' ));
    $user->firstname = htmlspecialchars(Input::get( 'firstname' ));
    $user->lastname = htmlspecialchars(Input::get( 'lastname' ));

    $user->save();

    return Redirect::to( 'user' );
}

// ....