In this tutorial, we’ll implement the basic capability to upload file in Laravel 9 app based on VILT (Vue Inertia Laravel Tailwind) stack.
Laravel 9.x requires a minimum PHP version of 8.0. This tutorial assumes that you are using Linux, MacOS or WSL on Windows and Node.js, Composer and PHP 8 along with required modules i.e bcmatch, sqlite, mbstring, xml, zip, gd, mcrypt are properly installed. Also make sure that SQLite is installed as we’ll be using SQLite to keep things simple and quick; you may use MySQL or PostgreSQL as you like, however make sure the corresponding PHP module is installed too.
Using Composer, enter following command in terminal to install Laravel 9;
composer create-project laravel/laravel app 9.*
Now go inside the newly created folder app by entering following command;
cd app
Enter following command to add Jetstream package:
composer require laravel/Jetstream
Now install Jetstream assets by entering following command:
php artisan jetstream:install inertia
Assuming node.js and npm are installed, enter the following command to install required javascript dependencies and build client-side assets:
npm install && npm run build
Next, setup database and Laravel environment file. Here, I’m configuring SQLite database for simplicity purposes. While inside parent directory app, execute following command to create new SQLite database file;
touch database/database.sqlite
Find .env config file in the parent directory, open it and change DB_CONNECTION line as follows;
DB_CONNECTION = sqlite
You can remove DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD from .env when using SQLite database.
Now enter the following command in parent directory to create File model along with the migration:
php artisan make:model File -m
Open newly created migration file from database/migrations/ directory which is named something like …create_files_table.php and then insert the table fields as follows;
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('files', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('path');
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('files');
}
};
Enter following command in parent directory to migrate the above migration along with Jetstream authentication migrations;
php artisan migrate
Now open app/Models/File.php model file and insert protected fillable array as follows;
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class File extends Model
{
use HasFactory;
protected $fillable = [
'name', 'path'
];
}
Having done with the Model part of our app, we move onto Controller. Execute following command in parent directory to create FileController.php;
php artisan make:controller FileController
Go ahead and open this controller file which is located at app/Http/Controllers/FileController.php and amend it as follows in order to define basic functions;
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Redirect;
use Inertia\Inertia;
use App\Models\File;
use Illuminate\Support\Facades\Storage;
class FileController extends Controller
{
public function index()
{
$data = File::all()
->map(fn ($file) => [
'id' => $file->id,
'name' => $file->name,
'path' => asset('storage/' . $file->path),
]);
return Inertia::render('Files/Index', ['data' => $data]);
}
public function create()
{
return Inertia::render('Files/Create');
}
public function store()
{
Request::validate([
'file' => 'required|mimes:jpg,gif,png,pdf|max:4096'
]);
if(Request::file()){
$name = time() . '_' . Request::file('file')->getClientOriginalName();
$path = Request::file('file')->storeAs('uploads', $name, 'public');
File::create([
'name' => $name,
'path' => $path,
]);
}
return Redirect::route('files')->with('success', 'File uploaded.');
}
public function destroy(File $file)
{
if($file->path){
Storage::delete('public/' . $file->path);
}
$file->delete();
return Redirect::back()->with('success', 'File deleted.');
}
}
In order to define routes, open routes/web.php and put the following line at the upper side of it;
use App\Http\Controllers\FileController;
And then insert following routes into this routes/web.php just after dashboard route;
Route::get('files', [FileController::class, 'index'])
->name('files')
->middleware('auth');
Route::get('files/create', [FileController::class, 'create'])
->name('files.create')
->middleware('auth');
Route::post('files', [FileController::class, 'store'])
->name('files.store')
->middleware('auth');
Route::delete('files/{file}', [FileController::class, 'destroy'])
->name('files.destroy')
->middleware('auth');
We’re done with the Model and Controller parts of the app. Now let’s create Views using Vue.
First of all, for the purpose of displaying flash messages in views, let’s change share() function of app/Http/Middleware/HandleInertiaRequests.php as follows:
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'flash' => function () use ($request) {
return [
'success' => $request->session()->get('success'),
'error' => $request->session()->get('error'),
];
},
]);
}
Now, create directory named Files inside resources/js/Pages directory. While inside parent directory app, you may execute following command to make this directory;
mkdir resources/js/Pages/Files
Now create following files inside resources/js/Pages/Files directory:
- Index.vue
- Create.vue
You can execute following command inside parent directory to create these files in one go;
touch resources/js/Pages/Files/{Index,Create}.vue
Insert following code in resources/js/Pages/Files/Index.vue:
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue'
import DangerButton from '@/Components/DangerButton.vue'
import { router, Head, Link } from '@inertiajs/vue3'
const props = defineProps({
data: Object,
})
function destroy(id) {
router.visit(route('files.destroy', id), {method: 'delete'})
}
</script>
<template>
<AppLayout>
<Head title="Files" />
<h1 class="pl-6 py-2 text-3xl font-bold bg-gray-500 text-white">Files</h1>
<div v-if="$page.props.flash.success" class="bg-gray-300 font-bold text-blue-900 p-1">
{{ $page.props.flash.success }}
</div>
<div class="m-4">
<Link :href="route('files.create')" class="inline-flex items-center px-4 py-2 bg-green-800 border border-transparent rounded-lg font-semibold text-xs text-white uppercase tracking-widest hover:bg-green-700 active:bg-green-900 focus:outline-none focus:border-green-900 focus:ring ring-green-300 disabled:opacity-25 transition ease-in-out duration-150">
Upload a File
</Link>
</div>
<div class="bg-white rounded-md shadow overflow-x-auto">
<table class="w-full whitespace-nowrap">
<thead>
<tr class="bg-gray-500 text-white font-extrabold">
<th class="px-4 py-1 border">ID</th>
<th class="px-4 py-1 border">File</th>
<th class="px-4 py-1 border">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="item in data" :key="item.id">
<td class="px-4 py-1 border">{{item.id}}</td>
<td class="px-4 py-1 border"><a :href="item.path" target="_blank">{{item.name}}</a></td>
<td class="px-4 py-1 border">
<DangerButton @click="destroy(item.id)">
Delete
</DangerButton>
</td>
</tr>
</tbody>
</table>
</div>
</AppLayout>
</template>
Put following code in resources/js/Pages/Files/Create.vue:
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue'
import InputError from '@/Components/InputError.vue'
import InputLabel from '@/Components/InputLabel.vue'
import TextInput from '@/Components/TextInput.vue'
import PrimaryButton from '@/Components/PrimaryButton.vue'
import { Head, Link, useForm } from '@inertiajs/vue3'
const form = useForm({
file: null,
})
function submit() {
form.post(route('files.store'))
}
</script>
<template>
<AppLayout>
<Head title="Files" />
<h1 class="pl-6 py-2 text-3xl font-bold bg-gray-500 text-white">Files</h1>
<div class="min-h-screen flex flex-col sm:justify-start items-center pt-6 sm:pt-0 bg-gray-100">
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white shadow-md overflow-hidden sm:rounded-lg">
<form @submit.prevent="submit">
<div class="flex items-center justify-center mt-4">
<InputLabel for="file" value="Choose file:" class="mr-2 text-base"/>
<TextInput id="file" @input="form.file = $event.target.files[0]" type="file" class="border-2"/>
<InputError class="mt-2" :message="form.errors.file" />
</div>
<div class="flex items-center justify-center mt-4">
<PrimaryButton type="submit" class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Upload File
</PrimaryButton>
</div>
</form>
</div>
</div>
</AppLayout>
</template>
Finally, compile client side assets (JS and CSS files) by executing following command (make habit of running this command every time you make changes to client side views);
npm run build
Run following command in parent directory to create symbolic link from public/storage to storage/app/public. This is necessary to make files accessible from web:
php artisan storage:link
Run Laravel development server by executing:
php artisan serve
Enter the following URL in your browser to access index of Files (don’t forget to create a user login first):
localhost:8000/files
Upload some files, then take a look into storage/app/public directory where the uploaded files are saved.