How to correctly use audio file type validation in Laravel?

ghz 8months ago ⋅ 67 views

I'm pretty new to Laravel and am trying to learn the framework. I'm currently making a simple posts/leaderboard page where users should be able to upload an audio file to it. Everything works fine except for the audio upload.I'm not sure where the error is. I'm also using vite to use React code for my components.

For validation: I've tried using mimes such as audio/mpeg, mp3, mpeg, aac, wav. None of them worked when i uploaded an mp3. Neither does it save the file. Eventually I just tried making audio nullable, and adding no further validation. At that point it does redirect me to the posts page where I can see the data. But the audio is not saved anywhere. It is essentially lost.

I'll add all the necessary files below. Where am i doing something wrong?

create_inzending_table.php:

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('inzendings', function (Blueprint $table) {
            $table->id();
            $table->string('title');
            $table->string('category');
            $table->timestamp('posted_at')->nullable();
            $table->string('upvotes')->nullable();
            $table->string('description');
            $table->string('posted_by');
            $table->string('audio');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('inzendingen');
    }
};

Requests/StoreInzending.php:

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StoreInzending extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array<string, mixed>
     */
    public function rules()
    {
        return [
            'audio' =>'nullable',
            'title' => ['required', 'string', 'max:255'],
            'description' => ['required', 'string', 'max:255'],
            'posted_by' => ['required', 'string', 'max:255'],
            'posted_at' => ['nullable', 'date'],
            'category' => ['required', 'string', 'max:255'],
            'upvotes' => ['integer'],

        ];
    }
}

Models/Inzending.php:

<?php

namespace App\Models;

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

class Inzending extends Model
{
    protected $fillable = [
        'audio',
        'title',
        'description',
        'imageUrl',
        'posted_at',
        'posted_by',
        'category',
        'upvotes'
    ];
    
    use HasFactory;
}

routes/web.php:

Route::get('inzending', [InzendingController::class,'index'])->name('inzending.index');
Route::get('inzending/create', [InzendingController::class,'create'])->name('inzending.create');
Route::post('inzending', [InzendingController::class,'store'])->name('inzending.store');

CreateInzendingForm.jsx:

import React, { useState } from 'react';
import { Link, useForm, usePage } from '@inertiajs/react';
import InputError from '@/Components/InputError';
import InputLabel from '@/Components/InputLabel';
import PrimaryButton from '@/Components/PrimaryButton';
import TextInput from '@/Components/TextInput';
import SelectInput from '@/Components/SelectInput';
import FileInput from './FileInput';

export default function CreateInzendingForm({ mustVerifyEmail, status, className }) {
    const user = usePage().props.auth.user;
    const inzending = usePage()

    const { data, setData, patch, post, errors, processing, recentlySuccessful } = useForm({
        name: user.name,
        email: user.email,
        education: user.education,  
        cohort: user.cohort,
        title: inzending.title,
        description: inzending.description,
        posted_at: inzending.posted_at,
        category: inzending.category,
        posted_by: user.name,
        audio: inzending.audio,
    });

    const submit = (e) => {
        e.preventDefault();

        console.log(data);
        post(route('inzending.store'));
    }


    return (
        <section>
            <header>
                <h2 className="text-lg font-medium text-gray-900 dark:text-gray-100">Create Inzending</h2>
            </header>

            <form onSubmit={submit} encType='multipart/form-data' className="mt-6 space-y-6">
                <div>
                    <InputLabel htmlFor="title" value="Title" />

                    <TextInput
                        id="title"
                        className="mt-1 block w-full"
                        onChange={(e) => setData('title', e.target.value)}
                        required
                        isFocused
                        autoComplete="title"
                    />
                    <InputError className="mt-2" message={errors.title} />
                </div>
                <div>
                    <InputLabel htmlFor="description" value="Description" />

                    <TextInput
                        id="description"
                        className="mt-1 block w-full"
                        onChange={(e) => setData('description', e.target.value)}
                        required
                        isFocused
                        autoComplete="description"
                    />
                    <InputError className="mt-2" message={errors.description} />
                </div>
                <div>
                    <InputLabel htmlFor="category" value="Category" />

                    <SelectInput
                        id="category"
                        className="mt-1 block w-full"
                        onChange={(e) => setData('category', e.target.value)}
                        required
                        isFocused
                        autoComplete="category"
                    />
                    <InputError className="mt-2" message={errors.category} />
                </div>
                <div>
                    <InputLabel htmlFor="posted_by" value="Posted By" />

                    <TextInput
                        id="posted_by"
                        className="mt-1 block w-full"
                        value={data.name}
                        onChange={(e) => setData('posted_by', e.target.value)}
                        required
                        isFocused
                        autoComplete="posted_by"
                    />
                    <InputError className="mt-2" message={errors.posted_by} />
                </div>
                    <div>
                    <InputLabel htmlFor="audio" value="Audio" />

                    <FileInput
                        id="audio"
                        name="audio"
                        className="mt-1 block w-full"
                        onChange={(e) => setData('audio', e.target.value)}
                        required
                        isFocused
                        autoComplete="audio"
                    />
                    <InputError className="mt-2" message={errors.audio} />

                <div className="flex items-center gap-4">
                    <PrimaryButton disabled={processing}>Save</PrimaryButton>
                </div>
                </div>
            </form>
        </section>
    );
}

FileInput.jsx:

import { forwardRef, useEffect, useRef } from 'react';

export default forwardRef(function FileInput({ type = 'file', name, className = '', isFocused = false, ...props }, ref) {
    const input = ref ? ref : useRef();

    useEffect(() => {
        if (isFocused) {
            input.current.focus();
        }
    }, []);

    return (
        <div className="flex flex-col items-start">
            <input
                {...props}
                name={name}
                type={type}
                className={
                    'border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm ' +
                    className
                }
                ref={input}
            />
        </div>
    );
});

config/filesystems.php:

'links' => [
        public_path('storage') => storage_path('app/public'),
        public_path('images') => storage_path('app/public/images'),
        public_path('videos') => storage_path('app/public/videos'),
        public_path('audio') => storage_path('app/public/audio'),
    ],

Any help is greatly appreciated!

Answers

It seems like you've set up the file upload functionality correctly, but there might be a small issue in your React component handling the file input. Let's adjust the FileInput.jsx component to ensure it handles file uploads properly:

import { forwardRef, useEffect, useRef } from 'react';

export default forwardRef(function FileInput({ name, className = '', isFocused = false, ...props }, ref) {
    const inputRef = useRef();

    useEffect(() => {
        if (isFocused) {
            inputRef.current.focus();
        }
    }, [isFocused]);

    return (
        <div className="flex flex-col items-start">
            <input
                {...props}
                name={name}
                type="file"
                className={
                    'border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm ' +
                    className
                }
                ref={ref || inputRef} // Use ref passed as prop or the local ref
            />
        </div>
    );
});

In your CreateInzendingForm.jsx, make sure you're setting up the FileInput component correctly:

import FileInput from './FileInput';

// Inside the form
<FileInput
    name="audio"
    className="mt-1 block w-full"
    onChange={(e) => setData('audio', e.target.files[0])} // Use e.target.files to get the file object
    required
    isFocused
    autoComplete="audio"
/>

By making these adjustments, the file input should properly handle file uploads, and you should be able to save the audio file along with other form data when submitting the form. Make sure you're storing the file path or file name in your database audio column, depending on how you're handling file storage in your Laravel application.