In this example I use the Laravel and Vue frameworks to implement a SPA tool to upload images and display a list of images that have been uploaded. The application also demonstrates image resizing with the amazing PHP ImageMagick library. So there are a few prerequisits to get started however I won't cover that in this tutorial. I've added some links to get you through the prerequisits:
- Install imagemagick and the pecl imagemagick package. Configure the extension in php.
- Install and set-up Laravel to use the Vue 2 Options API with Webpack compiler.
- Create required Controllers, Blade Views, routes(web.php, api.php, routes.js) and Vue components
Assuming you have an initial setup you will need to generate an API controller to handle requests made from the Vue components. You can generate the controller with php artisan:
php artisan make:controller Api/ImageController
Next, we need to specify a directory to store these images. For this tutorial we will upload our images to /public/images. So notice in the ImageController the method public_path('images/'.$imageName) is used to specify this location.
/app/Http/Controllers/Api/ImageController.php
We need two methods to handle the functionality for this application. An index method for GET requests to retrieve a list of all images. And an upload method to handle a POST request to upload an image.
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Imagick;
class ImageController extends Controller
{
/**
* index
*/
public function index()
{
$fileNames = [];
$imagesPath = public_path('images/');
$files = File::files($imagesPath);
foreach ($files as $key=>$file) {
$fileNames[] = [
'file'=>$file->getFilename(),
'path'=>$file->getPathname(),
'size'=>$file->getSize(),
'ext'=>$file->getExtension()
];
}
return response()->json($fileNames);
}
/**
* upload
*/
public function upload(Request $request)
{
$request->validate([
'image' => 'required|image|mimes:jpeg,png,jpg,gif,svg|max:2048',
]);
// Create a new Imagick object
$imagick = new Imagick();
// Read the uploaded image
$imagick->readImage($request->file('image')->getPathname());
// Resample the image (change the resolution)
$imagick->resizeImage(100, 100, Imagick::FILTER_LANCZOS, 1);
// Save the image
$imageName = time() . '.' . $request->file('image')->getClientOriginalExtension();
$imagick->writeImage(public_path('images/' . $imageName));
// Clear the Imagick object
$imagick->clear();
$imagick->destroy();
$fileNames = [];
$imagesPath = public_path('images/');
$files = File::files($imagesPath);
foreach ($files as $file) {
$fileNames[] = [
'file'=>$file->getFilename(),
'path'=>$file->getPathname(),
'bytes'=>$file->getSize(),
'ext'=>$file->getExtension()
];
}
return response()->json($fileNames);
}
}
/resources/js/Image/Upload.vue
The Upload.vue component will display an image upload form and a script to interact with the form actions. Once the user selects an image, they will be able to preview the image before upload. Notice the API request made with Axios POST to /api/images. These routes are specified in /routes/api.php
<template>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
Upload Image Form
<div class="row m-1 g-1">
<div class="form-group col-md-12">
<input class="form-control" :class="[{'text-danger': 'image' in errors && errors.image.length > 0}]" type="file" @change="onFileChange" name="image" id="image" accept="image/*" >
</div>
<div v-if="'image' in errors" class="text-danger">
<span v-for="(error, index) in errors['image']" :key="'type-'+index">
{{error}}
</span>
</div>
</div>
<div v-if="preview" class="mt-3">
<img :src="preview" alt="preview" width="100" height="100" />
</div>
<div class="row m-1 g-1">
<div class="form-group col-12">
<button type="button" @click.prevent="cancelUpload" class="btn btn-sm btn-fw btn-danger float-end mx-2">
Cancel
</button>
<button class="btn btn-sm btn-fw btn-primary float-end" type="submit" @click.prevent="uploadImage">Upload Image</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
image: null,
preview: null,
user: {},
errors:{
image: []
},
};
},
methods: {
cancelUpload(){
let redirectTo = this.$route.query.redirect || { name: 'admin-images' };
this.$router.push(redirectTo);
},
async uploadImage(){
if(this.image)
{
let fd = new FormData();
fd.append('image', this.image);
try {
let resp = (await axios.post('/api/images', fd)).data.data;
let redirectTo = this.$route.query.redirect || { name: 'admin-images' };
this.$router.push(redirectTo);
} catch(error) {
this.errors = error;
}
}
else {
this.errors.image.push(`An image file is required for upload.`);
}
},
onFileChange(e) {
let file = e.target.files[0];
if (file) {
this.image = file;
this.preview = URL.createObjectURL(file);
}
}
},
created() {
console.log('Image/Upload COMPONENT CREATED.');
}
}
</script>
/resources/admin/js/Image/Index.vue
The Index.vue component will display a list of images that currently exist in the /public/images directory. Also notice the API GET request made to /api/images.
<template>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-10">
<router-link
class="btn btn-primary mb-2 float-right"
:to="{ name: 'image-upload', query: {redirect: $route.fullPath} }">
Image Upload
</router-link>
<div class="mb-4 row" v-for="(image, index) in images" :key="index">
<img
:src="`/images/${image.file}`"
class="img-thumbnail "
width="100"
/>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
images: [],
user: {}
};
},
methods: {
},
async created() {
let response = await axios.get("/api/images");
this.images = response.data.length ? response.data : [];
}
};
</script>
/resources/js/routes.js
In order to see the Image/Index.vue and Image/Upload.vue components we need to specify a route for each in routes.js. Because the path /images and /image/upload routes are handled via Vue we need to use a <router-link> inside of another Vue component order to navigate to /images and /image/upload.
<router-link class="nav-link active" :to="{name: 'image-index'}">Images</router-link>
<router-link class="nav-link active" :to="{name: 'image-upload'}">Upload</router-link>
Then configure the routes in /resources/js/routes.js like below:
import ImageIndex from "./Image/Index";
import ImageUpload from "./Image/Upload";
const routes = [
// add these 2 new routes
{
path: "/images",
component: ImageIndex,
name: "image-index",
},
{
path: "/image/upload",
component: ImageUpload,
name: "image-upload",
}
];
/routes/api.php
Finally we create API routes to upload and retrieve images. Notice both routes both go to /images and either Route::get or Route::post will be used to identify them properly in a request.
// GET request to ‘/api/images’
Route::get('/images', [App\Http\Controllers\Api\ImageController::class, 'index']);
// POST Request to ‘/api/images’
Route::post('/images', [App\Http\Controllers\Api\ImageController::class, 'upload']);