Yii2图片上传时change事件未完成即触发submit报错问题排查
Yii2 配合 jQuery 自动提交图片上传报错问题修复
问题背景
开发中通过监听文件选择框的change事件,选中图片后自动触发表单提交,后端抛出Attempt to read property "name" on null错误,最初怀疑是前端事件提前触发导致,相关代码如下:
视图文件 create.php
<?php \yii\bootstrap4\ActiveForm::begin([ 'options' => [ 'enctype' => 'multipart/form-data', 'id' => 'dynamic-form' ] ]) ?> <button class="btn btn-primary btn-file"> Select Thumbnail <input type="file" id="recipeThumbnail" name="recipe"> </button> <?php \yii\bootstrap4\ActiveForm::end() ?>
JS 文件 app.js
$(function () { 'use strict'; $('#recipeThumbnail').change(ev => { $(ev.target).closest('form').trigger('submit'); }) });
模型文件 Recipe.php
public function save($runValidaiton = true, $attributeNames = null) { $isInsert = $this->isNewRecord; if ($isInsert) { $this->recipe_id = Yii::$app->security->generateRandomString(16); $this->name = $this->recipe->name; } $saved = parent::save($runValidaiton, $attributeNames); if (!$saved) { return false; } if ($isInsert) { $recipePath = Yii::getAlias('@frontend/web/storage/thumbnail/' . $this->recipe_id . '.jpg'); if (!is_dir(dirname($recipePath))) { FileHelper::createDirectory(dirname($recipePath)); } $this->recipe->saveAs($recipePath); } return true; }
控制器文件 RecipeController.php
public function actionCreate() { $model = new Recipe(); $model->recipe = UploadedFile::getInstanceByName('recipe'); if ($this->request->isPost) { if (Yii::$app->request->isPost && $model->save()) { return $this->redirect(['view', 'recipe_id' => $model->recipe_id]); } } else { $model->loadDefaultValues(); } return $this->render('create', [ 'model' => $model, ]); }
根因定位
该错误和前端change事件触发时机无关:jQuery对file类型input的change事件,默认就是在用户完成文件选择、输入框值更新完成后才触发,不存在提前执行的问题。报错完全来自后端逻辑缺陷,核心问题有两个:
- 控制器代码执行顺序错误:获取上传文件实例的代码写在了POST请求判断外,GET请求访问页面、上传大小超限、文件上传失败等场景下,
UploadedFile::getInstanceByName('recipe')会返回null,该null值会被直接赋值给$model->recipe - 模型层缺少空值校验:save方法中未判断
$this->recipe是否为有效上传实例,直接访问其name属性、调用saveAs方法,只要上传实例为null就会抛出属性访问错误
修复方案
1. 调整控制器代码执行顺序
将上传文件获取逻辑移到POST请求判断内部,仅在确实收到表单提交请求时才处理上传文件:
public function actionCreate() { $model = new Recipe(); if ($this->request->isPost) { // 仅POST请求下获取上传实例 $model->recipe = UploadedFile::getInstanceByName('recipe'); if ($model->save()) { return $this->redirect(['view', 'recipe_id' => $model->recipe_id]); } } else { $model->loadDefaultValues(); } return $this->render('create', [ 'model' => $model, ]); }
2. 模型层增加校验逻辑
首先在模型的rules()方法中添加上传文件验证规则,从框架层面拦截无效上传:
public function rules() { return [ // 其他原有规则... [['recipe'], 'file', 'skipOnEmpty' => false, 'extensions' => 'jpg,jpeg,png', 'maxSize' => 2*1024*1024], ]; }
然后修改save方法,增加空值判断,避免直接访问null的属性:
public function save($runValidaiton = true, $attributeNames = null) { $isInsert = $this->isNewRecord; if ($isInsert) { $this->recipe_id = Yii::$app->security->generateRandomString(16); // 确认上传实例有效再读取属性 if ($this->recipe) { $this->name = $this->recipe->name; } } $saved = parent::save($runValidaiton, $attributeNames); if (!$saved) { return false; } if ($isInsert && $this->recipe) { $recipePath = Yii::getAlias('@frontend/web/storage/thumbnail/' . $this->recipe_id . '.jpg'); if (!is_dir(dirname($recipePath))) { FileHelper::createDirectory(dirname($recipePath)); } $this->recipe->saveAs($recipePath); } return true; }
3. 前端可选兜底校验
如果需要确认事件触发时机,可以在change回调中增加文件存在性判断,确认选中有效文件后再提交:
$(function () { 'use strict'; $('#recipeThumbnail').change(ev => { // 校验是否选中了有效文件 if (ev.target.files && ev.target.files.length > 0) { $(ev.target).closest('form').trigger('submit'); } }) });
排查思路总结
- 前端事件类问题优先在回调入口打日志调试,通过打印选中的文件列表即可确认事件触发时机是否正常,不要盲目猜测事件执行顺序
- 后端属性访问报错优先定位报错行,确认调用属性的变量来源、赋值逻辑,顺着赋值链路排查即可快速定位空值来源
- 涉及文件上传的逻辑必须做两层校验:控制器层判断请求类型再处理上传,模型层做非空判断+文件规则验证,避免异常场景下直接报错
内容的提问来源于stack exchange,提问作者yai
相关产品推荐
相关产品推荐

