Yii2: ActiveForm: combine rules / multiple validation on one field - php

LoginForm:
public function rules()
{
return [
// username and password are both required
[['username', 'password'], 'required'],
// username should be a number and of 8 digits
[['username'], 'number', 'message'=>'{attribute} must be a number'],
[['username'], 'string', 'length' => 8],
// password is validated by validatePassword()
['password', 'validatePassword'],
];
}
/**
* Validates the password.
* This method serves as the inline validation for password.
*
* #param string $attribute the attribute currently being validated
* #param array $params the additional name-value pairs given in the rule
*/
public function validatePassword($attribute, $params)
{
if (!$this->hasErrors()) {
$user = $this->getUser();
if (!$user || !$user->validatePassword($this->password)) {
$this->addError($attribute, 'Incorrect username or password.');
}
}
}
I have set up 2 rules for the same field as you can see above:
[['username'], 'number', 'message'=>'{attribute} must be a number'],
[['username'], 'string', 'length' => 8],
I would like the form to display different error messages for the following 3 scenarios situations:
The provided value is neither a number, nor 8 characters (digits).
The provided value is a number, but is not of 8 characters (digits).
The provided value is not a number, but is of 8 characters (digits).
My question is 2 fold:
A. Is there a way to combine these rules in any standard, Yii2 way.
B. In my previous question I have tried to set up a custom validator (the obvious way to solve this), but it was very simply ignored. The only way I could make it validate was if I added the username field to a scenario. However, once I added password too, it was again ignored. Any reason's for this that you can think of? EDIT: skipOnError = false changed nothing at all in this behaviour.
So please, when you answer, make sure you test it preferably in yii2/advanced; I barely touched the default set up, so it should be easy to test.
EDIT: for clarity, I would like to only allow numbers that are of 8 characters (digits), so they can potentially have a leading 0, eg. 00000001, or 00000000 for that matter. This is why it has to be a numeric string.

The best way to combine rules and display custom error messages for different situations is to create a custom validator. Now if you want that to work on client-side too (it was one of my problems detailed in question B above, thanks to #Beowulfenator for the lead on this), you have to create an actual custom validator class extended from the yii2 native validator class.
Here is an example:
CustomValidator.php
<?php
namespace app\components\validators;
use Yii;
use yii\validators\Validator;
class CustomValidator extends Validator
{
public function init() {
parent::init();
}
public function validateAttribute($model, $attribute) {
$model->addError($attribute, $attribute.' message');
}
public function clientValidateAttribute($model, $attribute, $view)
{
return <<<JS
messages.push('$attribute message');
JS;
}
}
LoginForm.php
<?php
namespace common\models;
use Yii;
use yii\base\Model;
use app\components\validators\CustomValidator;
/**
* Login form
*/
class LoginForm extends Model
{
public $username;
public $password;
public $custom;
private $_user;
/**
* #inheritdoc
*/
public function rules()
{
return [
// username and password are both required
[['username', 'password'], 'required'],
// username should be a number and of 8 digits
[['username'], 'number', 'message'=>'{attribute} must be a number'],
[['username'], 'string', 'length' => 8],
// password is validated by validatePassword()
['password', 'validatePassword'],
['custom', CustomValidator::className()],
];
}
// ...
login.php
<?php
/* #var $this yii\web\View */
/* #var $form yii\bootstrap\ActiveForm */
/* #var $model \common\models\LoginForm */
use yii\helpers\Html;
use yii\bootstrap\ActiveForm;
$this->title = 'Login';
?>
<div class="site-login text-center">
<h1><?php echo Yii::$app->name; ?></h1>
<?php $form = ActiveForm::begin([
'id' => 'login-form',
'fieldConfig' => ['template' => "{label}\n{input}"],
'enableClientValidation' => true,
'validateOnSubmit' => true,
]); ?>
<?= $form->errorSummary($model, ['header'=>'']) ?>
<div class="row">
<div class="col-lg-4 col-lg-offset-4">
<div class="col-lg-10 col-lg-offset-1">
<div style="margin-top:40px">
<?= $form->field($model, 'username') ?>
</div>
<div>
<?= $form->field($model, 'password')->passwordInput() ?>
</div>
<div>
<?= $form->field($model, 'custom') ?>
</div>
<div class="form-group" style="margin-top:40px">
<?= Html::submitButton('Login', ['class' => 'btn btn-default', 'name' => 'login-button']) ?>
</div>
</div>
</div>
</div>
<?php ActiveForm::end(); ?>
</div>

Finally you need this :
the value is required
the value must be a string of 8 chars
the value must contains only digits
So you should simply try :
['username', 'required'],
['username', 'string', 'min' => 8, 'max' => 8],
['username', 'match', 'pattern' => '/^[0-9]{8}$/', 'message'=>'{attribute} must be a number'],

Yii2 ignores your validation rules may because you duplicated not only attribue but also types.
With number validation, i think you should use min/max option to validate number length.
For this case:
'min'=>10000000,'max'=>99999999

Related

yii2 captcha validation not work

I'm trying to add captcha to my login page on my yii2 application.
I have tried some tutorial, the problem is captcha is always correct like no validation.
I have tried this:
https://www.yiiframework.com/doc/api/2.0/yii-captcha-captcha
https://mumunotesss.blogspot.com/2015/06/implementation-captcha-on-yii2.html
http://yii2-user.readthedocs.io/en/latest/howto/adding-captcha.html
How to add captcha in Yii-2 application?
My code is:
LoginForm
public $username;
public $password;
public $rememberMe = true;
private $_user = false;
public $captcha; // add this varible to your model class.
/**
* #return array the validation rules.
*/
public function rules() {
return [
// username and password are both required
[['username', 'password','captcha'], 'required'],
// rememberMe must be a boolean value
['rememberMe', 'boolean'],
['captcha', 'captcha','captchaAction'=>'/site/captcha'], // add this code to your rules.
// password is validated by validatePassword()
['password', 'validatePassword'],
];
}
SiteController
public function actions()
{
return [
'error' => [
'class' => 'yii\web\ErrorAction',
],
'captcha' => [
'class' => 'yii\captcha\CaptchaAction',
'fixedVerifyCode' => YII_ENV_TEST ? 'testme' : null,
],
];
}
view login.php
<div class="form-goup">
<?= $form->field($model, 'captcha')->widget(yii\captcha\Captcha::className(), '
['template' => '<div class="row"><div class="col-lg-3" style="margin-right:25px;">{image}</div><div class="col-lg-6">{input}</div></div>',
]); ?>
</div>
Your code works but you have syntax errors in it, a letter (this is only cosmetic) and a surplus ' which is completely wrong:
<div class="form-group">
<?= $form->field($model, 'captcha')->widget(yii\captcha\Captcha::className(), ['template' => '<div class="row"><div class="col-lg-3" style="margin-right:25px;">{image}</div><div class="col-lg-6">{input}</div></div>',]); ?>
</div>
This should be in view before submit button <?php ActiveForm::end(); ?>
I know the topic is old but it might be the issue with your url rules. Have you tried adding:
'captcha'=>site/captcha'
in your config file, url section

CakePHP 3 - How to automatically show validation errors in view for form without model

I'm reviewing a basic contact form which is not associated with any model. I would like some advice on the best way to leverage Cake's automatic view rendering of field errors for this situation.
Controller
Performs validation through a custom Validator.
public function index()
{
if ($this->request->is('post')) {
// Validate the form
$validator = new EnquiryValidator();
$data = $this->request->data();
$errors = $validator->errors($data);
if (empty($errors)) {
// Send email, etc.
// ...
// Refresh page on success
}
// Show error
$this->Flash->error('Unable to send email');
}
}
View
<?= $this->Form->create(); ?>
<?= $this->Form->input('name', [
'autofocus' => 'autofocus',
'placeholder' => 'Your name',
'required'
]);
?>
<?= $this->Form->input('email', [
'placeholder' => 'Your email address',
'required'
]);
?>
<?= $this->Form->input('subject', [
'placeholder' => 'What would you like to discuss?',
'required'
]);
?>
<?= $this->Form->input('message', [
'label' => 'Query',
'placeholder' => 'How can we help?',
'cols' => '30',
'rows' => '10',
'required'
]);
?>
<div class="text-right">
<?= $this->Form->button('Send'); ?>
</div>
<?= $this->Form->end(); ?>
Currently the form will not show any errors next to the input fields. I assume it's because there is no entity associated with the form or something like that, but I'm not sure.
What is the best solution? Can the validation be performed in a better way to automatically provide field errors in the view?
Modelless forms
Use a modelless form. It can be used to validate data and perform actions, similar to tables and entities, and the form helper supports it just like entities, ie, you simply pass the modelless form instance to the FormHelper::create() call.
Here's the example from the docs, modified a little to match your case:
src/Form/EnquiryForm.php
namespace App\Form;
use App\...\EnquiryValidator;
use Cake\Form\Form;
use Cake\Form\Schema;
use Cake\Validation\Validator;
class EnquiryForm extends Form
{
protected function _buildSchema(Schema $schema)
{
return $schema
->addField('name', 'string')
->addField('email', ['type' => 'string'])
->addField('subject', ['type' => 'string'])
->addField('message', ['type' => 'text']);
}
protected function _buildValidator(Validator $validator)
{
return new EnquiryValidator();
}
protected function _execute(array $data)
{
// Send email, etc.
return true;
}
}
in your controller
use App\Form\EnquiryForm;
// ...
public function index()
{
$enquiry = new EnquiryForm();
if ($this->request->is('post')) {
if ($enquiry->execute($this->request->data)) {
$this->Flash->success('Everything is fine.');
// ...
} else {
$this->Flash->error('Unable to send email.');
}
}
$this->set('enquiry', $enquiry);
}
in your view template
<?= $this->Form->create($enquiry); ?>
See also
Cookbook > Modelless Forms

Login [ Auth->identify() ] always false on CakePHP 3

I started using CakePHP 3 after a time using CakePHP 2 and I am having troubles to create the authentication login.
The new auth function $this->Auth->identify() always return false.
On the database, the password are encrypted perfect and the query who takes the user it's ok too.
My code:
AppController:
[...]
class AppController extends Controller{
public function initialize(){
$this->loadComponent('Flash');
$this->loadComponent('Auth', [
'loginRedirect' => [
'controller' => 'Admin',
'action' => 'index'
],
'logoutRedirect' => [
'controller' => 'Pages',
'action' => 'display'
]
]);
}
public function beforeFilter(Event $event)
{
$this->Auth->allow(['display']);
}
}
UserController:
[...]
class UsersController extends AppController{
public function beforeFilter(Event $event)
{
parent::beforeFilter($event);
$this->Auth->allow(['logout']);
}
[...]
public function login()
{
if ($this->request->is('post')) {
$user = $this->Auth->identify();
if ($user) {
$this->Auth->setUser($user);
return $this->redirect($this->Auth->redirectUrl());
}
$this->Flash->error(__('Invalid username or password, try again'));
}
}
[...]
User (Model Entity):
<?php
namespace App\Model\Entity;
use Cake\Auth\DefaultPasswordHasher;
use Cake\ORM\Entity;
class User extends Entity{
protected $_accessible = [*];
protected function _setPassword($password){
return (new DefaultPasswordHasher)->hash($password);
}
}
View:
<div class="users form">
<?= $this->Flash->render('auth') ?>
<?= $this->Form->create() ?>
<fieldset>
<legend><?= __('Please enter your username and password') ?></legend>
<?= $this->Form->input('username') ?>
<?= $this->Form->input('password') ?>
</fieldset>
<?= $this->Form->button(__('Login')); ?>
<?= $this->Form->end() ?>
</div>
CakePHP3 uses a different hashing algorithm by default than 2 (bcrypt vs. SHA1), so you need to make your password length longer. Change your password field to VARCHAR(255) to be safe.
When CakePHP 3 tries to identify your in-memory hashed password from this->Auth->identify() vs. the hashed password in the database, it will never match because some characters are missing. Changing to 255 is more than needed, but can help future proof if an even more secure hash is used in the future. 255 is recommended because the the character count can be stored in one byte.
Solved: The type on database was less than required. Changed to varchar(255) and now works fine :)
I had the same issue. The Login [ Auth->identify() ] was not working for me. Changing password length in db will resolve the issue.
Hi share my snippets to Login Auth, all Testing is OK, in CakePHP 3.1, customs (Table + view login BootStrap 3 + SQL base + custom bootstrap.php for Spanish in Inflector::rules(*******))
All code in
https://bitbucket.org/snippets/eom/rLo49
I came out with a solution just by adding use Cake\Auth\DefaultPasswordHasher; and its following override method _setPassword.
Here is the change:
Model/table.php
<?php
use Cake\Auth\DefaultPasswordHasher;
// Somewhere in the class
protected function _setPassword($password) {
return (new DefaultPasswordHasher)->hash($password);
}

How to create a Yii2 model without a database

I would like to create a yii2 model without a database. Instead, the data is generated dynamically and not stored, just displayed to the user as a json. Basically, I would just like a get a simple, basic example of a non-database Model working but I can't find any documentation on it.
So how would I write a model without a database? I have extended \yii\base\Model but I get the following error message:
<?xml version="1.0" encoding="UTF-8"?>
<response>
<name>PHP Fatal Error</name>
<message>Call to undefined method my\app\models\Test::find()</message>
<code>1</code>
<type>yii\base\ErrorException</type>
<file>/my/app/vendor/yiisoft/yii2/rest/IndexAction.php</file>
<line>61</line>
<stack-trace>
<item>#0 [internal function]: yii\base\ErrorHandler->handleFatalError()</item>
<item>#1 {main}</item>
</stack-trace>
</response>
To implement find(), I must return a database query object.
My Model is completely blank, I'm just looking for a simple example to understand the principal.
<?php
namespace my\app\models;
class Test extends \yii\base\Model{
}
This is a Model from one of my projects. This Model is not connected with any database.
<?php
/**
* Created by PhpStorm.
* User: Abhimanyu
* Date: 18-02-2015
* Time: 22:07
*/
namespace backend\models;
use yii\base\Model;
class BasicSettingForm extends Model
{
public $appName;
public $appBackendTheme;
public $appFrontendTheme;
public $cacheClass;
public $appTour;
public function rules()
{
return [
// Application Name
['appName', 'required'],
['appName', 'string', 'max' => 150],
// Application Backend Theme
['appBackendTheme', 'required'],
// Application Frontend Theme
['appFrontendTheme', 'required'],
// Cache Class
['cacheClass', 'required'],
['cacheClass', 'string', 'max' => 128],
// Application Tour
['appTour', 'boolean']
];
}
public function attributeLabels()
{
return [
'appName' => 'Application Name',
'appFrontendTheme' => 'Frontend Theme',
'appBackendTheme' => 'Backend Theme',
'cacheClass' => 'Cache Class',
'appTour' => 'Show introduction tour for new users'
];
}
}
Use this Model like any other.
e.g. view.php:
<?php
/**
* Created by PhpStorm.
* User: Abhimanyu
* Date: 18-02-2015
* Time: 16:47
*/
use abhimanyu\installer\helpers\enums\Configuration as Enum;
use yii\caching\DbCache;
use yii\caching\FileCache;
use yii\helpers\Html;
use yii\widgets\ActiveForm;
/** #var $this \yii\web\View */
/** #var $model \backend\models\BasicSettingForm */
/** #var $themes */
$this->title = 'Basic Settings - ' . Yii::$app->name;
?>
<div class="panel panel-default">
<div class="panel-heading">Basic Settings</div>
<div class="panel-body">
<?= $this->render('/alert') ?>
<?php $form = ActiveForm::begin([
'id' => 'basic-setting-form',
'enableAjaxValidation' => FALSE,
]); ?>
<h4>Application Settings</h4>
<div class="form-group">
<?= $form->field($model, 'appName')->textInput([
'value' => Yii::$app->config->get(
Enum::APP_NAME, 'Starter Kit'),
'autofocus' => TRUE,
'autocomplete' => 'off'
])
?>
</div>
<hr/>
<h4>Theme Settings</h4>
<div class="form-group">
<?= $form->field($model, 'appBackendTheme')->dropDownList($themes, [
'class' => 'form-control',
'options' => [
Yii::$app->config->get(Enum::APP_BACKEND_THEME, 'yeti') => ['selected ' => TRUE]
]
]) ?>
</div>
<div class="form-group">
<?= $form->field($model, 'appFrontendTheme')->dropDownList($themes, [
'class' => 'form-control',
'options' => [
Yii::$app->config->get(Enum::APP_FRONTEND_THEME, 'readable') => ['selected ' => TRUE]
]
]) ?>
</div>
<hr/>
<h4>Cache Setting</h4>
<div class="form-group">
<?= $form->field($model, 'cacheClass')->dropDownList(
[
FileCache::className() => 'File Cache',
DbCache::className() => 'Db Cache'
],
[
'class' => 'form-control',
'options' => [
Yii::$app->config->get(Enum::CACHE_CLASS, FileCache::className()) => ['selected ' => TRUE]
]
]) ?>
</div>
<hr/>
<h4>Introduction Tour</h4>
<div class="form-group">
<div class="checkbox">
<?= $form->field($model, 'appTour')->checkbox() ?>
</div>
</div>
<?= Html::submitButton('Save', ['class' => 'btn btn-primary']) ?>
<?php $form::end(); ?>
</div>
The reason for using a model is to perform some kind of logic on data that you are getting from somewhere. A model can be used to perform data validation, return properties of the model and their labels, and allows for massive assignment. If you don't need these features for your data model, then don't use a model!
If you are not requiring data validation (i.e. you are not changing any data via forms or other external source), and you are not requiring access to behaviors or events, then you probably need to just use yii\base\Object. This will give you access to getters and setters for properties of the object, which seems to be all you need.
So your class ends up looking like this. I've included getting data from another model, in case that's what you want to do;
<?php
namespace my\app\models;
use \path\to\some\other\model\to\use\OtherModels;
class Test extends \yii\base\Object{
public function getProperty1(){
return "Whatever you want property1 to be";
}
public function getProperty2(){
return "Whatever you want property2 to be";
}
public function getOtherModels(){
return OtherModels::findAll();
}
}
You would then simply use it like this;
$test = new Test;
echo $test->property1;
foreach ($test->otherModels as $otherModel){
\\Do something
}
The function you have tried to use, find(),is only relevant to a database and so won't be available to your class if you've extended yii\base\Model, yii\base\Component or yii\base\Object, unless you want to define such a function yourself.
A lightweight way to create models without database backend is to use a DynamicModel:
DynamicModel is a model class primarily used to support ad hoc data validation.
Just write in your controller:
$model = new DynamicModel(compact('name', 'email'));
$model->addRule(['name', 'email'], 'string', ['max' => 128])
->addRule('email', 'email')
->validate();
and then pass $model to your view.
A full example can be found in http://www.yiiframework.com/wiki/759/create-form-with-dynamicmodel/.
This is perfect for user input for calling APIs, creating forms on the fly, etc.
As has been pointed out in the comments and other answers, your model needs to extend \yii\db\BaseActiveRecord. That said you can store your json as a nosql database such as MongoDb or in a key-value cache such as Redis instead. Both have Yii implementions: \yii\mongodb\ActiveRecord and \yii\redis\ActiveRecord

Relational attribute in Yii2 form

I am trying to get to figure out the proper way of handling a form receiving relational data in Yii2. I haven't been able to find any good examples of this. I have 2 models Sets and SetsIntensity, every Set may have one SetsIntensity associated with it. I'm am trying to make a form where you can input both at the same time. I'm not sure how to handle getting the input for a particular field 'intensity' in SetsIntensity.
Where
$model = new \app\models\Sets();
If I put it in the field like this client validation won't work and the attribute name is ambiguous and saving becomes difficult
<?= $form->field($model, 'lift_id_fk') ?>
<?= $form->field($model, 'reps') ?>
<?= $form->field($model, 'sets') ?>
<?= $form->field($model, 'type') ?>
<?= $form->field($model, 'setsintensity') ?>
I would like to do something like this but I get an error if I do
<?= $form->field($model, 'setsintensity.intensity') ?>
Exception (Unknown Property) 'yii\base\UnknownPropertyException' with message 'Getting unknown property: app\models\Sets::setsintensity.intensity'
I could do make another object in the controller $setsintensity = new Setsintensity(); but I feel this is a cumbersome solution and probably not good practice especially for handling multiple relations
<?= $form->field($setsintensity, 'intensity') ?>
relevant code from SetsModel
class Sets extends \yii\db\ActiveRecord
{
public function scenarios() {
$scenarios = parent::scenarios();
$scenarios['program'] = ['lift_id_fk', 'reps', 'sets', 'type', 'intensity'];
return $scenarios;
}
public function rules()
{
return [
[['lift_id_fk'], 'required'],
[['lift_id_fk', 'reps', 'sets','setsintensity'], 'integer'],
[['type'], 'string', 'max' => 1],
['intensity', 'safe', 'on'=>'program']
];
}
public function getSetsintensity()
{
return $this->hasOne(Setsintensity::className(), ['sets_id_fk' => 'sets_id_pk']);
}
SetsIntensity Model
class Setsintensity extends \yii\db\ActiveRecord
{
public static function tableName()
{
return 'setsintensity';
}
public function rules()
{
return [
[['sets_id_fk', 'intensity', 'ref_set'], 'required'],
[['sets_id_fk', 'intensity', 'ref_set'], 'integer']
];
}
public function getSetsIdFk()
{
return $this->hasOne(Sets::className(), ['sets_id_pk' => 'sets_id_fk']);
}
}
I was also thinking maybe I could put in a hasOne() relation for the specific attribute 'intensity' in 'Sets'
You should simply try this :
<?= $form->field($model->setsintensity, 'intensity') ?>
EDIT : And because "every Set may have one SetsIntensity", you should check this relation before displaying form, e.g. :
if ($model->setsintensity===null)
{
$setsintensity = new SetsIntensity;
$model->link('setsintensity', setsintensity);
}
PS: link method requires that the primary key value is not null.

Categories