Custom Yii2 grid ActionButton - yii2

I've problem with a section of code that I've created to generate dinamically buttons that I need to include in my yii2 Grid ActionColumn. With this function I can define a button just specifying an array of parameter:
name
controller (destination)
icon
Function works pretty good, but I can't replace the static string "my name" with my variabile $config['icon'] because I can't send the value to the function.
Can I solve this problem? (I'm using Kartik grid estension)
foreach(...) {
$actionColumns['controller'] = $config['controller'];
$actionColumns['buttons'] = array($config['name'] => function ($url, $model, $key) {
return Html::a('my name', $url);
});
$actionColumns['template'] = '{'.$config['name'].'}';
}
Ty

I think you can passing the value by use for closure
function ($url, $model, $key) use ($config['icon'])
{
.....
}
so in your case
foreach(...) {
$actionColumns['controller'] = $config['controller'];
$actionColumns['buttons'] = array($config['name'] =>
function ($url, $model, $key) use ($config['icon']) {
return Html::a($config['icon'], $url);
});
$actionColumns['template'] = '{'.$config['name'].'}';
}

Related

Validation error on custom model attributes not displayed in form

I have a model with the following custom attributes topic_names, topic_details (string fields). I have also a model form with the custom attributes and custom rules. When I insert wrong data in the form fields, there is a model error, but it isn't displayed.
Model code:
......
public function rules()
{
return [
...
[['topics_names','topics_details'],'string'],
[['topics_names'],'checkCorrectAndSetTopics'],
];
}
public function checkCorrectAndSetTopics(){
if($this->topics_names AND $this->topics_details){
$topicsNamesArray = explode(',',$this->topics_names);
$topicsDetailsArray = explode(';',$this->topics_details);
if(sizeof($topicsNamesArray) !== sizeof($topicsDetailsArray)){
$this->addError('topics_names', \Yii::t('app', 'The topics names and details sets have different sizes'));
return FALSE;
}
}
return TRUE;
}
The problem is when the second rules is violeted, the form doesn't show any error, but there is. I checked it debugging the code below.
Form code:
..........
<?php
ActiveForm::$autoIdPrefix = createRandomId();//Function which creates a random id
$form = ActiveForm::begin(
['enableAjaxValidation' => true, "options"=> ["class"=>"extra-form"]]);
?>
<?= $form->errorSummary($model); ?>
<?= $form->field($model, 'topics_names')->textInput()
->label(\Yii::t('app', 'Topics Names'))?>
<?= $form->field($model, 'topics_details')->textarea(['rows' => 6])
->label(\Yii::t('app', 'Topics Details'))?>
........
Controller code:
public function actionAddExtraData($id){
if(!Yii::$app->request->isAjax){
throw new ForbiddenHttpException(\Yii::t('app','Cannot access this action directly.'));
}
$event = $this->findModel($id);
$extraData = ExtraData::find()
->andWhere(['event_id'=>$id])
->one();
if(!$extraData){
$extraData = new ExtraData();
$extraData->event_id = $id;
}else{
$extraData->prePerformForm();//Insert data on custom attributes
}
if(Yii::$app->request->isPost AND Yii::$app->request->isAjax AND Yii::$app->request->post("submitting") != TRUE
AND $extraData->load(Yii::$app->request->post())){
Yii::$app->response->format = Response::FORMAT_JSON;
$validation = ActiveForm::validate($extraData);
return $validation;
}
if ($extraData->load(Yii::$app->request->post()) && $extraData->save()) {
if (Yii::$app->request->isAjax) {
Yii::$app->response->format = Response::FORMAT_JSON;
return ["success" => TRUE];
} else {
return $this->redirect(Yii::$app->request->referrer);
}
}
return $this->renderAjax('_event_extra_form',['model'=>$extraData,'event'=>$event]);
}
First thing, pretty sure you don't need to return true or false, you just need to add error. Second thing, in your example you name the attribute, you can actually get this when defining the function, so your function could look something like this
public function checkCorrectAndSetTopics($attribute, $model){
if($this->topics_names AND $this->topics_details){
$topicsNamesArray = explode(',',$this->topics_names);
$topicsDetailsArray = explode(';',$this->topics_details);
if(sizeof($topicsNamesArray) !== sizeof($topicsDetailsArray)){
$this->addError($attribute, \Yii::t('app', 'The topics names and details sets have different sizes'));
}
}
}

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'],
];
}

Yii2 POST image to model in API without Yii2 Naming convention

I'm creating an endpoint for a mobile application to send a image to the server. I'm posting the image with the POSTMAN extension for chrome. The image is in the $_FILES variable, and named image. How can I load this image into a model, or the UploadedFile class? The $model->load(Yii::$app->request->post()) line does not correctly load the file, as it is not in Yii2's naming convention for forms.
It's currently returning:
{
"success": false,
"message": "Required parameter 'image' is not set."
}
Code
models\Image.php
<?php
namespace api\modules\v1\models;
use yii\base\Model;
use yii\web\UploadedFile;
class Image extends Model
{
/**
* #var UploadedFile
*/
public $image;
public function rules()
{
return [
[['image'], 'file', 'skipOnEmpty' => false, 'extensions' => 'png, jpg'],
];
}
public function upload()
{
$path = dirname(dirname(__FILE__)) . '/temp/';
if ($this->validate()) {
$this->image->saveAs($path . $this->image->baseName . '.' . $this->image->extension);
return true;
} else {
die(var_dump($this->errors));
return false;
}
}
}
controllers\DefaultController.php
<?php
namespace api\modules\v1\controllers;
use api\modules\v1\models\Image;
use yii\web\Controller;
use yii\web\UploadedFile;
use Yii;
class DefaultController extends Controller
{
public $enableCsrfValidation = false;
public function actionIndex()
{
Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
$model = new Image();
if (Yii::$app->request->isPost) {
if($model->load(Yii::$app->request->post()))
{
$model->image = UploadedFile::getInstance($model, 'image');
if ($model->upload()) {
// file is uploaded successfully
return ['success' => true, 'message' => 'File saved.'];
}
else return ['success' => false, 'message' => 'Could not save file.'];
}
else return ['success' => false, 'message' => 'Required parameter \'image\' is not set.'];
}
else return ['success' => false, 'message' => 'Not a POST request.'];
}
}
Postman
Your problem seems to be the name you are using to send the image file. Usually Yii2 uses names for form attributes like "ModelName[attributeName]" and you are sending your image file with the name "image"
There are 2 ways of fixing this:
Change the name you use to send your image file to follow the same naming conveniton. However you don't seem to want that.
Use getInstanceByName('image') method instead of getInstance($model, 'image')
The problem come here
When you send files via api they are not sent asynchronously. If you check
echo '<pre>';
print_r($_FILES); //returns nothing
print_r($_POST["image"]); //returns something
echo '</pre>';
die;
One reason is that your controller extendsyii\web\controller which is not used by rest apis, extend yii\rest\controller
The other way to go about this is by using javascript formData when sending the post request
This is a way i handled a previous ajax post of an image probably itll give you a guideline
The form
<?php $form = ActiveForm::begin(['options' => ['enctype' =>
'multipart/form-data','id'=>'slider_form']]); ?> //dont forget enctype
<?= $form->field($model, 'file')->fileInput() ?>
Then on the ajax post
var formData = new FormData($('form#slider_form')[0].files);
$.post(
href, //serialize Yii2 form
{other atributes ,formData:formData}
)
Then on the controller simply access via
$model->file =$_FILES["TblSlider"]; //here this depends on your form attributes check with var_dump($_FILES)
$file_tmp = $_FILES["TblSlider"]["tmp_name"]["file"];
$file_ext = pathinfo($_FILES['TblSlider']['name']["file"], PATHINFO_EXTENSION);
if(!empty($model->file)){
$filename = strtotime(date("Y-m-d h:m:s")).".".$file_ext;
move_uploaded_file($file_tmp, "../uploads/siteimages/slider/".$filename);
///move_uploaded_file($file_tmp, Yii::getAlias("#uploads/siteimages/slider/").$filename);
$model->image = $filename;
}
I hope this helps

Yii2: How to map a CSV string in an attribute to CheckboxList in a form?

I have a model with an attribute that holds a CSV string.
(The model is actually an ActiveRecord object but I guess this is not important. Correct me if I'm wrong.)
/**
* #property string $colors Can be something like "red" or "red,green,blue" or ""
*/
class Product extends Model {
}
And I have a form in which I'd like to display this attribute as a checkboxList so that the user can select the possible values with simple clicks instead of typing into a textInput.
Theoretically, it should look similar to this:
<?php $availableColors = ['red' => 'Red', 'green' => 'Green', 'blue' => 'Blue']; ?>
<?php $form = ActiveForm::begin([]); ?>
<?= $form->field($model, 'colors')->checkboxList($availableColors) ?>
<?php ActiveForm::end(); ?>
This does obviously not work since the field colors would need to be an array. But in my model it is a string.
What would be a good way to achieve that? With JS or pseudo attributes? The colors attribute must not be changed since it is already used in other contexts that shouldn't be modified.
You can override beforeValidate method in your model, to implode your colors array into string. In your view you can use following:
<?= $form->field($model, 'colors')->checkboxList($availableColors,
[
'item'=> function ($index, $label, $name, $checked, $value) use ($model) {
$colors = explode(';', $model->colors);
$checked = in_array($value, $colors);
return Html::checkbox($name, $checked, [
'value' => $value,
'label' => $label,
]);
}
]) ?>
CSV is a file format used for moving tabular data between programs that natively operate on incompatible formats. Using it as a model attribute is not very elegant (to say it nicely). In my opinion you should have started out storing your colors in an array.
That being said you can achieve converting the array data from the dropdown list to CSV using the beforeValidate() function in your model:
public function beforeValidate() {
$this->colors = explode(';', $this->colors);
return parent::beforeValidate();
}
I think this is a PHP question, but anyway you can use PHP explode for build the array you need. See here for more details and then user the array inside the checkboxList
Now I solved it with an extra model for the form. This seems to me a proper solution.
/**
* #property string $colors Can be something like "red" or "red,green,blue" or ""
*/
class Product extends Model {
}
/**
* #property string[] $colorsAsArray
*/
class ProductForm extends Product {
public function rules() {
return array_merge(parent::rules(), [
['colorsAsArray', 'safe'] // just to make it possible to use load()
]);
}
public function getColorsAsArray() {
return explode(',', $this->colors);
}
public function setColorsAsArray($value) {
$this->colors = self::implode($value);
}
protected static function implode($value) {
if ($value == 'none-value') return '';
return implode(',', $value);
}
/* - - - - - - - - - - optional - - - - - - - - - - */
public function attributeLabels() {
$attributeLabels = parent::attributeLabels();
return array_merge($attributeLabels, [
'colorsAsArray' => $attributeLabels['colors'],
]);
}
}
With this I can use the form that way:
<?php $availableColors = ['red' => 'Red', 'green' => 'Green', 'blue' => 'Blue']; ?>
<?php $form = ActiveForm::begin([]); ?>
<?= $form->field($model, 'colorsAsArray')
->checkboxList($availableColors, ['unselect' => 'none-value']) ?>
<?php ActiveForm::end(); ?>
Of course, now the controller has to use the inherited model class.
The solution deals also with the issue if no checkbox is selected. That is why 'none-value' is introduced.

create an expandable grid view in Yii2

I am trying to create an expandable grid view in Yii2 but I have some problems.
I get this warning:
PHP Warning – yii\base\ErrorException
reset() expects parameter 1 to be array, null given
in C:\xampp\htdocs\advanced\vendor\yiisoft\yii2\grid\DataColumn.php at line 129
$provider = $this->grid->dataProvider;
if ($this->label === null) {
if ($provider instanceof ActiveDataProvider && $provider->query instanceof ActiveQueryInterface) {
/* #var $model Model */
$model = new $provider->query->modelClass;
$label = $model->getAttributeLabel($this->attribute);
} else {
$models = $provider->getModels();
129 if (($model = reset($models)) instanceof Model) {
/* #var $model Model */
$label = $model->getAttributeLabel($this->attribute);
} else {
$label = Inflector::camel2words($this->attribute);
}
}
} else {
$label = $this->label;
}
This is my search model code:
<?php
namespace app\models;
use Yii;
use yii\data\ActiveDataProvider;
use yii\base\Model;
use app\models\Articles;
/**
*
*/
class ArticlesSearch extends Model
{
/* your calculated attribute */
public $article_num;
public $title;
public $jour_id;
/* setup rules */
public function rules() {
return [
/* your other rules */
[['title'], 'safe']
];
}
public function search($params) {
$query = Articles::find()->select('*')
->where(['`journal_id`'=>$this->jour_id]);
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
if (!($this->load($params) && $this->validate())) {
return $dataProvider;
}
$query->orderBy('`articles`.`publication_date` ASC ');
$query->andWhere(['LIKE','title',$this->title]);
// $query->orFilterWhere(['like', '`publishers`.`name`', $this->name]);
return $dataProvider;
}
public function getCount()
{
}
public function getModels()
{
}
}
I added two last methods to my class to bypass the following error:
Calling unknown method: app\models\ArticlesSearch::getCount()
Calling unknown method: app\models\ArticlesSearch::getModels()
I don't know why I need this two methods in my class. I have written two other search models and I didn't put these methods there and they work fine!!!
Controller code:
$dataProvider=new ArticlesSearch();
$dataProvider->jour_id=$param['journalID'];
$searchModel= $dataProvider->search(Yii::$app->request->queryParams);
return $this->render('index', [ 'searchModel' => $searchModel,
'dataProvider' => $dataProvider]);
View code:
<?= GridView::widget([
'dataProvider' => $dataProvider,
'filterModel' =>$searchModel,
'columns' => [
[
'class'=>'kartik\grid\SerialColumn',
'contentOptions'=>['class'=>'kartik-sheet-style'],
//'width'=>'36px',
'header'=>'',
'headerOptions'=>['class'=>'kartik-sheet-style']
],
[
'class'=>'kartik\grid\CheckboxColumn',
'headerOptions'=>['class'=>'kartik-sheet-style'],
],
[
'class'=>'kartik\grid\ExpandRowColumn',
//'width'=>'50px',
'value'=>function ($model, $key, $index, $column) {
return GridView::ROW_COLLAPSED;
},
'detail'=>function ($model, $key, $index, $column) {
// return Yii::$app->controller->renderPartial('_expand-row-details', ['model'=>$articles]);
},
'headerOptions'=>['class'=>'kartik-sheet-style']
//'disabled'=>true,
//'detailUrl'=>Url::to(['/site/test-expand'])
],
[
'attribute'=>'Title',
//'value' => $model->title,
// 'width'=>'410px',
],
],
]);
?>
Any help would be much appreciated.
Try this amended code and see if it works. I've added some comments to explain what is going on.
<?php
namespace app\models;
use Yii;
use yii\data\ActiveDataProvider;
use app\models\Articles;
/**
** firstly, your search model needs to extend your original class, that was you have access to all the original attributes of your model, without having to declare them again.
**/
class ArticlesSearch extends Articles
{
//If your original model already has these properties, you don't need to declare them again.
public $article_num;
public $title;
public $jour_id;
/* Here you should declare rules for ALL the attributes that you want to use in search */
public function rules() {
return [
/* your other rules */
[['title', 'jour_id', 'article_num'], 'safe']
];
}
public function search($params) {
//Start by defining your basic search function
$query = Articles::find();
//Add in the dataProvider
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
//Start building the query that will be used to retrieve results
$query->where(['jour_id' => $this->jour_id]);
//Try to load the $params and validate them. If this fails, just return the dataProvider and do nothing else
if (!($this->load($params) && $this->validate())) {
return $dataProvider;
}
//If we're still in the method, carry on building the query
$query->orderBy('`articles`.`publication_date` ASC ');
//andFilterWhere() is better because it ignores empty values
$query->andFilterWhere(['LIKE','title',$this->title]);
return $dataProvider;
}
//Finally, remove the two extras functions you put in. If everything is working, you shouldn't need them.
}
Next, in your controller, use this code;
//Here you are telling Yii what model you want to use for searching, and for generating the form for the grid search.
$searchModel = new ArticlesSearch();
//I'm not sure where this $param is coming from.
$dataProvider->jour_id=$param['journalID'];
//Now you are actually setting up the dataProvider for the grid view. Notice that your $searchModel->search() method always returns a dataProvider, so this is the correct way to do it. Yii::$app->request->queryParams is loading the parameters that the search method will use for it's parameters.
$dataProvider = $searchModel->search(Yii::$app->request->queryParams);
return $this->render('index', ['searchModel' => $searchModel, 'dataProvider' => $dataProvider]);
Now you should be able to use the grid widget as normal, use the dataProvider and searchModel for your dataProvider and filterModel respectively, but don't mix them up!