Yii2 - Validating nested objects - yii

Here's a question about a topic that Ive been thinking about for a while.
In Yii2, it is recommended generally to create Form Models for your requests. Rules are added to those models to validate the input. An example is the EntryForm in the Yii2 guide
<?php
namespace app\models;
use Yii;
use yii\base\Model;
class EntryForm extends Model
{
public $name;
public $email;
public function rules()
{
return [
[['name', 'email'], 'required'],
['email', 'email'],
];
}
}
My problem is, when we have nested objects. An example is a form for creating Customer with multiple Branches. If Customer and Branch are two separate models, but both get submitted in one form, what is the best option for validating input from such a nested form. Bear in mind that here the input is nested. Example:
{
"name": "customer",
"vat_no": "12345678",
"time_zone": 277,
"category": 1,
"email": "customer#mycustomer.com",
"stores":[
{
"name": "store1",
"phone": 1234567
},
{
"name": "store2",
"phone": 2345678
}
]
}

For simple cases you may use one model and custom validator inside of your form model:
public function rules() {
return [
// ...
['stores', 'validateStores'],
];
}
public function validateStores() {
$phoneValidator = new StringValidator(); // use real validators
$nameValidator = new StringValidator(); // use real validators
foreach ($this->stores as $store) {
if (!$phoneValidator->validate($store['phone'], $error)) {
$this->addError('stores', $error);
return; // stop on first error
}
if (!$nameValidator->validate($store['name'], $error)) {
$this->addError('stores', $error);
return; // end on first error
}
}
}
validateStores() may be extracted to separate validator class, then you may also use EachValidator instead of foreach.
For more complicated nested models you should probably create separate StoreForm model for stores (so you will have nested form models), and call validate() on children.
/**
* #var StoreForm[]
*/
public $stores;
public function rules() {
return [
// ...
['stores', 'validateStores'],
];
}
public function validateStores() {
foreach ($this->stores as $store) {
if (!$store->validate()) {
$this->addError('stores', 'Stores config is incorrect.');
return;
}
}
}

Related

How to change JSON returned by query using Helidon 2.0.0-M-2

I'm using Helidon 2.0.0-M2.
When I run the query below I get back a list of JSON objects.
dbClient.execute(exec -> exec.createNamedQuery("select-dsitem-by-id")
.addParam("userId", dataItemId)
.execute())
.thenAccept(response::send)
.exceptionally(throwable -> sendError(throwable, response));
Returned list
[
{
"data": "qwerty",
"user_id": "12345"
},
{
"data": "qwerty123",
"user_id": "22345"
}
]
The attribute names seem to be taken directly from the database column name. e.g. one attribute name returned is "user_id". However, I want it to be "userId". I also want to create a parent wrapper for this list like:
{
"userList": [
{
"data": "qwerty",
"user_id": "12345"
},
{
"data": "qwerty123",
"user_id": "22345"
}
]
}
What is the best way to do this with the dbclient?
Thanks
Simple approach:
Change your SQL statement to return the correct name, such as:
SELECT data, user_id as userId FROM mytable
Complicated approach:
We are working on a better support to map to a JSON stream.
Currently there is only one (a bit complicated) way to achieve this:
You can create a custom mapper from a DbRow to JsonObject. This mapper needs to be a general one (it must work for any DbRow of any query).
The built-in mapper uses metadata provided on the columns. I have prepared a simple example (that just expects to have a single type of statements):
class DbRecordMapperProvider implements DbMapperProvider {
private static final DbMapper<JsonObject> MAPPER = new DbRecordMapper();
#SuppressWarnings("unchecked")
#Override
public <T> Optional<DbMapper<T>> mapper(Class<T> aClass) {
if (JsonObject.class.equals(aClass)) {
return Optional.of((DbMapper<T>)MAPPER);
}
return Optional.empty();
}
}
class DbRecordMapper implements DbMapper<JsonObject> {
#Override
public JsonObject read(DbRow dbRow) {
return Json.createObjectBuilder()
.add("name", dbRow.column("FIRSTPART").as(String.class))
.add("message", dbRow.column("SECONDPART").as(String.class))
.build();
}
#Override
public Map<String, ?> toNamedParameters(JsonObject dbRecord) {
return dbRecord;
}
#Override
public List<?> toIndexedParameters(JsonObject dbRecord) {
throw new IllegalStateException("Cannot convert json object to indexed parameters");
}
}
The important method is public JsonObject read(DbRow dbRow).
Once you have such a DbMapperProvider, you register it with the DbClient:
dbClient = DbClient.builder()
.config(config.get("db"))
.mapperProvider(new DbRecordMapperProvider())
.build();

How to validate two dimensional array in Yii2

How to validate two dimensional array in Yii2.
passenger[0][name] = bell
passenger[0][email] = myemail#test.com
passenger[1][name] = carson123
passenger[1][email] = carson###test.com
how to validate the name and email in this array
Thanks
Probably the most clean solution for validating 2-dimensional array is treating this as array of models. So each array with set of email and name data should be validated separately.
class Passenger extends ActiveRecord {
public function rules() {
return [
[['email', 'name'], 'required'],
[['email'], 'email'],
];
}
}
class PassengersForm extends Model {
/**
* #var Passenger[]
*/
private $passengersModels = [];
public function loadPassengersData($passengersData) {
$this->passengersModels = [];
foreach ($passengersData as $passengerData) {
$model = new Passenger();
$model->setAttributes($passengerData);
$this->passengersModels[] = $model;
}
return !empty($this->passengers);
}
public function validatePassengers() {
foreach ($this->passengersModels as $passenger) {
if (!$passenger->validate()) {
$this->addErrors($passenger->getErrors());
return false;
}
}
return true;
}
}
And in controller:
$model = new PassengersForm();
$model->loadPassengersData(\Yii::$app->request->post('passenger', []));
$isValid = $model->validatePassengers();
You may also use DynamicModel instead of creating Passanger model if you're using it only for validation.
Alternatively you could just create your own validator and use it for each element of array:
public function rules() {
return [
[['passengers'], 'each', 'rule' => [PassengerDataValidator::class]],
];
}
You may also want to read Collecting tabular input section in guide (unfortunately it is still incomplete).

Yii2 - loadMultiple with form model

I have a very simple scenario where I'm receiving a list of Variance Positions from the end user. To be able to validate the structure of the input, I created the following model for the single item that I should receive:
class VariancePositionsForm extends Model{
public $id;
public $position;
public function rules()
{
return [
[['id','position'], 'required'],
[['id', 'position'], 'integer'],
];
}
}
And in the controller, I have the following:
$variancePositions = [];
for($i=0;$i<sizeof(Yii::$app->request->post());$i++)
{
$variancePositions[] = new VariancePositionsForm();
}
VariancePositionsForm::loadMultiple($variancePositions, Yii::$app->request->post());
When I try to var_dump($variancePositions) however, I'm finding that its empty. In other words, loadMultiple() is not loading the models. What am I doing wrong?
Because you don't load the model from the form, only from json you have to add an empty string into the last parameter in this function:
VariancePositionsForm::loadMultiple($variancePositions, Yii::$app->request->post(), '');
look here:
https://github.com/yiisoft/yii2/blob/master/framework/base/Model.php#L884

Yii2 - Unique validator two attributes different models

I have a model Printer, a model Category, and a model for the relation between the two models CategoryPrinterRel
In the CategoryPrinterRel model I need a unique validator between the $category_id, and the client of the printer $printer->client_id
Up till now I have tried
public function rules()
{
[['category_id', $this->printer->client_id], 'unique', 'targetAttribute' => ['category_id']]
}
Is there any other way to do this though?
The problem with the method I'm using is that when the printer object is empty, trying $this->printer->client_id gives an error
I was looking for something more elegant, or built in. For now I have opted for a custom validator however. In the model:
public function rules()
{
return [
[['category_id', 'printer_id'], 'integer'],
[['printer_id', 'category_id'], 'required'],
[['cat_id'],'validateUniquenessOnClient']
];
}
public function validateUniquenessOnClient($attribute, $params, $validator)
{
$isPrinterUniqueOnClient = DbPrinterRepository::IsPrinterCatRelUniqueOnClient($this->category_id, $this->printer_id);
if(!$isPrinterGroupUniqueOnClient)
{
$this->addError($attribute, "There is already a printer using that category ({$this->cat->name}).");
}
}

Extended CRUD, function formSubmit don't get all form elements?

i have a problem, i cannot get values (added with $form->addField) from form in CRUD. I just get what's in the model, but there just isn't extra values...
MODEL:
class Model_Admin extends Model_Table {
public $table ='admin';
function init(){
parent::init();
$this->addField('name')->mandatory('Name required');
$this->addField('email')->mandatory('Email required');
$this->addField('password')->type('password')->mandatory('Password required');
}
}
On page i create extended CRUD and add two more fields:
$adminCRUD = $this->add('MyCRUD');
$adminCRUD->setModel('Admin');
if($adminCRUD->isEditing('add')){
$adminCRUD->form->addField('line','test2','TEST LINE');
$adminCRUD->form->addField('DropDown', 'appfield','Manages Applications')
->setAttr('multiple')
->setModel('Application');
}
Extended CRUD:
class MyRUD extends CRUD {
function formSubmit($form)
{
//var_dump($form);
var_dump($form->get('test2'));
var_dump($form->get('appfield'));
try {
//$form->update();
$self = $this;
$this->api->addHook('pre-render', function () use ($self) {
$self->formSubmitSuccess()->execute();
});
} catch (Exception_ValidityCheck $e) {
$form->displayError($e->getField(), $e->getMessage());
}
}
}
I get null for $form->get('test2') or $form->get('appfield'). I checked whole $form object and there isn't values from test2.. Somwhere in the process gets lost (or droped), how to get it in extended CRUD?
Thanks in advance!