CakePHP 4 - Validate password when not empty

526 views Asked by At

While editing a user in CakePHP 4, I want to validate the fields 'password' and 'password_check' only when the 'password' field is not empty.

When 'password' is not empty, those validation rules should be active:

  • 'password' should count at least 8 characters.
  • 'password' should count at most 60 characters.
  • 'password_check' should be required.
  • 'password_check' should count at least 8 characters.
  • 'password_check' should count at most 60 characters.
  • 'password_check should be identical to 'password'.

In UsersController.php, I tried to remove the 'password' value out of the request data so it's not validated when the entity is patched, but apperently that's not possible:

if (empty($this->request->getData('password'))) {
    unset($this->request->getData('password')); // error: can't use method return value in write context
    unset($this->request->getData('password_check')); // error: can't use method return value in write context
};

$user = $this->Users->patchEntity($user, $this->request->getData()); // validator is called here

Can somebody guide me the way so my preferred validation happens in an efficient way? Thanks a lot!

Oh yes, here's my UsersTable.php code:

<?php
declare(strict_types=1);

namespace App\Model\Table;

use Cake\ORM\Query;
use Cake\ORM\RulesChecker;
use Cake\ORM\Table;
use Cake\Validation\Validator;

/**
 * Users Model
 *
 * @property \App\Model\Table\LanguagesTable&\Cake\ORM\Association\BelongsTo $Languages
 * @property \App\Model\Table\RolesTable&\Cake\ORM\Association\BelongsTo $Roles
 * @property \App\Model\Table\ArticleCategoriesTable&\Cake\ORM\Association\HasMany $ArticleCategories
 * @property \App\Model\Table\ArticleImagesTable&\Cake\ORM\Association\HasMany $ArticleImages
 * @property \App\Model\Table\ArticleTagsTable&\Cake\ORM\Association\HasMany $ArticleTags
 * @property \App\Model\Table\ArticlesTable&\Cake\ORM\Association\HasMany $Articles
 * @property \App\Model\Table\CategoriesTable&\Cake\ORM\Association\HasMany $Categories
 * @property \App\Model\Table\LinksTable&\Cake\ORM\Association\HasMany $Links
 * @property \App\Model\Table\MenusTable&\Cake\ORM\Association\HasMany $Menus
 * @property \App\Model\Table\ModulePartsTable&\Cake\ORM\Association\HasMany $ModuleParts
 * @property \App\Model\Table\ModulesTable&\Cake\ORM\Association\HasMany $Modules
 * @property \App\Model\Table\PagesTable&\Cake\ORM\Association\HasMany $Pages
 * @property \App\Model\Table\PartsTable&\Cake\ORM\Association\HasMany $Parts
 * @property \App\Model\Table\TagsTable&\Cake\ORM\Association\HasMany $Tags
 *
 * @method \App\Model\Entity\User newEmptyEntity()
 * @method \App\Model\Entity\User newEntity(array $data, array $options = [])
 * @method \App\Model\Entity\User[] newEntities(array $data, array $options = [])
 * @method \App\Model\Entity\User get($primaryKey, $options = [])
 * @method \App\Model\Entity\User findOrCreate($search, ?callable $callback = null, $options = [])
 * @method \App\Model\Entity\User patchEntity(\Cake\Datasource\EntityInterface $entity, array $data, array $options = [])
 * @method \App\Model\Entity\User[] patchEntities(iterable $entities, array $data, array $options = [])
 * @method \App\Model\Entity\User|false save(\Cake\Datasource\EntityInterface $entity, $options = [])
 * @method \App\Model\Entity\User saveOrFail(\Cake\Datasource\EntityInterface $entity, $options = [])
 * @method \App\Model\Entity\User[]|\Cake\Datasource\ResultSetInterface|false saveMany(iterable $entities, $options = [])
 * @method \App\Model\Entity\User[]|\Cake\Datasource\ResultSetInterface saveManyOrFail(iterable $entities, $options = [])
 * @method \App\Model\Entity\User[]|\Cake\Datasource\ResultSetInterface|false deleteMany(iterable $entities, $options = [])
 * @method \App\Model\Entity\User[]|\Cake\Datasource\ResultSetInterface deleteManyOrFail(iterable $entities, $options = [])
 *
 * @mixin \Cake\ORM\Behavior\TimestampBehavior
 */
class UsersTable extends Table
{
    /**
     * Initialize method
     *
     * @param array $config The configuration for the Table.
     * @return void
     */
    public function initialize(array $config): void
    {
        parent::initialize($config);

        $this->setTable('users');
        $this->setDisplayField('username');
        $this->setPrimaryKey('id');

        $this->addBehavior('Timestamp');

        $this->belongsTo('Languages', [
            'foreignKey' => 'language_id',
            'joinType' => 'INNER',
        ]);
        $this->belongsTo('Roles', [
            'foreignKey' => 'role_id',
            'joinType' => 'INNER',
        ]);
        $this->hasMany('ArticleCategories', [
            'foreignKey' => 'user_id',
        ]);
        $this->hasMany('ArticleImages', [
            'foreignKey' => 'user_id',
        ]);
        $this->hasMany('ArticleTags', [
            'foreignKey' => 'user_id',
        ]);
        $this->hasMany('Articles', [
            'foreignKey' => 'user_id',
        ]);
        $this->hasMany('Categories', [
            'foreignKey' => 'user_id',
        ]);
        $this->hasMany('Links', [
            'foreignKey' => 'user_id',
        ]);
        $this->hasMany('Menus', [
            'foreignKey' => 'user_id',
        ]);
        $this->hasMany('ModuleParts', [
            'foreignKey' => 'user_id',
        ]);
        $this->hasMany('Modules', [
            'foreignKey' => 'user_id',
        ]);
        $this->hasMany('Pages', [
            'foreignKey' => 'user_id',
        ]);
        $this->hasMany('Parts', [
            'foreignKey' => 'user_id',
        ]);
        $this->hasMany('Tags', [
            'foreignKey' => 'user_id',
        ]);
    }

    /**
     * Default validation rules.
     *
     * @param \Cake\Validation\Validator $validator Validator instance.
     * @return \Cake\Validation\Validator
     */
    public function validationDefault(Validator $validator): Validator
    {
        $validator
            ->scalar('first_name', __('Valid first_name is required.'))
            ->maxLength('first_name', 30, __('First name should count at most 30 characters.'))
            ->requirePresence('first_name', 'create')
            ->notEmptyString('first_name', __('First name is required.'));

        $validator
            ->scalar('last_name', __('Valid last name is required.'))
            ->maxLength('last_name', 30, __('Last name should count at most 30 characters.'))
            ->requirePresence('last_name', 'create')
            ->notEmptyString('last_name', __('Last name is required.'));

        $validator
            ->scalar('username', __('Valid username is required.'))
            ->maxLength('username', 30, __('Username should count at most 30 characters.'))
            ->requirePresence('username', 'create')
            ->notEmptyString('username', __('Username is required.'));

        $validator
            ->email('email', true, __('Valid email is required.'))
            ->requirePresence('email', 'create')
            ->notEmptyString('email', __('Email is required.'));

        $validator
            ->scalar('password', __('Valid password is required.'))
            ->minLength('password', 8, __('Password should count at least 8 characters.'))
            ->maxLength('password', 60, __('Password should count at most 60 characters.'))
            ->requirePresence('password', 'create')
            ->notEmptyString('password', __('Password is required.'));

        $validator
            ->scalar('password_check', __('Password check is required.'))
            ->minLength('password_check', 8, __('Password check should count at least 8 characters.'))
            ->maxLength('password_check', 60, __('Password check should count at most 60 characters.'))
            ->requirePresence('password_check', 'create')
            ->notEmptyString('password_check', __('Password check is required.'))
            ->sameAs('password_check', 'password', __('Password check and password should be identical.'));
        
        $validator
            ->integer('role_id', __('Valid role is required.'))
            ->notEmptyString('role_id', __('Role is required.'));
        
        $validator
            ->boolean('is_active', __('Valid is active is required.'))
            ->notEmptyString('is_active', __('Is active is required.'));

        $validator
            ->integer('language_id', __('Valid language is required.'))
            ->notEmptyString('language_id', __('Language is required.'));

        return $validator;
    }

    /**
     * Returns a rules checker object that will be used for validating
     * application integrity.
     *
     * @param \Cake\ORM\RulesChecker $rules The rules object to be modified.
     * @return \Cake\ORM\RulesChecker
     */
    public function buildRules(RulesChecker $rules): RulesChecker
    {
        $rules->add($rules->isUnique(['username']), ['errorField' => 'username']);
        $rules->add($rules->isUnique(['email']), ['errorField' => 'email']);
        $rules->add($rules->existsIn('role_id', 'Roles'), ['errorField' => 'role_id']);
        $rules->add($rules->existsIn('language_id', 'Languages'), ['errorField' => 'language_id']);

        return $rules;
    }
}
1

There are 1 answers

3
ndm On

There are exceptions, but usually when you feel the need to modify the data in the actual request object, you're most likely doing something wrong.

The intended way to modify (request) data before marshalling when creating/patching entities, is the beforeMarshal event/callback.

// in `UsersTable` class

public function beforeMarshal(
    \Cake\Event\EventInterface $event,
    \ArrayAccess $data,
    \ArrayObject $options
): void {
    if (empty($data['password'])) {
        unset($data['password']);
    }
    if (empty($data['password_check'])) {
        unset($data['password_check']);
    }
}

See also