Я создал API Laravel CRUD и не могу загрузить изображение

уже несколько дней у меня возникают проблемы с загрузкой изображения с помощью Laravel API. Я отправлял запрос через модифицированные аксиомы React. Код выглядит следующим образом.

axios-client.js

import axios from "axios";

const axiosClient = axios.create({
    baseURL : `${import.meta.env.VITE_API_BASE_URL}/api`
});

axiosClient.interceptors.request.use((config) => {
    const token = localStorage.getItem('ACCESS_TOKEN');
    config.headers.Authorization = `Bearer ${token}`;

    if (config.data instanceof FormData) {
        console.info('Form Data accepted')
        config.headers['Content-Type'] = 'multipart/form-data';
    }

    return config;
});

axiosClient.interceptors.response.use((response) => {
    return response;
}, (error) => {
    try {
        const { response } = error;
        if (response.status === 401) {
            localStorage.removeItem('ACCESS_TOKEN');
        }
    } catch (e) {
        console.info(e);
    }
    throw error;
});

export default axiosClient;

Компонент React

BrandForm.jsx

import React, { useState, useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import axiosClient from '../../axios-client';

function BrandForm() {
  const {id} = useParams();
  const navigate = useNavigate();
  const [loading,setLoading] = useState(false);
  const [errors, setErrors] = useState(null);
  const [brand,setBrand] = useState({
    id: null,
    name: '',
    logo: null
  });
  const config = {headers: {'Content-Type': 'multipart/form-data'}}

  if (id){
    useEffect(()=>{
        setLoading(true);
        axiosClient.get(`/car-make/${id}`)
            .then(({data})=>{
              console.info(data)
                setBrand(data);
                setLoading(false)
            })
            .catch((e)=>{
                console.info(e)
                setLoading(false);
            })
    },[]);
  }
  const onSubmit = (e)=>{
    e.preventDefault();
    setErrors(null);
    
    if (brand.id){
        axiosClient.put(`/car-make/${brand.id}`, brand, config)
        .then(({data})=>{
            //TODO show notification
            console.info('response update', data)
            navigate(path);
        })
        .catch( (err) => {
          console.info(err)
            const response = err.response;
            if (response && response.status ===422){
              setErrors(response.data.errors);
            }
          });
    } 
    else {
      
        axiosClient.post(`/car-make`, brand, config)
        .then(()=>{
            //TODO show notification
            console.info('response create', data)
            navigate(path);
        })
        .catch( (err) => {
            const response = err.response;
            if (response && response.status ===422){
              setErrors(response.data.errors);
            }
          });

    }

  }

  return (
    <div>
      {brand?.id ? <h1>Edit Brand: {brand.name}</h1> : <h1>New Brand</h1>}
      <div className = "card animated fadeInDown">
        {loading && <div className = "text-center">Loading...</div>}
        {errors && (
          <div className = "alert">
            {Object.keys(errors).map((key) => (
              <p key = {key}>{errors[key][0]}</p>
            ))}
          </div>
        )}
        {!loading && (
          <form onSubmit = {onSubmit}>
            <input
              value = {brand?.name}
              onChange = {(e) => setBrand({ ...brand, name: e.target.value })}
              type = "text"
              placeholder = "Name"
            />
            <input
              onChange = {(e) => setBrand({ ...brand, logo: e.target.files[0] })}
              type = "file"
              placeholder = "Logo"
            />
            <button className = "btn" style = {{ marginTop: '20px' }}>Save</button>
          </form>
        )}
      </div>
    </div>
  );
}

export default BrandForm;

Ларавел

API.php

Route::middleware('auth:sanctum')->group(function(){

    Route::put('/transfer/execute/{id}', [TransferController::class,'execute']);
    Route::put('/transfer/cancel/{id}', [TransferController::class,'cancel']);
    Route::apiResource('/transfer', TransferController::class);
    Route::apiResource('/vehicle', VehicleController::class);
  
    Route::apiResource('/car-make', CarMakeController::class);
    Route::apiResource('/car-model', CarModelController::class);

    Route::get('/companies/partner',[CompanyController::class,'indexPartner']);
    Route::get('/companies/fleet',[CompanyController::class,'indexFlota']);
    Route::apiResource('/companies', CompanyController::class);
    
    
    Route::get('/user', function (Request $request) {
        return $request->user();
    });
    Route::post('/logout',  [AuthController::class,'logout']); 
    Route::apiResource('/users', UserController::class);

   
});


Route::post('/signup',  [AuthController::class,'signup']);
Route::post('/login',   [AuthController::class,'login']);

Модель

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class CarMake extends Model
{
    use HasFactory;
    protected $fillable = ['name', 'logo'];
    protected $primarykey = "id";
    
  
    public function carModels()
    {
        return $this->hasMany(CarModel::class,'make');
    }
}

Контроллер

.
.
.
public function update(UpdateCarMakeRequest $request, CarMake $carMake)
    {
        Log::info('Update method called');
        Log::info('Update Brand Request - Raw Request Data:', $request->all());
        
        $data = $request->validated();
    
        if ($request->hasFile('logo')) {
            $file = $request->file('logo');
            $fileName = time() . '_' . $file->getClientOriginalName();
            $filePath = $file->storeAs('logo', $fileName, 'public');
            $data['logo'] = $filePath;
        } else {
            unset($data['logo']);
        }
    
        Log::info('Update Brand Request - Validated Data:', $data);
    
        $carMake->update($data);
    
        return new CarMakeResource($carMake);
    }
.
.
.

Запрос на обновление

class UpdateCarMakeRequest extends FormRequest
{
    public function authorize()
    {
        return true; // Adjust as needed
    }

    public function rules()
    {
        return [
            'name' => 'required|string|max:255',
            'logo' => 'nullable|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
        ];
    }

    protected function failedValidation(Validator $validator)
    {
        Log::error('Validation errors:', $validator->errors()->toArray());
        throw new HttpResponseException(response()->json([
            'errors' => $validator->errors()
        ], 422));
    }
}

Ресурс

class CarMakeResource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @return array<string, mixed>
     */
    public function toArray(Request $request): array
    {
        return [
            'id'=> $this->id,
            'name' => $this->name,
            'logo'=> $this->logo,
        ];

    }
}

Файл журнала выглядит так после попытки редактирования

[2024-07-18 05:29:50] local.ERROR: Validation errors: {"name":["The name field is required."]} 
[2024-07-18 05:44:36] local.ERROR: Validation errors: {"name":["The name field is required."]} 

Примечание: когда я пытаюсь обновить CarMake без изображения и без параметра конфигурации в axios запросите, все работает нормально

Заранее спасибо за ваши усилия!

Это была моя попытка принудительно запросить Content-Type, но это не помогло. Основная цель этого перехватчика — отправить данные носителя по запросу.

i.rijad 18.07.2024 07:57
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Навигация по приложениям React: Исчерпывающее руководство по React Router
Навигация по приложениям React: Исчерпывающее руководство по React Router
React Router стала незаменимой библиотекой для создания одностраничных приложений с навигацией в React. В этой статье блога мы подробно рассмотрим...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
1
1
71
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

Обработка данных вашего перехватчика запросов полностью избыточна. Axios уже обрабатывает FormData полезные нагрузки и делает это правильно. У вас отсутствует токен границы mime.

Удалите это и позвольте Axios и вашему браузеру установить соответствующий заголовок типа контента.

if (config.data instanceof FormData) {
  console.info('Form Data accepted')
  config.headers['Content-Type'] = 'multipart/form-data';
}

Вы также не можете вставлять файлы в документ JSON. Создайте экземпляр FormData для полезной нагрузки вашего запроса, заполните данные и отправьте их.

const onSubmit = async (e) => {
  e.preventDefault();
  setErrors(null);

  const fd = new FormData();
  Object.entries(brand).forEach(([key, value]) => {
    if (value !== null) fd.append(key, value);
  });

  try {
    if (brand.id) {
      await axiosClient.put(`/car-make/${encodeURIComponent(brand.id)}`, fd);
    } else {
      await axiosClient.post(`/car-make`, fd);
    }
    
    // use your dev-tools to inspect responses, not console.info()

    navigate(path);
  } catch (err) {
    console.warn(err);
    const response = err.response;
    if (response && response.status === 422) {
      setErrors(response.data.errors);
    }
  }
};

Чтобы Laravel мог обрабатывать запросы multipart/form-data PUT, вам может потребоваться использовать axios.post() и добавить _method=PUT

await axios.post(`/car-make/${encodeURIComponent(brand.id)}`, fd, {
  params: {
    _method: 'PUT',
  },
});

См. Запрос PATCH и PUT не работает с данными формы

Я все еще думаю, что эта проблема вызвана серверной частью, я попробовал с почтальоном, и результат тот же. ``` {"Поле имени обязательно."}```

i.rijad 18.07.2024 08:24

Это касается обоих ваших запросов PUT и POST? Я не знаю Laravel, но, возможно, он так же рискованно обрабатывает PUT-данные PHP. Возможно, вам придется использовать POST для обоих

Phil 18.07.2024 08:27

POST-запрос работает для интерфейса кода await axiosClient.post(/car-make, fd) и внутреннего контроллера {$data = $request->validated(); if ($request->hasFile('logo')) { $file = $request->file('logo'); $fileName = time() . '_' . $file->getClientOriginalName(); $filePath = $file->storeAs('logos', $fileName, 'public'); $data['logo']=$filePath; } else {$data['logo'] = null;} $carMake= CarMake::create([ 'name' => $data['name'], 'logo' => $data['logo'], ]); }, но PUT — нет.

i.rijad 18.07.2024 11:18

Метод validated() используется для получения проверенных входных данных.

В вашем контроллере вместо метода validated() используйте $request->validate()

Итак, ваш контроллер должен выглядеть так

public function update(UpdateCarMakeRequest $request, CarMake $carMake)
    {
        Log::info('Update method called');
        Log::info('Update Brand Request - Raw Request Data:', $request->all());
        
        $data = $request->validate();
    
        if ($request->hasFile('logo')) {
            $file = $request->file('logo');
            $fileName = time() . '_' . $file->getClientOriginalName();
            $filePath = $file->storeAs('logo', $fileName, 'public');
            $data['logo'] = $filePath;
        } else {
            unset($data['logo']);
        }
    
        Log::info('Update Brand Request - Validated Data:', $data);
    
        $carMake->update($data);
    
        return new CarMakeResource($carMake);
    }

Вы можете обратиться к документации для получения дополнительной информации.

Основываясь на помощи @Phil, я наконец нашел решение, которое заключается в том, что у вас есть интерфейс для использования метода POST и определения поля с именем _method и значением GET .. вот рабочий код:

const onSubmit = async (e)=>{
e.preventDefault();
setErrors(null);

const fd = new FormData();
fd.append('_method','PUT');
Object.entries(brand).forEach(([key, value]) => {
  if (value !== null) fd.append(key, value);
});


try {
  if (brand.id) {
    await axiosClient.post(`/car-make/${encodeURIComponent(brand.id)}`, fd)
    .then(({data})=>{
      //TODO show notification
      navigate(path);
  })
  .catch( (err) => {
      const response = err.response;
      if (response && response.status ===422){
        setErrors(response.data.errors);
      }
    });
  } else {
    await axiosClient.post(`/car-make`, fd)
    .then((data)=>{
      //TODO show notification
      navigate(path);
  })
  .catch( (err) => {
      const response = err.response;
      if (response && response.status ===422){
        setErrors(response.data.errors);
      }
    });
  }
  navigate(path);
} catch (err) {
  console.warn(err);
  const response = err.response;
  if (response && response.status === 422) {
    setErrors(response.data.errors);
  }
}

}

Спасибо, Фил!

Другие вопросы по теме