Define multiple scenarios and validate with multiple scenarios in Yii 2 model - yii2

In model I have defined multiple scenarios:
public function rules() {
return [
[['in_quantity'], 'required','on'=>['stockIn']],
[['out_quantity'], 'required','on'=>['stockOut']],
];
}
Is it possible to use both scenario stockIn and stockOut for single model validation?
$StockModel->scenario[] = 'stockOut';
$StockModel->scenario[] = 'stockIn';
or
$StockModel->scenario = ['stockOut','stockIn'];

You can't have multiple scenarios for model. But you can have multiple scenarios for rule:
public function rules() {
return [
[['in_quantity'], 'required', 'on' => ['stockIn', 'stockOut']],
[['out_quantity'], 'required', 'on' => ['stockIn', 'stockOut']],
];
}
If you need multiple scenarios for model, it means that you're overusing scenarios feature.
Also note that it is not recommended to use too many scenarios in one model - scenarios work fine for simple cases, but more complicated cases should be handled by separate models for each scenario.

You can create multiple scenarios this way in model
class MyModel extends \yii\db\ActiveRecord {
const SCENARIO_CREATE = 'scenario_create';
const SCENARIO_UPDATE = 'scenario_update';
// get scenarios
public function scenarios()
{
return [
self::SCENARIO_CREATE => ['user_id', 'name', 'desc', 'published','date_create'],
self::SCENARIO_UPDATE => ['user_id', 'name', 'desc', 'date_update'],
];
}
public function rules()
{
[['user_id'], 'integer'],
[['name','desc'], 'string', 'max' => 70],
[['date_create', 'date_update'], 'date', 'format' => 'php:Y-m-d H:i:s'],
];
}
}
and you can use this way anywhere
public function actionIndex() {
$model = new MyModel;
$model->scenario = MyModel::SCENARIO_CREATE;
if ($model->load(\Yii::$app->request->post())){
if($model->save()){
// some operations
}
}
}

You could if you extend the rule with when for server validation:
[
['in_quantity'],
'required',
'when' => function ($model) {
return $model->scenario === 'stockIn' || $model->scenario === 'stockOut';
}
]
Also if you want to validate in the form (aka client side validation) you could also use the whenClient that expect a js function:
'whenClient' => "function (attribute, value) {
const scenario = $('#stock-scenario').val()
return scenario === 'stockIn' || scenario = 'stockOut';
}"

Related

Cannot generate HalResource for object of type ArrayObject

I've some problems to return a paginator object as HAL json collection. I'm using the latest versions of zend-expressive and zend-expressive-hal.
This is the setting from my ConfigProvider:
public function __invoke() : array
{
return [
'dependencies' => $this->getDependencies(),
MetadataMap::class => $this->getHalConfig(),
];
}
public function getHalConfig() : array
{
return [
[
'__class__' => RouteBasedCollectionMetadata::class,
'collection_class' => RoleCollection::class,
'collection_relation' => 'user_roles',
'route' => 'api.user.roles',
],
];
}
And these are my handler methods:
public function get(ServerRequestInterface $request) : ResponseInterface
{
// read some records from the database
$select = new Select();
$select->from(['r' => 'user_roles']);
$select->columns(['id', 'name']);
$paginator = new RoleCollection(new DbSelect($select, $this->dbAdapter));
$paginator->setItemCountPerPage(25);
$paginator->setCurrentPageNumber(1);
return $this->createResponse($request, $paginator);
}
private function createResponse(ServerRequestInterface $request, $instance) : ResponseInterface
{
return $this->responseFactory->createResponse(
$request,
$this->resourceGenerator->fromObject($instance, $request)
);
}
The RoleCollection class is only an inheritance of the Paginator:
class RoleCollection extends Paginator
{
}
The error message which I get is:
Cannot generate Zend\Expressive\Hal\HalResource for object of type ArrayObject; not in metadata map
I think you are missing the metadata for the Role object itself.
For example this is something similar for my posts object:
MetadataMap::class => [
[
'__class__' => RouteBasedCollectionMetadata::class,
'collection_class' => Posts::class,
'collection_relation' => 'posts',
'route' => 'api.posts',
],
[
'__class__' => RouteBasedResourceMetadata::class,
'resource_class' => Post::class,
'route' => 'api.posts.view',
'extractor' => ArraySerializable::class,
],
],
You have only described the collection and the resource class is missing for a single role.
I also see the resource generator tries to parse an ArrayObject. This should be wrapped in a Role object, which you can add to the MetadataMap.
Where it goes wrong in your code is this line:
$paginator = new RoleCollection(new DbSelect($select, $this->dbAdapter));
This adds the result of a query into the paginator, but the paginator does not know how to handle it. If I remember correctly, the DbSelect return a ResultSet. I'm guessing this is where the ArrayObject is coming from. What you probably need is to override that ResultSet and make sure it returns an array of Role objects. You might want to look into the dbselect adapter and the hydrating resultset.
Once you have the Role object in the paginator, you can describe it in the metadata.
[
'__class__' => RouteBasedResourceMetadata::class,
'resource_class' => UserRole::class,
'route' => 'api.roles',
'extractor' => ...,
],
I use doctrine myself with hal so zend-db is out of my scope. If you need more help, I suggest the zf forums.

Yii2: how to remove required attribute in a view?

I have a text field that was defined as required in its model. But a view needs not be required. I try this way to remove the required attribute but it doesn't work:
<?= $form->field($model, 'city')->textInput(['required' => false]) ?>
I need to change it in a view or in its controller. But not in its model (because others view needs the required attribute.).
I know how to do it using jQuery but I prefer with PHP/Yii2.
Update (requiered by the nice help of #Muhammad Omer Aslam):
My model is called Persons.
My view is called _form.
My controller is called PersonsControllers. It has the update function:
actionUpdate($id):
public function actionUpdate($id)
{
$model = $this->findModel($id); // How to add my new scenario here?
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->id_person]);
}
return $this->render('update', [
'model' => $model,
]);
}
You can use scenarios to make the field required or not for the specific view. You can assign the active fields that are required for the scenario, and those fields will be the subject to validation.
I assume the model is Profile. In below example firstname, lastname and city is required in the default scenario.
A model may be used in different scenarios, by default the scenario default is used. Let's say in your case we can declare a scenario special that will only require firstname and lastname. In your model, you will declare a constant for the scenario name, and then override the scenarios() method, key=>value pairs with the active field names being passed in form of an array to the value will be assigned.
namespace app\models;
use yii\db\ActiveRecord;
class Profile extends ActiveRecord
{
const SCENARIO_SPECIAL = 'special';
public function scenarios()
{
$scenarios = parent::scenarios();
$scenarios[self::SCENARIO_SPECIAL] = ['firstname', 'lastname'];
return $scenarios;
}
}
and then inside your controller/action for that view where you do not want the city field to be required, initialize the Profile model object as below
public function actionProfile(){
$model = new \common\models\Profile(['scenario'=> \common\models\Profile::SCENARIO_SPECIAL]);
return $this->render('profile',['model'=>$model]);
}
Now if you submit the form inside this view it will ask only for the firstname and lastname whereas in your previous forms/views if you try to submit the form it will ask you to provide the city when trying to submit, you don't have to change or add anything for the rest of the forms or the rules.
As you are trying to update the record and do not want the city to be required when updating the record, the only difference that could be is to assign the scenario like below as you are not creating a new object for the model.
$model->scenario=\common\models\Profile::SCENARIO_SPECIAL;
In the model:
const SCENARIO_MYSPECIAL = 'myspecial';
public function rules()
{
return [
[['id_person', 'city'], 'required', 'on' => self::SCENARIO_DEFAULT],
[['id_person'], 'required', 'on' => self::SCENARIO_MYSPECIAL],
];
}
In the controller:
public function actionUpdate($id)
{
$model = $this->findModel($id);
$model->scenario = 'myspecial';
if ($model->load(Yii::$app->request->post()) && $model->save()) {
return $this->redirect(['view', 'id' => $model->id_person]);
}
return $this->render('update', [
'model' => $model,
]);
}
go to the model and remove the attribute
public function rules()
{
return [
[['id_person', 'city'], 'required'],
[['id_person'], 'required'],
];
}
EX:
public function rules()
{
return [
[['id_person'], 'required'],
[['id_person'], 'required'],
];
}

Many to many relationships in Yii2

I am trying to create a many to many relationship between a profile and several fields within that profile including languages and specialities. I have looked at several implementations and understand that there are several extensions, but I am required to minimise usage of extensions.
I have created the proper migration...this is a skeleton and the user table is purely for logins and OAuth...so keys can be ignored. As you can see in my controller I don't really know the way forward at this point. My language model is for all intensive purposes static from this controller(It is controlled by an admin backend). What is working? If I make a couple modifications to the below code then the checkboxlist will display with the proper checked items that I manually added to the lookup table. Trying to modify the lookup table from code, I have been unable to do unless I populate the $languageModel with a findOne(knownPK), however that is not usable, because multiple checkboxes can be selected resulting in an array and the link command requires ActiveRecordInterface which is singular. Ideally I would like to simply use
$languageModel->load(Yii::$app->request->post(),$trainerModel->formName());
but that isn't working.
Additionally, is there a mechanism within the framework to remove lookups or is that done manually. Any help or insight would be helpful. Thank you in advance.
public function safeUp()
{
$this->createTable('student', [
'id' => $this->primaryKey(),
'user_id' => $this->integer()->notNull(),
]);
$this->createTable('language', [
'id' => $this->primaryKey(),
'language' => $this->string(63),
]);
$this->createTable('student_language',[
'id' => $this->primaryKey(),
'language_id' => $this->integer(),
'student_id' => $this->integer()
]);
$this->addForeignKey('fk-student-language-language', 'student_language', 'language_id', 'language', 'id', 'CASCADE', 'CASCADE');
$this->addForeignKey('fk-student-language-student', 'student_language', 'student_id', 'student', 'id', 'CASCADE', 'CASCADE');
$this->addForeignKey('fk-student-user-user_id', 'student', 'user_id', 'user', 'id', 'CASCADE', 'CASCADE');
}
My student Active Record
/**
* #return \yii\db\ActiveQuery
*/
public function getstudentLanguages()
{
return $this->hasMany(studentLanguage::className(), ['student_id' => 'id']);
}
public function getLanguages(){
return $this->hasMany(Language::className(),['id'=>'language_id'])->viaTable('student_language',['student_id'=>'id']);
}
My Language Model
/**
* #return \yii\db\ActiveQuery
*/
public function getstudentLanguages()
{
return $this->hasMany(studentLanguage::className(), ['language_id' => 'id']);
}
public function getLanguages(){
return $this->hasMany(student::className(),['id'=>'student_id'])->viaTable('student_language',['language_id'=>'id']);
}
My Controller
public function actionProfile()
{
if (Yii::$app->user->isGuest) {
throw new UnauthorizedHttpException('This page requires you to be logged in');
} else {
$user_id = Yii::$app->user->identity->getId();
$studentModel = $this->findstudentModelByUserId($user_id);
if (is_null($studentModel)) {
$studentModel = new student();
$studentModel->setAttribute('user_id', $user_id);
$studentModel->save();
}
$languageModel = ??????????????????
$studentModel->load(Yii::$app->request->post()) && $studentModel->save() && $studentModel->link('languages',$languageModel);
}
return $this->render('profile', ['model' => $studentModel]);
}
My View
<?= $form->field($model, 'languages')->checkboxList(\common\models\Language::find()->select(
['language', 'id'])->indexBy('id')->column(), ['prompt' => 'select Language']); ?>
This is my solution at this point, however I am interested to hear improvements.
Note: There is no error checking etc...just functional code.
I added this to the student activerecord class
public function linkMultiple( $name,Array $models){
studentLanguage::deleteAll(['student_id' =>$this->id]);
foreach($models as $model){
$this->link($name, $model);
}
return true;
}
Modified my Controller action to the following
public function actionProfile()
{
if (Yii::$app->user->isGuest) {
throw new UnauthorizedHttpException('This page requires you to be logged in');
} else {
$user_id = Yii::$app->user->identity->getId();
$studentModel = $this->findstudentModelByUserId($user_id);
if (is_null($studentModel)) {
$studentModel = new student();
$studentModel->setAttribute('user_id', $user_id);
$studentModel->save();
}
$languages = Yii::$app->request->post('student')['languages'];
$languageModels = Language::findAll($languages);
$studentModel->load(Yii::$app->request->post()) && $studentModel->save() && $studentModel->linkMultiple('languages',$languageModels);
}
return $this->render('profile', ['model' => $studentModel]);
}

Yii2 model custom rules and validations for attribute which is array

Being trying to sort this out but going nowhere with it. I have got an array as attribute for a model and I am trying to create custom validation for some of the keys in the array as required. Or even can't figure out how the attribute labels will work? Here is my code:
MODEL
...
public $company = [
'name' => '',
'trading_name' => '',
'type' => '',
];
public function attributeLabels(){
return [
'company[name]' => 'Company Name',
];
}
public function rules(){
return [
[['company[name]','company[trading_name'], 'safe'],
[['company[name]'], 'return_check','skipOnEmpty'=> false],
];
}
public function return_check($attribute, $params){
$this->addError($attribute ,'Required ');
return false;
}
...
I have even tried to pass the whole array and check in the validator method for the keys and values but the custom validator is not even triggered.
I think you need separated model for company.
I've used custom rule functions, and they all worked. Try removing the return clause at the end of the return_check function.
Here's what has worked for me:
class Essid extends ActiveRecord {
public function rules() {
return [
['network_name', 'checkNetworkName']
]
}
public function checkNetworkName($attribute, $params){
if (!$this->hasErrors()) {
if ( !ctype_alnum($this->network_name) )
$this->addError($attribute, Yii::t('app', 'Not a valid Network Name'));
}
}
}
Hope it helps

how to define a relationship in Yii2 with orOnCondition

I am logging changes in my database to a table called audit_field. For a given model I would like to retrieve all the audit_fields for this model as well as some of the related models.
For example:
<?php
class Job extends ActiveRecord
{
public function getAuditFields()
{
$link = []; // what do I put here to get "1=1" ?
return $this->hasMany(AuditField::className(), $link)
->orOnCondition([
'audit_field.model_id' => $this->job_id,
'audit_field.model_name' => get_class($this),
])
->orOnCondition([
'audit_field.model_id' => ArrayHelper::map($this->getJobTeches()->all(), 'id', 'id'),
'audit_field.model_name' => 'app\models\JobTech',
]);
}
public function getJobTeches()
{
return $this->hasMany(JobTech::className(), ['job_id' => 'job_id']);
}
}
What I want:
SELECT * FROM audit_field WHERE (...my or conditions...);
// or
SELECT * FROM audit_field WHERE (1=1) AND (...my or conditions...);
What I get:
SELECT * FROM audit_field WHERE (1=0) AND (...my or conditions...)
Found the answer, don't use hasMany, instead just return an ActiveQuery:
<?php
class Job extends ActiveRecord
{
public function getAuditFields()
{
return AuditField::find()
->orOnCondition([
'audit_field.model_id' => $this->job_id,
'audit_field.model_name' => get_class($this),
])
->orOnCondition([
'audit_field.model_id' => ArrayHelper::map($this->getJobTeches()->all(), 'id', 'id'),
'audit_field.model_name' => 'app\models\JobTech',
]);
}
}