different rules for the same AR model in Yii - php

I have one model extending AR class with specific rules. But now i need to insert row into this table, but with other rules. Is i need to create other model with new rules, or it is possible to define orther rules?

You can set validation scenario. For example:
$model = new Post();
$model->scenario = 'new_line';
$model->attributes = $_GET['data'];
if ($model->validate()){
$model->save(false);
}
in your model:
public function rules()
{
return array(
array('username, text', 'required','on' => 'new_line')
);
}
In model rules all array lines must have key "on", else this rules will not apply.
Read more here.

If you are extending your class (active records) then you can actually just override your rules() function i.e.:
class User extends ActiveRecord(){
function rules(){
return array(array(
// Nomrally a rule
))
}
}
And then make your next class:
class User_extended extends ActiveRecord(){
function rules(){
return array(array(
// Nomrally a rule
))
}
}
And that should be it. You can then call the User_extended class and your rules will apply to the parent User class since Yii grabs the rules in a $this context and $this will be your child class.
But you can also use scenarios here, but it might get dirty especially if you need to override other methods.

thx. Now i'm trying to use this
/**
* #param string $attribute fields names wich should be validated
* #param array $params additional params for validation
*/
public function ValidatorName($attribute,$params) { … }

Related

Yii2. Adding attribute and rule dynamically to model

I am writing a widget and I want to avoid user adding code to their model (I know it would be easier but using it to learn something new).
Do you know if it is possible to add an attribute (which is not in your database, so it will be virtual) to a model and add a rule for that attribute?. You have no access to change that model code.
I know rules is an array. In the past I have merged rules from parent class using array_merge. Can it be done externally? Does Yii2 has a method for that?
An idea is to extend model provided by the user with a "model" inside my widget an there use:
public function init() {
/*Since it is extended this not even would be necessary,
I could declare the attribute as usual*/
$attribute = "categories";
$this->{$attribute} = null; //To create attribute on the fly
parent::init();
}
public function rules() {
$rules = [...];
//Then here merge parent rules with mine.
return array_merge(parent::rules, $rules);
}
But If I extend it, when I use that model in an ActiveForm in example for a checkbox, it will use my "CustomModel", so I want to avoid that. Any other ideas? How to do it without extending their model?
Add Dynamic Attributes to a existing Model
When you want to add dynamic attributes during runtime to a existing model. Then you need some custom code, you need: A Model-Class, and a extended class, which will do the dynamic part and which have array to hold the dynamic information. These array will merged in the needed function with the return arrays of the Model-Class.
Here is a kind of mockup, it's not fully working. But maybe you get an idea what you need to do:
class MyDynamicModel extends MyNoneDynamicModel
{
private $dynamicFields = [];
private $dynamicRules = [];
public function setDynamicFields($aryDynamics) {
$this->dynamicFields = $aryDynamics;
}
public function setDynamicRules($aryDynamics) {
$this->dynamicRules = $aryDynamics;
}
public function __get($name)
{
if (isset($this->dynamicFields[$name])) {
return $this->dynamicFields[$name];
}
return parent::__get($name);
}
public function __set($name, $value)
{
if (isset($this->dynamicFields[$name])) {
return $this->dynamicFields[$name] = $value;
}
return parent::__set($name, $value);
}
public function rules() {
return array_merge(parent::rules, $this->dynamicRules);
}
}
Full Dynamic Attributes
When all attributes are dynamic and you don't need a database. Then use the new DynamicModel of Yii2. The doc states also:
DynamicModel is a model class primarily used to support ad hoc data validation.
Here is a full example with form integration from the Yii2-Wiki, so i don't make a example here.
Virtual Attributes
When you want to add a attribute to the model, which is not in the database. Then just declare a public variable in the model:
public $myVirtualAttribute;
Then you can just use it in the rules like the other (database-)attributes.
To do Massive Assignment don't forget to add a safe rule to the model rules:
public function rules()
{
return [
...,
[['myVirtualAttribute'], 'safe'],
...
];
}
The reason for this is very well explained here:
Yii2 non-DB (or virtual) attribute isn't populated during massive assignment?

How to modify request input after validation in laravel?

I found method Request::replace, that allows to replace input parameters in Request.
But currently i can see only one way to implement it - to write same replacing input code in every controller action.
Is it possible somehow to group code, that will be executed after request successful validation, but before controller action is started?
For example, i need to support ISO2 languages in my api, but under the hood, i have to transform them into legacy ones, that are really stored in the database. Currently i have this code in controller:
// Controller action context
$iso = $request->input('language');
$legacy = Language::iso2ToLegacy($iso);
$request->replace(['language' => $legacy]);
// Controller action code starts
I think what you're looking for is the passedValidation() method from the ValidatesWhenResolvedTrait trait
How to use it:
Create custom Request: php artisan make:request UpdateLanguageRequest
Put validation rules into the rules() method inside UpdateLanguageRequest class
Use passedValidation() method to make any actions on the Request object after successful validation
namespace App\Http\Requests;
use App\...\Language;
class UpdateLanguageRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
// here goes your rules, f.e.:
'language' => ['max:255']
];
}
protected function passedValidation()
{
$this->replace(['language' => Language::iso2ToLegacy($this->language)]);
}
}
Use UpdateLanguageRequest class in your Controller instead Request
public function someControllerMethod(UpdateLanguageRequest $request){
// the $request->language data was already modified at this point
}
*And maybe you want to use merge not replace method since replace will replace all other data in request and the merge method will replace only specific values
This solution worked for me based on Alexander Ivashchenko answer above:
<?php
namespace App\Http\Requests\User;
class UserUpdateRequest extends UserRequest
{
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules(): array
{
return [
'name'=>'required|string',
'email'=>'required|string|email',
'password'=>'min:8'
];
}
}
Our parent UserRequest class:
<?php
namespace App\Http\Requests\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Hash;
abstract class UserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize(): bool
{
return true;
}
/**
* Handle a passed validation attempt.
*
* #return void
*/
protected function passedValidation()
{
if ($this->has('password')) {
$this->merge(
['password' => Hash::make($this->input('password'))]
);
}
}
public function validated(): array
{
if ($this->has('password')) {
return array_merge(parent::validated(), ['password' => $this->input('password')]);
}
return parent::validated();
}
}
I am overriding validated method also. If we access each input element individually his answer works but in order to use bulk assignment in our controllers as follow we need the validated overriding.
...
public function update(UserUpdateRequest $request, User $user): JsonResource
{
$user->update($request->validated());
...
}
...
This happens because validated method get the data directly from the Validator instead of the Request. Another possible solution could be a custom validator wit a DTO approach, but for simple stuff this above it's enough.
Is it possible somehow to group code, that will be executed after
request successful validation, but before controller action is
started?
You may do it using a middleware as validator, for example:
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\JsonResponse;
class InputValidator
{
public function handle($request, Closure $next, $fullyQualifiedNameOfModel)
{
$model = app($fullyQualifiedNameOfModel);
$validator = app('validator')->make($request->input(), $model->rules($request));
if ($validator->fails()) {
return $this->response($request, $validator->errors());
}
return $next($request);
}
protected function response($request, $errors)
{
if($request->ajax()) {
return new JsonResponse($errors, 422);
}
return redirect()->back()->withErrors($errors)->withInput();
}
}
Add the following entry in the end of $routeMiddleware in App\Http\Kernel.php class:
'validator' => 'App\Http\Middleware\InputValidator'
Add the rules method in Eloquent Model for example, app\Product.php is model and the rules method is declared as given below:
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules(\Illuminate\Http\Request $request)
{
return [
'title' => 'required|unique:products,title,'.$request->route()->parameter('id'),
'slug' => 'required|unique:products,slug,'.$request->route()->parameter('id'),
];
}
Declare the route like this:
$router->get('create', [
'uses' => 'ProductController#create',
'as' => 'Product.create',
'permission' => 'manage_tag',
'middleware' => 'validator:App\Product' // Fully qualified model name
]);
You may add more middleware using array for example:
'middleware' => ['auth', 'validator:App\Product']
This is a way to replace the FormRequest using a single middleware. I use this middleware with model name as argument to validate all my models using a single middleware instead of individual FormRequest class for each controller.
Here, validator is the middleware and App\Product is the model name which I pass as argument and from within the middleware I validate that model.
According to your question, the code inside your controller will be executed only after input validation passes, otherwise the redirect/ajax response will be done. For your specific reason, you may create a specific middleware. This is just an idea that could be used in your case IMO, I mean you can add code for replacing inputs in the specific middleware after validation passes.
Use merge instead of replace
$iso = $request->merge('language');
$legacy = Language::iso2ToLegacy($iso);
$request->merge(['language' => $legacy]);

Yii2 rest save multiple models

Using a REST approach I want to be able to save more than one model in a single action.
class MyController extends ActiveController {
public $modelClass = 'models\MyModel';
}
class MyModel extends ActiveRecord {
...
}
That automagically creates actions for a REST api. The problem is that I want to save more than one model, using only that code in a POST will result in a new record just for MyModel. What if I need to save AnotherModel?
Thanks for any suggestion.
ActiveController implements a common set of basic actions for supporting RESTful access to ActiveRecord. For more advanced use you will need to override them or just merge to them your own custom actions where you will be implementing your own code & logic.
Check in your app the /vendor/yiisoft/yii2/rest/ folder to see how ActiveController is structured and what is doing each of its actions.
Now to start by overriding an ActiveController's action by a custom one, you can do it within your controller. Here is a first example where i'm overriding the createAction:
1-
class MyController extends ActiveController
{
public $modelClass = 'models\MyModel';
public function actions()
{
$actions = parent::actions();
unset($actions['create']);
return $actions;
}
public function actionCreate(){
// your code
}
}
2-
Or you can follow the ActiveController's structure which you can see in /vendor/yiisoft/yii2/rest/ActiveController.php by placing your custom actions in separate files. Here is an example where I'm overriding the updateAction by a custom one where i'm initializing its parameters from myController class :
class MyController extends ActiveController
{
public $modelClass = 'models\MyModel';
public function actions() {
$actions = parent::actions();
$custom_actions = [
'update' => [
'class' => 'app\controllers\actions\WhateverAction',
'modelClass' => $this->modelClass,
'checkAccess' => [$this, 'checkAccess'],
'scenario' => $this->updateScenario,
'params' => \Yii::$app->request->bodyParams,
],
];
return array_merge($actions, $custom_actions);
}
}
Now let's say as example that in my new action file app\controllers\actions\WhateverAction.php I'm expecting the Post Request (which i'm storing in $params) to have a subModels attribute storing a list of child models to which I'm going to apply some extra code like relating them with their parent model if they already exists in first place :
namespace app\controllers\actions;
use Yii;
use yii\base\Model;
use yii\db\ActiveRecord;
use yii\web\ServerErrorHttpException;
use yii\rest\Action;
use app\models\YourSubModel;
class WhateverAction extends Action
{
public $scenario = Model::SCENARIO_DEFAULT;
public $params;
public function run($id)
{
$model = $this->findModel($id);
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id, $model);
}
$model->scenario = $this->scenario;
$model->load($this->params, '');
foreach ($this->params["subModels"] as $subModel) {
/**
* your code related to each of your model's posted child
* for example those lines will relate each child model
* to the parent model by saving that to database as their
* relationship has been defined in their respective models (many_to_many or one_to_many)
*
**/
$subModel = YourSubModel::findOne($subModel['id']);
if (!$subModel) throw new ServerErrorHttpException('Failed to update due to unknown related objects.');
$subModel->link('myParentModelName', $model);
//...
}
// ...
return $model;
}
}
So if I understand you wish to add a new database entry not only for the model you are querying, but for another model.
The best place to do this would be in the AfterSave() or BeforeSave() functions of the first model class. Which one would depend on the data you are saving.

Using filters from other classes within rules method in Yii 2?

Consider this code inside a model:
public function rules() {
return [
[['company_name', 'first_name', 'last_name'], 'sanitize'],
//........
];
}
sanitize is a custom method inside the current class, which is:
public function sanitize($attribute) {
$this->{$attribute} = general::stripTagsConvert($this->{$attribute}, null, true);
}
Now this method obviously will come in handy in many models so I don't want to keep repeating the same code in every model. Is there a way I can reference another class in the rules in place of the current sanitize method name which is binded to the current class?
Yes, it's definitely possible.
Create separate validator. Let assume it's called SanitizeValidator and placed in common/components folder.
Your custom validator must extend from framework base validator and override validateAttribute() method. Put your logic inside this method:
use yii\validators\Validator;
class SanitizeValidator extends Validator
{
/**
* #inheritdoc
*/
public function validateAttribute($model, $attribute)
{
$model->$attribute = general::stripTagsConvert($model->$attribute, null, true);
}
}
Then in model you can attach this validator like this:
use common/components/SanitizeValidator;
/**
* #inheritdoc
*/
public function rules()
{
return [
[['company_name', 'first_name', 'last_name'], SanitizeValidator::className()],
];
}
Check the official documentation about custom validators here and there.

Yii Validate an un-bound Variable (non-stored)

Classic problem:
verify that a user accepted the contract terms but the value of the acceptance is not stored (bound) in the database...
Extend CFormModel rather than CActiveForm (because CActiveForm binds
values to DB)
Post a CFormModel to a controller action
Validate a CFormModel
I'm asking this question to answer it because the existing questions end in see the documentation...
extend CFormModle, define the rules and got to validate. With bound variables you validated as part of save. Now you validate() by itself but Validate requires a list of attributes which is not defined in CFormModel. So, what do you do? You do this:
$contract->validate($contract->attributeNames())
Here's the full example:
class Contract extends CFormModel
{
...
public $agree = false;
...
public function rules()
{
return array(
array('agree', 'required', 'requiredValue' => 1, 'message' => 'You must accept term to use our service'),
);
}
public function attributeLabels()
{
return array(
'agree'=>' I accept the contract terms'
);
}
}
Then in the controller you do this:
public function actionAgree(){
$contract = new Contract;
if(isset($_POST['Contract'])){
//$contract->attributes=$_POST['Contract']; //contract attributes not defined in CFormModel
...
$contract->agree = $_POST['Contract']['agree'];
...
}
if(!$contract->validate($contract->attributeNames())){
//re-render the form here and it will show up with validation errors marked!
}
The results:

Categories