This is the situation: I'm new on Yii2 and wanted to use some file uploader widget within ActiveForm.. so far I've found this excelent one: \kartik\widget\FileInput
With this widget I can manage file upload and then, when enter in edit mode, show the previous uploaded image with the oportunite to replace it.
The problem is that if I press the "Update" button of the form without modifying the image yii says that the image "can't be empty" because I've set the 'required' rule in my model.
After an awful afternoon and a more productive night, I've encountered a solution that worked for me..
The main problem was that file input don't send its value (name of the file stored in database) when updating. It only sends the image info if browsed and selected through file input..
So, my workaround was creating another "virtual" field for managing file upload, named "upload_image". To achieve this I simple added a public property with this name to my model class: public $upload_image;
I also add the folowing validation to rules method on Model class:
public function rules()
{
return [
[['upload_image'], 'file', 'extensions' => 'png, jpg', 'skipOnEmpty' => true],
[['image'], 'required'],
];
}
Here, 'upload_image' is my virtual column. I added 'file' validation with 'skipOnEmpty' = true, and 'image' is the field on my database, that must be required in my case.
Then, in my view I configured 'upload_image' widget like follows:
echo FileInput::widget([
'model' => $model,
'attribute' => 'upload_image',
'pluginOptions' => [
'initialPreview'=>[
Html::img("/uploads/" . $model->image)
],
'overwriteInitial'=>true
]
]);
In 'initialPreview' option I asign my image name, stored in '$model->image' property returned from database.
Finally, my controller looks like follow:
public function actionUpdate($id)
{
$model = $this->findModel($id);
$model->load(Yii::$app->request->post());
if(Yii::$app->request->isPost){
//Try to get file info
$upload_image = \yii\web\UploadedFile::getInstance($model, 'upload_image');
//If received, then I get the file name and asign it to $model->image in order to store it in db
if(!empty($upload_image)){
$image_name = $upload_image->name;
$model->image = $image_name;
}
//I proceed to validate model. Notice that will validate that 'image' is required and also 'image_upload' as file, but this last is optional
if ($model->validate() && $model->save()) {
//If all went OK, then I proceed to save the image in filesystem
if(!empty($upload_image)){
$upload_image->saveAs('uploads/' . $image_name);
}
return $this->redirect(['view', 'id' => $model->id]);
}
}
return $this->render('update', [
'model' => $model,
]);
}
I have encountered another solution by creating scenarios. In your case I would modify the rules like this:
public funtion rules() {
[['image'], 'file'],
[['image'], 'required', 'on'=> 'create']
}
So the fileupload field will be required only in create action. In update action I have this code:
public function actionUpdate($id)
{
$model = $this->findModel($id);
if ($model->load(Yii::$app->request->post())) {
$newCover = UploadedFile::getInstance($model, 'image');
if (!empty($newCover)) {
$newCoverName = Yii::$app->security->generateRandomString();
unlink($model->cover);
$model->cover = 'uploads/covers/' . $newCoverName . '.' . $newCover->extension;
$newCover->saveAs('uploads/covers/' . $newCoverName . '.' . $newCover->extension);
}
if ($model->validate() && $model->save()) {
return $this->redirect(['view', 'id' => $model->post_id]);
} else {
// error saving model
}
} else {
return $this->render('update', [
'model' => $model,
]);
}
}
In the update scenario the image filed is not required but the code checks if nothing was uploaded and doesn't change the previous value.
My form file:
<?= $form->field($model, 'image')->widget(FileInput::classname(), [
'options' => ['accept'=>'image/*'],
'pluginOptions'=>[
'allowedFileExtensions'=>['jpg', 'gif', 'png', 'bmp'],
'showUpload' => true,
'initialPreview' => [
$model->cover ? Html::img($model->cover) : null, // checks the models to display the preview
],
'overwriteInitial' => false,
],
]); ?>
I think is a little more easier than a virtual field. Hope it helps!
Try preloading the file input field with the contents of that field. This way, you will not lose data after submitting your form.
I looked through kartik file-input widget (nice find, btw) and I came across a way to do this
// Display an initial preview of files with caption
// (useful in UPDATE scenarios). Set overwrite `initialPreview`
// to `false` to append uploaded images to the initial preview.
echo FileInput::widget([
'name' => 'attachment_49[]',
'options' => [
'multiple' => true
],
'pluginOptions' => [
'initialPreview' => [
Html::img("/images/moon.jpg", ['class'=>'file-preview-image', 'alt'=>'The Moon', 'title'=>'The Moon']),
Html::img("/images/earth.jpg", ['class'=>'file-preview-image', 'alt'=>'The Earth', 'title'=>'The Earth']),
],
'initialCaption'=>"The Moon and the Earth",
'overwriteInitial'=>false
]
]);
You may also want to relax the required rule in your model for that field, so it does not complain on validation. You may choose to prompt the user through subtler means.
From Krajee:
http://webtips.krajee.com/advanced-upload-using-yii2-fileinput-widget
Create, delete, update: really easy, look no further.
(1) I've set the 'required' rule in my model too.
(2) To work on Wampserver:
Yii::$app->params['uploadPath'] = Yii::$app->basePath . '/web/uploads/';
Yii::$app->params['uploadUrl'] = Yii::$app->urlManager->baseUrl . '/uploads/';
Related
Hi everyone i'm having trouble with my software developed with yii2.
I Have a model called Anagrafica and with its primary key id. With this model everything works.
I also have a model called AnagraficaOpzioniCarriera which extend the first one.
I have a view anagrafica/index that show a Kartik grid with the data of people enrolled that you can find in anagrafica. Admin user can update the data of an Anagrafica model by clicking on an the attribute "cognome" that render to anagrafica/update.
this is the command that call the controller AnagraficaController to reach anagrafica/update
'cognome'=>Grid::Labels('cognome',['anagrafica/update'],\app\helpers\Permits::allow('anagrafica','update'),'id','btn','info','10%'),
This is AnagraficaController
public function actionUpdate($id,$error=0,$message='')
{
$id = (int)$id;
$model = Anagrafica::findOne(['id' => $id]);
$model->scenario = 'update';
if ($model->load(Yii::$app->request->post())) {
if($model->validate()){
}
if($model->save(false)){
return $this->redirect(['anagrafica/update','id'=>$model->id]);
}
}
}
return $this->render('update', ['model' => $model, 'extended'=>true]);
}
i removed some portions of code to semplify it, but this is the core.
One time the view anagrafica/update is reached in this page i have an ActiveForm to modify data of the model and i have a render to a grid that show the attributes contained in AnagraficaOpzioniCarriera about the $model that i'm updating.
<?= $this->render('_opzioni_carriera',['parent'=>$model]); ?>
anagrafica/_opzioni_carriera view contain a Kartik grid that shows the column in the model AnagraficaOpzioniCarriera
<?php
use kartik\grid\GridView;
use kartik\select2\Select2;
use kartik\widgets\ActiveForm;
use kartik\editable\Editable;
use kartik\widgets\SwitchInput;
use yii\helpers\ArrayHelper;
use app\helpers\Autoconfigurazione;
use app\models\AnagraficaOpzioniCarriera;
use app\helpers\Grid;
use yii\helpers\Html;
use app\helpers\UserInfo;
/* #var $this yii\web\View */
/* #var $model app\models\AnagraficaOpzioniCarriera*/
$model = new AnagraficaOpzioniCarriera(['scenario'=>'search']);
?>
<div class="">
<?php
echo GridView::widget([
'options'=>[
'id'=>'opzioni_carriera',
],
'dataProvider'=> $model->search($parent->id,Yii::$app->request->queryParams),
'showPageSummary'=>false,
'headerRowOptions'=>['class'=>'kartik-sheet-style'],
'pjax'=>true, // pjax is set to always true for this demo
'pjaxSettings'=>[
'neverTimeout'=>true,
],
'toolbar'=> [
[
'content'=>''
],
],
'panel'=>[
'heading'=>false,
'footer'=>false,
'after'=>false,
],
'columns' => Grid::gridColumns([
'model'=>$model,
'checkbox'=>false,
'remove'=>Grid::gridRemove($model),
'extraOptions' =>[
'cashback' =>Grid::YNColumn('cashback',['anagrafica-opzioni-carriera/update', 'id' => $parent->id],'left',true,'5%'),
'compensa'=>Grid::YNColumn('compensa',['anagrafica-opzioni-carriera/update', 'id' => $parent->id],'left',true,'5%'),
'associazione'=>Grid::YNColumn('associazione',['anagrafica-opzioni-carriera/update', 'id' => $parent->id],'left',true,'5%'),
'formazione'=>Grid::YNColumn('formazione',['anagrafica-opzioni-carriera/update', 'id' => $parent->id],'left',true,'5%'),
],
]);
?>
</div>
cashback, compensa etc.. are the attributes in the model AnagraficaOpzioniCarriera.
Here when i try to update this attributes everything looks fine, the function model->validate() and model->load returns true value, but at the end of the process doesn't works.
Honestly i don't know what i have to return from the function of the controller.
public function actionUpdate($id)
{
$model = AnagraficaOpzioniCarriera::findOne(['id_anagrafica' => $id]);
if (!$model) {
// Se l'anagrafica opzioni carriera non esiste, genera un'eccezione 404
throw new \yii\web\NotFoundHttpException(Yii::t('app', 'The requested page does not exist.'));
}
$model->scenario = 'update';
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
if(Yii::$app->request->post('cashback') != null) $model->cashback = Yii::$app->request->post('cashback');
if(Yii::$app->request->post('compensa') != null) $model->cashback = Yii::$app->request->post('compensa');
if(Yii::$app->request->post('associazione') != null) $model->cashback = Yii::$app->request->post('associazione');
if(Yii::$app->request->post('formazione') != null) $model->cashback = Yii::$app->request->post('formazione');
if ($model->save()) {
return Json::encode(["success" => true, 'message' => 'Dati aggiornati']);
}
}
// Mostra il form di modifica
return $this->render('_opzioni_carriera', [
'parent' => $model,
]);
}
anyone can help me? i hope i explained my problem in a good form, but my english is not the best, i know. Anyway thanks in aadvance to everyone who want to try to help me, if you need anything other you can easily ask.
I tried every everything, also a logger but nothing worked
Like someone suggest these are the rules of the model AnagraficaOpzioni, but like i said prevously model->validate() works, for this reason i think the problem is not over there
public function rules()
{
return [
[['id_anagrafica'], 'required'],
[['id_anagrafica'], 'integer'],
[['cashback', 'compensa', 'associazione', 'formazione'], 'required', 'on'=>['update']],
[['cashback', 'compensa', 'associazione', 'formazione'], 'integer'],
[['id_anagrafica', 'cashback', 'compensa', 'associazione', 'formazione',], 'safe', 'on'=>['search']],
];
}
I'm able to add file details in database but not able to update it.
I am able to add file details entries, but when i try to update only the file that i am updating is moved to the storage folder. My update manager doesn't show any errors and doesn't update the file details in database.
this is my file form
protected function addElements()
{
// Add "name" field
$this->add([
'type' => 'file',
'name' => 'image',
'attributes' => [
'id' => 'image'
],
'options' => [
'label' => 'ImageFile',
],
]);
// Add the Submit button
$this->add([
'type' => 'submit',
'name' => 'submit',
'attributes' => [
'value' => 'Add Image File',
'id' => 'submit',
],
]);
// Add the CSRF field
$this->add([
'type' => 'csrf',
'name' => 'csrf',
'options' => [
'csrf_options' => [
'timeout' => 600
]
],
]);
}
public function addInputFilter()
{
$inputFilter = new InputFilter\InputFilter();
// File Input
$fileInput = new InputFilter\FileInput('image');
$fileInput->setRequired(true);
$inputFilter->add($fileInput);
$this->setInputFilter($inputFilter);
}
}
this is the update image manager
public function updateImage($name, $size)
{
$images = new Images();
$images->setName($name);
$images->setSize($size);
// Apply changes to database.
$this->entityManager->flush();
}
and this is my controller
public function editAction()
{
$id = (int)$this->params()->fromRoute('id', -1);
if ($id<1) {
$this->getResponse()->setStatusCode(404);
return;
}
$image = $this->entityManager->getRepository(Images::class)
->find($id);
if ($image == null) {
$this->getResponse()->setStatusCode(404);
return;
}
// Create form
$form = new ImageUploadForm('update', $this->entityManager);
$request = $this->getRequest();
if ($this->getRequest()->isPost()) {
$data = array_merge_recursive(
$request->getPost()->toArray(),
$request->getFiles()->toArray()
);
$form->setData($data);
if($form->isValid()) {
$data = $form->getData();
$imgtmp = $data["image"]["tmp_name"];
$name = $data["image"]["name"];
$size = $data["image"]["size"];
$filepath = $this->_dir.$name;
move_uploaded_file($imgtmp, $filepath);
$this->achimotaImagesManager->updateImage($name, $size);
var_dump($name, $size);
return $this->redirect()->toRoute('images', ['action'=>'index']);
}
}
return new ViewModel([
'form' => $form,
]);
}
Do not create a new object
If you update an Images entity (consider naming it Image if it is one image), you should not create a new one. Hand over the $image you need to update:
public function updateImage($image, $name, $size){
$image->setName($name);
$image->setSize($size);
...
}
Persist before flush
You need to persist the entity before you flush.
$this->entityManager->persist($image);
$this->entityManager->flush();
Organize the code nicer
Do not inject entity manager in your controller. Inject rather a service through a factory, which handles all features of your Image entity. (ImageService.php)
Do not inject entity manager into your ImageService neither. Create a ImageMapper service, inject that into your ImageService. Create all Doctrine-related features in this Mapper. This has this advantage: Doctrin specific functionality is only in your Mapper files. Should you need to use another solution to store data, you only need to replace the Mapper files, providing the Service with the same interface.
Controller
public function editAction()
{
...
$this->serviceImage->update($image,$name,$size);
...
}
Service - ImageService.php
public function update($image,$name,$size)
{
$image->setName($name);
$image->setSize($size);
$this->mapperImage->save($image);
}
Mapper - ImageMapper.php
public function save($image)
{
$this->managerEntity->persist($image);
$this->managerEntity->flush();
}
Consider adding rich comments and typehints to the arguments and return value of the functions.
Moreover
The form should not be created in your controller. Put that code in your ImageService too. And consider inject form into the service. (Make sure you define the form for the factory in the getFormElementConfig()! This is more advance stuff, if you do not test with phpunit, you might not bother creating form as a service, hovever it leads to a very organized codebase.)
var_dump($name, $size) has no place in your controller. (If this is for debug purposes, it is OK, but use rather something like XDebug with a compatible IDE - PHPStorm is far the best one.)
This line is not so easy to understand: $filepath = $this->_dir.$name; Maybe:
$filePath = _dir . $name;
Naming convention: look for camelCase.
I have a form to update my news post which have title, content and image upload button at the same page. The image uploader use Kartik FileINput widget for Yii2 applying Ajax method.
This is my actionUpload that processed the AJAX uploading
public function actionUpload($id)
{
$model = $this->findModel($id);
$currentImage=$model->image;
if (Yii::$app->request->isAjax)
{
\Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
$image = $model->uploadImage();
if($image===false)
{
//echo "image instance is empty";
$model->image = $currentImage;
}
if ($model->validate() && $model->save())
{
if($image !== false)
{
$model->image = $image->basename.'.'.$image->extension;
$image->saveAs('uploads/batam/'.$model->image);
$model->save(false);
return \yii\helpers\Json::encode(array("image" => $model->image,
));
}
}
else
{echo "validation failed";}
}
}
This is my widget file upload filed in my form (note : it's inside the active form)
echo FileInput::widget([
'name' => 'image',
'options'=>[
'multiple'=>false
],
'pluginOptions' => [
'uploadUrl' => Url::to(['news/upload','id'=>$id]),
'uploadExtraData' => [
'id'=>$id,
],
'initialPreview'=>[
Yii::getAlias('#web').'/uploads/batam'.$model->image
],
'initialPreviewAsData'=>true,
'overwriteInitial'=>false,
'maxFileSize'=>2800
]
]);
this is my model rule and uploadImage method
public function rules()
{
return [
[['title', 'content'], 'required'],
[['content'], 'string'],
[['created_at', 'updated_at'], 'safe'],
[['image'], 'file','skipOnEmpty'=>true, 'extensions'=>'jpg,png,jpeg'],
[['title'], 'string', 'max' => 255],
];
}
public function uploadImage() {
$image = UploadedFile::getInstanceByName('image');
if (empty($image)) {
return false;
}
return $image;
}
I have 2 issues here that needs help :
1. The validation rules won't work. I can upload any file with any extension and it's succesfully uploaded and updated in the database
2. The image upload was contained within the active form, so after a successful upload using AJAX, when I press 'update' button, the image won't display. The image field is treated as empty, so the update button behave as if I updated an empty image. In my database the image path of the AJAX-uploaded file of the respected news-id has been succesfully stored and the file is uploaded.
How to solve this? Actually I don't mind if the upload method didn't use AJAX but just the ordinary, single form submit method with post, thus omitting the upload ajax button, but according to Kartik documentary, if I want to use the widget that display the previously stored image, it needs Ajax.
I had looked around some similar question in StackOverflow etc but all of them had the problem with empty image instance in the Ajax call method ( well, I previously faced the same problem, but i had apply the Formdata on my ajax after I researched), while I don't have the problem with the empty instance on ajax call, but empty instance on the normal form submit.
This is my actionUpdate
public function actionUpdate($id)
{
$model = $this->findModel($id);
$image= UploadedFile::getInstanceByName('image');
if ($model->load(Yii::$app->request->post()) && $model->save() ){
var_dump($image);exit();
return $this->redirect(['view', 'id' => $model->id]);
} else {
return $this->render('update', [
'model' => $model,
]);
}
}
Thanks in advance!
First of all, the reason why the validators are not working is because you are NOT creating the file input widget from the model, you must do it this way in order for your rules in the class model to work:
echo $form->field($model, 'image')->widget(FileInput::classname(), [
'options' => ['accept' => 'image/*'],
]);
Then I don't know how the structure of your database, but my advice for how to handle image uploads. Is, first, to create a public variable inside the class separated from the image field of the database, this public image variable will be the one that will instanciate the widget on the form and also the one with the rules. This way, when you post your form, the image field from the database will not be overwrited.
So, then when you upload using ajax, you will send the id of the object too, and in the upload function once everything is set, you will store the path, or the image, or whatever in the image field of the database.
I want to change scenario in view page by jQuery.
There is a checkbox in my form, I want inputbox be required in checked checkbox.
my rule:
public function rules() {
return [
.
.
.
['pass' , 'required', 'on'=> 'checked']
]
}
my view page:
<?=$form->field($model, 'check')->checkbox()?>
<?=$form->field($model, 'Pass')->textInput(['maxlength' => 20])?>
Try to set up your rule like this:
public function rules() {
$checkBoxID = Html::getInputId($this, 'check');
return [
/* other rules */
['pass' , 'required',
'when' => function($model) {
return $model->check;
},
'whenClient'=> "function(attribute, value){
return $('#{$checkBoxID}').prop('checked');
}",
'on' => 'checked'
],
];
}
Read about conditional validation and whenClient validator property. Also consider naming you scenario more informative, e.g. by its purpose, like 'on' => 'sign-up', or 'on' => 'login'. Scenario is useful when you need certain rules to apply in particular cases, but you need to specify this scenario explicitly. Either when you instantiate a model
$model = new MyModel();
$model->scenario = 'sign-up';
and passing it into a view, or before doing any validation after $model->load($data)
i am using Yii2 and Kartik's FileInput extension and I have successfully get the file uploads working(only single upload). My problem now is that, I get the error as in the title(with logs attached) if I did not choose any files(It should be optional).
After much searching over the internet, I think it has to be something to do with array, but I am not sure how to fix that, especially even with the logs pointing to the exact line!
Here is my log,
Here is my model,
namespace app\models;
use Yii;
class FormMovement extends \yii\db\ActiveRecord
{
public $file;
public static function tableName()
{
return 'form_movement';
}
public function rules()
{
return [
[['fm_date_received', 'fm_form_name', 'fm_from', 'fm_ptj'], 'required'],
[['form_id'], 'integer'],
[['fm_date_received', 'fm_date_action1', 'fm_date_action2','fm_upload'], 'safe'],
[['fm_form_name', 'fm_note'], 'string', 'max' => 500],
[['fm_from', 'fm_ptj', 'fm_action1', 'fm_action2'], 'string', 'max' => 100],
[['file'], 'file', 'skipOnEmpty' => true, 'extensions'=>'jpg,pdf,png,doc,docx,xls,xlsx, jpeg', 'maxFiles' => 3],
];
}
My controller function, the log shows that it is at the line 75, which is this one,
$model->fm_upload='uploads/'.$fileName.'.'.$model->file->extension;
Been tinkering with it, but no success.
public function actionCreate()
{
$model = new FormMovement();
if ($model->load(Yii::$app->request->post())) {
//set the file name
$fileName = $model -> fm_form_name;
//get instance
$model->file = UploadedFile :: getInstance($model, 'file');
//set the file path in the db
$model->fm_upload='uploads/'.$fileName.'.'.$model->file->extension;
//save the file to the server directory
$model->save();
$model->file->saveAs('uploads/'.$fileName.'.'.$model->file->extension);
return $this->redirect(['view', 'id' => $model->form_id]);
} else {
return $this->render('create', [
'model' => $model,
]);
}
}
Finally my view,
<div class="form-group kv-fieldset-inline">
<?= Html::activeLabel($model, 'file[]', [
'label'=>'MUAT NAIK FAIL',
'class'=>'col-sm-1 control-label'
]) ?>
<div class="col-sm-8">
<?= $form->field($model, 'file',[
'showLabels'=>false
])->widget(FileInput::classname(), [
'options' => ['accept' => 'file/*', 'multiple' => 'true'],
'pluginOptions'=>[
'showUpload' => false,
]
]) ?>
</div>
</div>
This part should be refactored:
//set the file name
$fileName = $model -> fm_form_name;
//get instance
$model->file = UploadedFile :: getInstance($model, 'file');
//set the file path in the db
$model->fm_upload='uploads/'.$fileName.'.'.$model->file->extension;
//save the file to the server directory
$model->save();
$model->file->saveAs('uploads/'.$fileName.'.'.$model->file->extension);
like this:
$model->file = UploadedFile::getInstance($model, 'file');
$model->save();
if ($model->file) {
$model->fm_upload = "uploads/{$model->fm_form_name}.{$model->file->extension}";
$model->file->saveAs("uploads/{$model->fm_form_name}.{$model->file->extension}");
}
Also note that you don't handle failed validation in your controller at all.
For further refactoring, this line:
$model->file = UploadedFile::getInstance($model, 'file');
can be moved to beforeValidate() event handler.
This part:
if ($model->file) {
$model->fm_upload = "uploads/{$model->fm_form_name}.{$model->file->extension}";
$model->file->saveAs("uploads/{$model->fm_form_name}.{$model->file->extension}");
}
can be moved to afterSave() event handler to keep your controller slim.
In saveAs() it's better to use alias, I desribed it in this answer.