уже несколько дней у меня возникают проблемы с загрузкой изображения с помощью 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 запросите, все работает нормально
Заранее спасибо за ваши усилия!
Обработка данных вашего перехватчика запросов полностью избыточна. 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 не работает с данными формы
Я все еще думаю, что эта проблема вызвана серверной частью, я попробовал с почтальоном, и результат тот же. ``` {"Поле имени обязательно."}```
Это касается обоих ваших запросов PUT и POST? Я не знаю Laravel, но, возможно, он так же рискованно обрабатывает PUT-данные PHP. Возможно, вам придется использовать POST для обоих
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 — нет.
Метод 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);
}
}
}
Спасибо, Фил!
Это была моя попытка принудительно запросить Content-Type, но это не помогло. Основная цель этого перехватчика — отправить данные носителя по запросу.