没有不值得去解决的问题,也没有不值得去学习的技术!

Reusing File Upload Actions Across Multiple Validation Scenarios in Yii 2.0

In the "base settings icon" scenario, a file with the zip extension fails to upload, as expected

1. The file upload API previously only supported the “upload topic material” scenario. Now I need to add an “upload base icon” scenario, so I decided to reuse the file upload API, as shown in Figure 1.

The file upload API previously only supported the "upload topic material" scenario. Now I need to add an "upload base icon" scenario, so I decided to reuse the file upload API
Figure 1

2. Add a new request parameter, scenario: optional, the scenario, default: default; config_base_location_icon: the icon for base settings, default: default.

3. Edit \api\rests\asset\UploadAction.php. The scenario is set through constructor initialization configuration.

PHP
<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */
 
namespace api\rests\asset;
 
use Yii;
use api\models\Upload;
use api\services\AssetService;
use yii\base\Exception;
use yii\base\Model;
use yii\web\UnprocessableEntityHttpException;
use yii\web\UploadedFile;
use yii\web\ServerErrorHttpException;
 
/**
 * 上传资源:/assets/upload(asset/upload)
 *
 * 1、请求参数列表
 * (1)files[]:必填,上传的文件
 * (2)scenario:可选,场景,default:默认;config_base_location_icon:基地设置的图标,默认:default
 *
 * 2、输入数据验证规则
 * (1)必填:files[]
 * (2)文件
 * A、可接受上传的文件扩展名列表
 * B、可接受上传的 MIME 类型列表
 * C、上传文件所需最少多少 Byte 的大小
 * D、上传文件所需最多多少 Byte 的大小
 * E、给定属性最多能承载多少个文件
 * (3)范围(['default', 'config_base_location_icon']):scenario
 *
 * 3、操作数据
 * (1)上传文件至临时的资源目录
 *
 * UploadAction implements the API endpoint for creating a new model from the given data.
 *
 * For more details and usage information on UploadAction, see the [guide article on rest controllers](guide:rest-controllers).
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class UploadAction extends Action
{
    /**
     * @var string the scenario to be assigned to the new model before it is validated and saved.
     */
    public $scenario = Model::SCENARIO_DEFAULT;
    /**
     * @var string the name of the view action. This property is need to create the URL when the model is successfully created.
     */
    public $viewAction = 'view';

    /**
     * Uploads a new model.
     * @return array the model newly uploaded
     * @throws ServerErrorHttpException 如果创建目录失败,将抛出 500 HTTP 异常
     * @throws ServerErrorHttpException if there is any error when creating the model
     * @throws Exception if the directory could not be created (i.e. php error due to parallel changes)
     */
    public function run()
    {
        if ($this->checkAccess) {
            call_user_func($this->checkAccess, $this->id);
        }

        $request = Yii::$app->request;
        $scenario = $request->post('scenario', $this->scenario);

        /* 验证场景的范围(['default', 'config_base_location_icon']) */
        if (!in_array($scenario, [$this->scenario, Upload::SCENARIO_CONFIG_BASE_LOCATION_ICON])) {
            throw new UnprocessableEntityHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '226076'), ['scenario' => $scenario])), 226076);
        }

        /* @var $model Upload */
        $model = new Upload(['scenario' => $scenario]);

        // 将 UploadedFile 实例数组赋值给 Upload::files
        $model->files = UploadedFile::getInstances($model, 'files');

        if ($model->validate()) {
            $assetService = new AssetService();

            $result = $assetService->uploadTempAssets($model->files);

            return ['code' => 10000, 'message' => Yii::t('success', '126033'), 'data' => ['items' => $result]];

        } elseif ($model->hasErrors()) {
            $response = Yii::$app->getResponse();
            $response->setStatusCode(422, 'Data Validation Failed.');
            $firstError = '';
            foreach ($model->getFirstErrors() as $message) {
                $firstError = $message;
                break;
            }
            return ['code' => 226004, 'message' => Yii::t('error', Yii::t('error', Yii::t('error', '226004'), ['first_error' => $firstError]))];
        } elseif (!$model->hasErrors()) {
            throw new ServerErrorHttpException('Failed to upload the object for unknown reason.');
        }
    }
}


4. Edit \common\logics\Upload.php. By specifying the on property of a rule, a rule can be applied only in a specific scenario to differentiate validation rules across scenarios. For example: uploading topic material is the default scenario, while uploading a base icon is the base settings icon scenario.

PHP
<?php
 
namespace common\logics;
 
use Yii;
use yii\base\Model;
use yii\web\UploadedFile;
 
class Upload extends Model
{
    const SCENARIO_CONFIG_BASE_LOCATION_ICON = 'config_base_location_icon'; // 场景:基地设置的图标
 
    /**
     * @var UploadedFile
     */
    public $files;
 
    /**
     * {@inheritdoc}
     */
    public function scenarios()
    {
        $scenarios = parent::scenarios();
 
        return $scenarios;
    }
 
    public function rules()
    {
        return [
            [['files'], 'image', 'skipOnEmpty' => false, 'extensions' => Yii::$app->params['pcsApi']['asset']['upload']['scenario']['configBaseLocationIcon']['extensions'], 'mimeTypes' => Yii::$app->params['pcsApi']['asset']['upload']['scenario']['configBaseLocationIcon']['mimeTypes'], 'minSize' => Yii::$app->params['pcsApi']['asset']['upload']['scenario']['configBaseLocationIcon']['minSize'], 'maxSize' => Yii::$app->params['pcsApi']['asset']['upload']['scenario']['configBaseLocationIcon']['maxSize'], 'maxFiles' => Yii::$app->params['pcsApi']['asset']['upload']['scenario']['configBaseLocationIcon']['maxFiles'], 'minWidth' => Yii::$app->params['pcsApi']['asset']['upload']['scenario']['configBaseLocationIcon']['minWidth'], 'maxWidth' => Yii::$app->params['pcsApi']['asset']['upload']['scenario']['configBaseLocationIcon']['maxWidth'], 'minHeight' => Yii::$app->params['pcsApi']['asset']['upload']['scenario']['configBaseLocationIcon']['minHeight'], 'maxHeight' => Yii::$app->params['pcsApi']['asset']['upload']['scenario']['configBaseLocationIcon']['maxHeight'], 'on' => self::SCENARIO_CONFIG_BASE_LOCATION_ICON],
            [['files'], 'file', 'skipOnEmpty' => false, 'extensions' => Yii::$app->params['pcsApi']['asset']['upload']['extensions'], 'mimeTypes' => Yii::$app->params['pcsApi']['asset']['upload']['mimeTypes'], 'minSize' => Yii::$app->params['pcsApi']['asset']['upload']['minSize'], 'maxSize' => Yii::$app->params['pcsApi']['asset']['upload']['maxSize'], 'maxFiles' => Yii::$app->params['pcsApi']['asset']['upload']['maxFiles']],
        ];
    }
 
    public function formName()
    {
        return '';
    }
}

5. The specific parameter configuration for the validation rules is in the file \common\config\params-local.php.

PHP

    // 策划指挥系统接口
    'pcsApi' => [
        'asset' => [ // 资源
            'basePath' => 'E:/wwwroot/pcs-api/storage', // BASE PATH
            'tempDir' => '/tmp', // TEMP DIR
            'hostInfo' => 'http://127.0.0.1/pcs-api/storage', // HOME URL
            'baseUrl' => '', // BASE URL
            'upload' => [ // 上传
                'extensions' => 'ogg, pdf, xml, zip, gz, mp4, mp3, wav, webm, gif, jpeg, jpg, png, webp, svg, svgz, tiff, css, csv, txt, vcf, vcard, mov, qt, mkv, mk3d, mka, mks, wmv, flv', // 可接受上传的文件扩展名列表
                'mimeTypes' => 'application/ogg, application/pdf, application/xml, application/zip, application/gzip, audio/mp4, audio/mpeg, audio/ogg, audio/vnd.wave, audio/webm, image/gif, image/jpeg, image/png, image/webp, image/svg+xml, image/tiff, text/css, text/csv, text/plain, text/vcard, text/xml, video/mpeg, video/mp4, video/ogg, video/quicktime, video/webm, video/x-matroska, video/x-ms-wmv, video/x-flv', // 可接受上传的 MIME 类型列表
                'minSize' => null, // 上传文件所需最少多少 Byte 的大小
                'maxSize' => 1024 * 1024 * 1024, // 上传文件所需最多多少 Byte 的大小
                'maxFiles' => 10, // 给定属性最多能承载多少个文件
                'scenario' => [ // 场景
                    'configBaseLocationIcon' => [ // 基地设置的图标
                        'extensions' => 'jpeg, jpg, png', // 可接受上传的文件扩展名列表
                        'mimeTypes' => 'image/jpeg, image/png', // 可接受上传的 MIME 类型列表
                        'minSize' => null, // 上传文件所需最少多少 Byte 的大小
                        'maxSize' => 1024 * 1024 * 2, // 上传文件所需最多多少 Byte 的大小
                        'maxFiles' => 2, // 给定属性最多能承载多少个文件
                        'minWidth' => 50, // 图片的最小宽度
                        'maxWidth' => 200, // 图片的最大宽度
                        'minHeight' => 50, // 图片的最小高度
                        'maxHeight' => 200, // 图片的最大高度
                    ],
                ],
            ],
        ],
    ],

6. In the “upload topic material” scenario, a file with the zip extension uploads successfully, as expected, as shown in Figure 2.

In the "upload topic material" scenario, a file with the zip extension uploads successfully, as expected
Figure 2

7. In the “base settings icon” scenario, a file with the zip extension fails to upload, as expected, as shown in Figure 3.

In the "base settings icon" scenario, a file with the zip extension fails to upload, as expected
Figure 3

8. maxFiles: The maximum number of files a given attribute can hold. It defaults to 1, meaning only single-file uploads are allowed. If the value is greater than one, the input value must be an array containing at most maxFiles uploaded file elements. In the “base settings icon” scenario, the number of uploaded files should only be 1. Currently, maxFiles is set to 2, allowing up to 2 files to be uploaded. The reason for this is that when maxFiles is 1, the response fails, as shown in Figure 4.

maxFiles: The maximum number of files a given attribute can hold. It defaults to 1, meaning only single-file uploads are allowed. If the value is greater than one, the input value must be an array containing at most maxFiles uploaded file elements. In the "base settings icon" scenario, the number of uploaded files should only be 1. Currently, maxFiles is set to 2, allowing up to 2 files to be uploaded. The reason for this is that when maxFiles is 1, the response fails
Figure 4

9. In different scenarios, some scenarios allow only 1 file, while others allow multiple files. Currently, maxFiles is set to 2, allowing up to 2 files to be uploaded, as shown in Figure 5.

In different scenarios, some scenarios allow only 1 file, while others allow multiple files. Currently, maxFiles is set to 2, allowing up to 2 files to be uploaded
Figure 5

10. Edit \api\rests\asset\UploadAction.php to check the scenario. If it is config_base_location_icon, upload a single file (i.e., the first file). Currently, maxFiles is set to 2, allowing up to 2 files to be uploaded (both files will be validated, but only the first file will be uploaded), as expected. Actually, the best approach here would be to respond with a failure and prompt that only one file can be uploaded, but that would require implementing another upload API, which is redundant. Therefore, I ultimately decided to reuse the existing one, as shown in Figure 6.

Edit \api\rests\asset\UploadAction.php to check the scenario. If it is config_base_location_icon, upload a single file (i.e., the first file). Currently, maxFiles is set to 2, allowing up to 2 files to be uploaded (both files will be validated, but only the first file will be uploaded), as expected. Actually, the best approach here would be to respond with a failure and prompt that only one file can be uploaded, but that would require implementing another upload API, which is redundant. Therefore, I ultimately decided to reuse the existing one
Figure 6
PHP
<?php
/**
 * @link http://www.yiiframework.com/
 * @copyright Copyright (c) 2008 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */
 
namespace api\rests\asset;
 
use Yii;
use api\models\Upload;
use api\services\AssetService;
use yii\base\Exception;
use yii\base\Model;
use yii\web\UnprocessableEntityHttpException;
use yii\web\UploadedFile;
use yii\web\ServerErrorHttpException;
 
/**
 * 上传资源:/assets/upload(asset/upload)
 *
 * 1、请求参数列表
 * (1)files[]:必填,上传的文件
 * (2)scenario:可选,场景,default:默认;config_base_location_icon:基地设置的图标,默认:default
 *
 * 2、输入数据验证规则
 * (1)必填:files[]
 * (2)文件
 * A、可接受上传的文件扩展名列表
 * B、可接受上传的 MIME 类型列表
 * C、上传文件所需最少多少 Byte 的大小
 * D、上传文件所需最多多少 Byte 的大小
 * E、给定属性最多能承载多少个文件
 * (3)范围(['default', 'config_base_location_icon']):scenario
 *
 * 3、操作数据
 * (1)上传文件至临时的资源目录
 *
 * UploadAction implements the API endpoint for creating a new model from the given data.
 *
 * For more details and usage information on UploadAction, see the [guide article on rest controllers](guide:rest-controllers).
 *
 * @author Qiang Wang <shuijingwanwq@163.com>
 * @since 1.0
 */
class UploadAction extends Action
{
    /**
     * @var string the scenario to be assigned to the new model before it is validated and saved.
     */
    public $scenario = Model::SCENARIO_DEFAULT;
    /**
     * @var string the name of the view action. This property is need to create the URL when the model is successfully created.
     */
    public $viewAction = 'view';

    /**
     * Uploads a new model.
     * @return array the model newly uploaded
     * @throws ServerErrorHttpException 如果创建目录失败,将抛出 500 HTTP 异常
     * @throws ServerErrorHttpException if there is any error when creating the model
     * @throws Exception if the directory could not be created (i.e. php error due to parallel changes)
     */
    public function run()
    {
        if ($this->checkAccess) {
            call_user_func($this->checkAccess, $this->id);
        }

        $request = Yii::$app->request;
        $scenario = $request->post('scenario', $this->scenario);

        /* 验证场景的范围(['default', 'config_base_location_icon']) */
        if (!in_array($scenario, [$this->scenario, Upload::SCENARIO_CONFIG_BASE_LOCATION_ICON])) {
            throw new UnprocessableEntityHttpException(Yii::t('error', Yii::t('error', Yii::t('error', '226076'), ['scenario' => $scenario])), 226076);
        }

        /* @var $model Upload */
        $model = new Upload(['scenario' => $scenario]);

        // 将 UploadedFile 实例数组赋值给 Upload::files
        $model->files = UploadedFile::getInstances($model, 'files');

        if ($model->validate()) {
            $assetService = new AssetService();

            /* 判断场景,如果为:config_base_location_icon,则上传单个文件(即第 1 个文件) */
            if ($scenario == Upload::SCENARIO_CONFIG_BASE_LOCATION_ICON) {
                // 将 UploadedFile 实例赋值给 Upload::files
                $model->files = [$model->files[0]];
            }

            $result = $assetService->uploadTempAssets($model->files);

            return ['code' => 10000, 'message' => Yii::t('success', '126033'), 'data' => ['items' => $result]];

        } elseif ($model->hasErrors()) {
            $response = Yii::$app->getResponse();
            $response->setStatusCode(422, 'Data Validation Failed.');
            $firstError = '';
            foreach ($model->getFirstErrors() as $message) {
                $firstError = $message;
                break;
            }
            return ['code' => 226004, 'message' => Yii::t('error', Yii::t('error', Yii::t('error', '226004'), ['first_error' => $firstError]))];
        } elseif (!$model->hasErrors()) {
            throw new ServerErrorHttpException('Failed to upload the object for unknown reason.');
        }
    }
}


关于作者 · 持续学习与技术实践

我是一名拥有 15 年以上开发经验的技术从业者,自 2013 年起持续运营个人技术博客,记录工作与个人项目中遇到的真实问题,以及解决问题过程中的探索、实践和思考。

除了技术写作,我也在持续开发和维护自己的独立项目,探索新技术和新工具在实际场景中的应用。我始终相信,没有不值得去解决的问题,也没有不值得去学习的技术。

如果这篇文章对你有所帮助,欢迎浏览博客中的其他文章,也欢迎交流技术经验与想法。

关于我  ·  GitHub  ·  邮件联系