You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用multipart/form-data时如何正确返回必填属性验证错误

Laravel商品添加必填属性校验异常问题解决方案

问题描述

在Laravel应用的商品添加功能中,需要校验用户提交所有is_force为true的必填属性,但遇到以下异常情况:

  • 当请求头为multipart/form-data且未提交全部必填属性时,接口仅返回数字“1”,商品未创建;
  • 使用application/json头并注释图片、图库验证规则时,可正常返回正确格式的验证错误。

相关代码

Controller代码

public function storeForSeller(AddSellerProductRequest $request): \Illuminate\Http\JsonResponse
{
    $uploaded = UploaderHelper::upload($request->file('image'), config('path.product'));
    $category = $this->categoryRepository->findById($request->category_id);

    $result = $this->productPropertyRepository->checkPropertyValidation($request->properties(null));

    if(!$result){
        return ControllerHelper::errorJsonResponse(trans('admin.invalid_property_items'));
    }

    $product = $this->productRepository->create($request->product([
        'image' => $uploaded ,
        'expire_date' => Carbon::now()->addDays($category->listing_duration),
        'user_id' => auth('sanctum')->id()
    ]));

    if (empty($product)) {
       UploaderHelper::delete($uploaded);
       return ControllerHelper::createError();
    }

    $this->productTagRepository->replaceTags($request->tags(), $product->id, auth('sanctum')->id());
    $this->productPropertyRepository->createAll($request->properties($product->id));

    $galleries = collect($request->galleries())->map(function ($item) use ($product){
        return [
            'title' => $product->title,
            'product_id' => $product->id,
            'url' => UploaderHelper::upload($item, config('path.product')),
            'updater' => auth('sanctum')->id()
        ];
    });

    $result =  $this->productGalleryRepository->insert($galleries->toArray());

    if(!$result){
       UploaderHelper::delete($uploaded);
       $this->productRepository->deleteById($product->id);
       return ControllerHelper::errorJsonResponse(trans('admin.error_image_upload'));
    }

    return ControllerHelper::successJsonData($product);
}

Request代码

public function rules(): array
{
    return [
        'id' => ['nullable', 'numeric', Rule::exists('products', 'id')->where('status', ListingStatus::PENDING->value), new Ownership('product', false)],
        'category_id' => ['required', Rule::exists('categories', 'id')->where('is_hidden', false)],
        'product_status' => ['required', 'in:' . implode(',', ProductStatus::toArray()),],
        'title' => ['required', 'max:500'],
        'price' => ['required', 'numeric', 'min:0', 'max:1000000'],
        'discount' => ['nullable', 'numeric', 'min:0', 'max:100'],
        'description' => ['nullable', 'string', 'min:0', 'max:2000'],
        'city_id' => 'nullable|integer|exists:' . Region::class . ',id',
        'state_id' => 'nullable|integer|exists:' . Region::class . ',id',
        'country_id' => 'nullable|integer|exists:' . Region::class . ',id',
        'tags' => ['required', 'array', 'max:10'],
        'tags.*' => ['required', 'string', 'max:50'],
        'image' => [Rule::requiredIf(empty(request('id'))), 'mimes:jpg,png,jpe,jpeg', 'max:500', 'image'],
        'galleries' => ['array', Rule::requiredIf(empty(request('id'))), 'max:5'],
        'galleries.*' => ['required', 'mimes:jpg,png,jpe,jpeg', 'max:500', 'image'],
        'properties' => ['array', Rule::requiredIf(empty(request('id'))), new ForceProperty()],
        'properties.*.property_id' => ['required',Rule::exists('properties', 'id')],
        'properties.*.property_item_id' => ['required',Rule::exists('property_items', 'id')],
    ];
}

public function product($merge, $except = null): array
{
    return $this->safe()->merge([
        'updater' => auth('sanctum')->id()
    ])->merge($merge)->except('id', 'tags', 'galleries', 'properties', $except);
}

public function tags(): array
{
    return $this->get('tags');
}

public function galleries(): array
{
    return $this->file('galleries');
}

public function properties($product_id): array
{
    return collect($this->get('properties'))->map(function ($value) use ($product_id) {
        $value['product_id'] = $product_id;
        $value['updater'] = auth()->id();
        return $value;
    })->toArray();
}

ForceProperty验证规则代码

class ForceProperty implements ValidationRule
{
    /**
     * Run the validation rule.
     *
     * @param  \Closure(string): PotentiallyTranslatedString  $fail
     */
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        $forceProperties = CategoryProperty::query()->where('is_force', true)->pluck('id','title')->toArray();
        $propertyIds = collect($value)->pluck('property_id')->toArray();
        $missingForceProperties = array_diff($forceProperties, $propertyIds);

        if (!empty($missingForceProperties)) {
            $missingForcePropertyTitles = array_flip($missingForceProperties);
            $fail("Please provide all required properties with IDs: " . implode(', ', $missingForcePropertyTitles));
        }
    }
}

解决方案

1. 调整文件上传顺序,避免验证前执行资源操作

当前Controller在Request验证通过前就执行了图片上传,若验证失败,不仅浪费存储资源,还可能因文件操作抛出未捕获异常导致响应格式异常。将文件上传逻辑移至Request验证通过之后:

修改Controller代码:

public function storeForSeller(AddSellerProductRequest $request): \Illuminate\Http\JsonResponse
{
    // Request已自动验证,此处开始执行业务逻辑
    $category = $this->categoryRepository->findById($request->category_id);

    $result = $this->productPropertyRepository->checkPropertyValidation($request->properties(null));
    if(!$result){
        return ControllerHelper::errorJsonResponse(trans('admin.invalid_property_items'));
    }

    // 验证通过后再上传图片
    $uploaded = UploaderHelper::upload($request->file('image'), config('path.product'));

    $product = $this->productRepository->create($request->product([
        'image' => $uploaded ,
        'expire_date' => Carbon::now()->addDays($category->listing_duration),
        'user_id' => auth('sanctum')->id()
    ]));

    // 后续逻辑保持不变...
}

2. 修复ForceProperty规则的业务逻辑错误

当前规则校验的是所有分类下的必填属性,实际业务中必填属性应与当前商品的分类绑定。修改ForceProperty类,关联当前请求的category_id:

class ForceProperty implements ValidationRule
{
    public function validate(string $attribute, mixed $value, Closure $fail): void
    {
        // 获取当前请求的分类ID
        $categoryId = request()->input('category_id');
        if (!$categoryId) {
            $fail('分类ID不能为空');
            return;
        }

        // 仅获取当前分类下的必填属性
        $forceProperties = CategoryProperty::query()
            ->where('category_id', $categoryId)
            ->where('is_force', true)
            ->pluck('id','title')
            ->toArray();

        $propertyIds = collect($value)->pluck('property_id')->toArray();
        $missingForceProperties = array_diff(array_keys($forceProperties), $propertyIds);

        if (!empty($missingForceProperties)) {
            // 根据缺失的ID获取对应标题
            $missingTitles = collect($forceProperties)->only($missingForceProperties)->values()->toArray();
            $fail("请填写所有必填属性:" . implode(', ', $missingTitles));
        }
    }
}

3. 确保Request验证失败时返回JSON响应

在AddSellerProductRequest中重写failedValidation方法,强制验证失败时返回JSON格式响应,避免multipart/form-data请求下的响应异常:

use Illuminate\Contracts\Validation\Validator;
use Illuminate\Http\Exceptions\HttpResponseException;

class AddSellerProductRequest extends FormRequest
{
    // 其他方法保持不变...

    protected function failedValidation(Validator $validator)
    {
        throw new HttpResponseException(
            response()->json([
                'status' => false,
                'message' => '验证失败',
                'errors' => $validator->errors()
            ], 422)
        );
    }
}

4. 移除冗余的属性校验逻辑

Controller中$this->productPropertyRepository->checkPropertyValidation(...)的校验与Request中的ForceProperty规则重复,可移除该冗余逻辑,统一由Request验证处理,减少代码重复和异常点。

总结

通过调整文件上传顺序、修复必填属性的分类关联逻辑、强制验证失败返回JSON响应,即可解决multipart/form-data请求下仅返回数字“1”的异常问题,确保所有请求头下都能返回一致的验证错误格式。

内容的提问来源于stack exchange,提问作者Reza Heydari

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 13:07:32