I'm implement FileInput in a form, when I create a new record or change the attribute in update it works correctly. However when I update the record without changing the file attribute I get a javascript error 'File cannot be empty' despite the file is already saved and I don't want to update it
echo $form->field($model, 'image')->fileInput();
echo $form->field($model, 'image')->hiddenInput(['value'=> $model->image]);


Thanks
| Q | A |
| --- | --- |
| Yii version | 2.0.8-dev |
| PHP version | 5.5.9-1ubuntu4.14 |
| Operating system | Linux Elementary OS Freya |
Yes, that's definitely not good behavior.
The way I solved it is by creating several new classes and modifying existing behavior in the yii\helpers\Html class.
common/helpers/Html.php
namespace common\helpers;
/**
* @inheritdoc
*/
class Html extends \yii\helpers\Html
{
/**
* @inheritdoc
*/
public static function activeFileInput($model, $attribute, $options = [])
{
// add a hidden field to know when a file has already been uploaded
$hiddenOptions = ['id' => null];
if (isset($options['name'])) {
$hiddenOptions['name'] = $options['name'];
}
return static::activeHiddenInput($model, $attribute, $hiddenOptions)
. static::activeInput('file', $model, $attribute, $options);
}
}
common/validators/FileHandlerValidator.php
namespace common\validators;
use SPLFileInfo;
use Yii;
use yii\helpers\FileHelper;
use yii\web\UploadedFile;
class FileHandlerValidator extends \yii\validators\FileValidator
{
public $folderPath = '@webroot';
public $notFoundFileName;
public $invalidFileName;
public $notSavedFile;
public $fileName;
public function init()
{
parent::init();
if ($this->notFoundFileName === null) {
$this->notFoundFileName = 'The file "{fileName}" was not found on folder "{folderPath}".';
}
if ($this->invalidFileName === null) {
$this->invalidFileName = 'The file name "{fileName}" is not valid.';
}
if ($this->notSavedFile === null) {
$this->notSavedFile = 'The file could not be saved to "{folderPath}" error: "{error}".';
}
}
public function validateAttribute($model, $attribute)
{
parent::validateAttribute($model, $attribute);
if (!$model->hasErrors($attribute)
&& $model->$attribute instanceof UploadedFile
) {
$file = $model->$attribute;
$model->$attribute = $this->fileName($file);
if (!$file->saveAs(Yii::getAlias(
"{$this->folderPath}/{$model->$attribute}"
))) {
$this->addError($model, $attribute, $this->notSavedFile, [
'folderPath' => $this->folderPath,
'error' => $file->error,
]);
}
}
}
public function validateValue($file)
{
if (is_string($file)) {
if (in_array(substr($file, 0, 1), ['/', '.'])) {
return [$this->invalidFileName, ['fileName' => $file]];
}
$fileInfo = new SPLFileInfo(
Yii::getAlias($this->folderPath . $file)
);
if ($fileInfo->getRealPath() === false) {
return [$this->notFoundFileName, [
'folderPath' => $this->folderPath,
'fileName' => $file,
]];
}
if (!empty($this->extensions)
&& !$this->validateFileInfoExtension($fileInfo)
) {
return [$this->wrongExtension, [
'file' => $file,
'extensions' => implode(', ', $this->extensions)
]];
}
return null;
} elseif ($file instanceof UploadedFile) {
// execute previos funcionality
return parent::validateValue($file);
}
return [$this->invalidFileName, ['fileName' => '']];
}
public function fileName(UploadedFile $file)
{
if (!empty($this->fileName)) {
return call_user_func($this->fileName, $file);
}
return $file->name;
}
public function validateFileInfoExtension(SPLFileInfo $fileInfo)
{
if ($this->checkExtensionByMimeType) {
$mimeType = FileHelper::getMimeType(
$fileInfo->getRealPath(),
null,
false
);
if ($mimeType === null) {
return false;
}
if (!in_array(
$fileInfo->getExtension(),
FileHelper::getExtensionsByMimeType($mimeType),
true
)) {
return false;
}
}
if (!in_array($fileInfo->getExtension(), $this->extensions, true)) {
return false;
}
return true;
}
}
commonmodelsGalleryImage.php
namespace common\models;
/**
* @property integer $id
* @property string $title
* @property string $description
* @property string|UploadedFile $file
*/
class GalleryImage extends \yii\db\ActiveRecord
{
public function rules()
{
return [
[['file'], 'required'],
[['title', 'description'], 'string'],
[
['file'],
'common\\validators\\FileHandlerValidator',
'folderPath' => '@webroot/galeries'
],
];
}
}
If $model->file receives an string that string will be checked know if there is a file with that name in folderPath then security validations will be executed on that file. That allows me to let the user be able to choose from a list of existing images on the galleries or upload a new one when creating or updating a GalleryImage record.
The hidden field generated by fileInput() is also breaking CodeCeption acceptance tests when using the form field name as the selector.
Situation:
Tests:
$I->selectOption('UserCertification[certification_option_id]', $this->certification['name']);
$I->seeElement('#usercertification-expiration[required]');
$I->fillField('UserCertification[expiration]', $expiration);
$I->attachFile('[name="UserCertification[file]"]', $filename);
$I->click('Add Certification', 'form div.form-group button.btn.btn-success');
Error:
[LogicException] A FileFormField can only be created from an input tag with a type of file (given type is hidden).
Screen Cap:

@davidjeddy
$I->attachFile('[name="UserCertification[file]"][type="file"]', $filename);
we'd need some frontend tests for the framework to verify and fix this correctly...
After #12840
The hidden input should render with given attribute value instead of empty string. It would help in all scenarios where the form has file input field(s) + other fields and the user input does not validate. You still want to accept inputs for the file input field, but don't want to push the user to re-select the file from his PC every time the form does not validate. This little change in helpers/BaseHtml.php would solve this use case:
public static function activeFileInput($model, $attribute, $options = [])
{
// add a hidden field so that if a model only has a file field, we can
// still use isset($_POST[$modelClass]) to detect if the input is submitted
//$hiddenOptions = ['id' => null, 'value' => ''];
$hiddenOptions = ['id' => null, 'value' => $model->$attribute];
if (isset($options['name'])) {
$hiddenOptions['name'] = $options['name'];
}
return static::activeHiddenInput($model, $attribute, $hiddenOptions)
. static::activeInput('file', $model, $attribute, $options);
}
@TomasMolnar Would you mind adding some test alongside your proposed change? It would go a long way towards getting your fix implemented for a future release.
I am sorry for my amateurism, I never wrote a piece of test...
@TomasMolnar Not time like the present to start :). Message me if you would like some assistance.
I think the desired behavior for solving this problem can be obtained by using scenarios, and setting FileValidator::skipOnEmpty based on your needs.
Model:
...
const SCENARIO_UPDATE = 'update';
public function rules()
{
return [
...
[['image'], 'file', 'skipOnEmpty' => ($this->scenario === self::SCENARIO_UPDATE), 'on' => [self::SCENARIO_DEFAULT, self::SCENARIO_UPDATE]],
];
}
...